JVM即Java Virtual Machine(Java虚拟机)的缩写,身为一名java开发者,适当了解JVM,拓展一下知识面并无坏处,本人结合最近的学习对JVM作了简单总结,现给你们分享。java
class loader顾名思义是类加载器,咱们的类文件(.class)是保存在硬盘上的,若是想要被jvm执行,须要有一个中间层把它加载到jvm中,这个工做就是由class loader作的,它经过IO流的形式把.class文件载入到虚拟机,类加载器分四种:c++
这部分是由c/c++编写的,属于最底层的类加载器。他会加载$JAVA_HOME/jre/lib/rt.jar
中的全部类,这个jar包中有咱们经常使用的最基本的类,好比java.lang.Object、java.lang.String等,这也就解释了为何咱们在使用这些类时不须要导包的缘由,启动类加载器已经事先加载到jvm中了。编程
使用java编写,它会加载$JAVA_HOME/jre/lib/ext/*.jar
。dom
也叫系统类加载器,使用java编写,加载当前应用的$CLASSPATH
中的全部类。eclipse
Java.lang.ClassLoader的子类,用户能够定制类的加载方式。(通常用不到)jvm
提到类加载器,就不得不提这两个机制,所谓双亲委派是指:当应用类加载器接收到一个加载类的请求时,不会立刻进行加载,而是委托给它的父类加载器——扩展类加载器去加载,而扩展类加载器又委托给启动类加载器,若是启动类加载器在它的范围内没有找到该类,则会抛一个ClassNotFoundException异常,这时它的子类加载器才会逐级向下去尝试加载,直到找个这个类。那么这有什么意义呢?设想,假如你建了一个java.lang的包,又在该包下建了一个String类,若是没有这个双亲委派机制,那么你本身写的String类是否是就把jre标准的String给覆盖了?java为了保护自身标准的类不会被覆盖,因而就采用了双亲委派把这些类隔离开来,也就是所谓的“沙箱机制”。编程语言
能够经过java.lang.Class<T>中的getClassLoader方法来获取当前类加载器。学习
public class JVMTest01 { public static void main(String[] args) { Object obj = new Object(); System.out.println(obj.getClass().getClassLoader()); JVMTest01 test = new JVMTest01(); System.out.println(test.getClass().getClassLoader()); System.out.println(test.getClass().getClassLoader().getParent()); System.out.println(test.getClass().getClassLoader().getParent().getParent()); } }
输出结果:spa
null sun.misc.Launcher$AppClassLoader@2a139a55 sun.misc.Launcher$ExtClassLoader@7852e922 null
咱们来分析一下这个结果,第二行和第三行的输出应该容易理解,JVMTest01是一个用户自定义的类,是由应用类加载器加载的,而它的父类加载器是扩展类加载器。但奇怪的是第一行和第四行的结果,为何是null?咱们知道Object类是由启动类加载器加载的,应用类加载器的父类的父类加载器也是启动类加载器,那为何获取不到呢?由于启动类加载器是jvm最底层的直接跟操做系统打交道的接口,是由c++编写的,已经很底层了,单靠java已经获取不到了,因此是null。操作系统
执行引擎负责解释指令,提交给操做系统执行。
本地接口的做用是融合不一样的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生之初正是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,因而就在内存中专门开辟了一块区域处理标记为native的代码,它的具体作法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。
目前该方法使用的愈来愈少了,除非是与硬件有关的应用,好比经过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。由于如今的异构领域间的通讯很发达,好比可使用 Socket通讯,也可使用Web Service等等。
它的具体作法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个很是小的内存空间,几乎能够忽略不记。
静态变量+常量+类信息+运行时常量池存在方法区中,该区被全部线程共享。
注:实例变量存在堆内存中,和方法区无关
栈主管Java程序运行,是在线程建立时建立,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来讲不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致,是线程私有的。
栈帧中主要保存3类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
栈操做(Operand Stack):记录出栈、入栈的操做。
栈帧数据(Frame Data):包括类文件、方法等等。
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
A方法又调用了 B方法,因而产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,因而产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
遵循“先进后出”/“后进先出”原则。
如图:
栈帧 2是最早被调用的方法,先入栈,而后方法 2 又调用了方法1,栈帧 1处于栈顶的位置,栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1和栈帧 2,线程结束,栈释放。
设想:若是方法中不断调用方法,栈帧一帧一帧的往上堆叠,终于超过了栈空间的上限,因而就报了java.lang.StackOverflowError
。这就是无限递归调用:
public void test() { test(); }
调用这个方法就会产生这个结果:
图中表示的关系是这样的:在栈中,保存了局部变量(基本类型+引用类型),而引用类型指向了堆内存中的一块对象实例,而这个实例是依据什么为蓝图建立的呢?就是存在于方法区中的类信息,它记录了该类的“DNA”,基于该类的全部实例都以此为模版进行建立。
注:本地方法存在于本地方法栈中,和普通Java方法不在同一个栈
一个JVM实例只存在一个堆内存,堆内存的大小是能够调节的,堆内存分为三部分:
注:JDK1.8开始,永久区替换为了元空间
新生区又分为:
图例:
全部的对象都是在伊甸区被new出来的,当伊甸园的空间用完时,程序又须要建立对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的再也不被其余对象所引用的对象进行销毁。而后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,而后移动到 1 区。那若是1 区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC以后发现依然没法进行对象的保存,就会产生OOM异常java.lang.OutOfMemoryError
。
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
若是出现java.lang.OutOfMemoryError: PermGen space
,说明是Java虚拟机对永久代Perm内存设置不够。通常出现这种状况,都是程序启动须要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终致使Perm区被占满。
注:
Jdk1.6及以前:有永久代, 常量池1.6在方法区
Jdk1.7:有永久代,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及以后:无永久代,常量池1.8在元空间
经常使用参数:
public static void main(String[] args) { long maxMemory = Runtime.getRuntime().maxMemory(); //返回 Java 虚拟机试图使用的最大内存量 long totalMemory = Runtime.getRuntime().totalMemory(); //返回 Java 虚拟机中的内存总量 System.out.println("MAX_MEMORY = " + maxMemory + "Byte " + (maxMemory / (double)1024 / 1024) + "MB"); System.out.println("TOTAL_MEMORY = " + totalMemory + "Byte " + (totalMemory / (double)1024 / 1024) + "MB"); }
在eclipse中配置jvm参数:
输出结果:
由图,咱们利用-Xms和-Xmx参数将初始内存和最大内存都设置为1024MB(实际结果981.5MB属于偏差)
注:永久代/元空间 只是JVM逻辑上有这么一块区域,但实际物理内存中并不存在,如何证实呢?如图:新生代+养老代 的内存总和已经等于TOTAL_MEMORY,说明实际内存中只有新生区和养老区,永久代/元空间只是逻辑上存在。
public static void main(String[] args) { String str = "hello world!"; while (true) { str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999); } }
参数配置:
-Xms8m -Xmx8m -XX:+PrintGCDetails
运行结果:
分析:咱们故意把堆内存调小至8M,而后再不断地在堆中生成String对象,直到产生OOM异常,从输出日志中能够看到,在抛出异常前JVM不断进行GC,直到最后一次Full GC以后,堆内存依旧没有足够的空间new出新的对象,因而就抛出了OOM异常。通常OOM异常都是在Full GC以后产生的。
-XX:+HeapDumpOnOutOfMemoryError
这个长参数是比较特别的,因此这里单独提一下,它的做用是当JVM产生OOM异常时,生成一个dump文件到你的工程目录下,能够配合eclipse的MAT(Eclipse Memory Analyzer)插件分析内存泄漏。