JVM初探
JVM
执行文件
JVM执行的文件为class文件,这个执行文件是什么意思呢,就是虚拟机能够识别的文件,类加载器加载链接初始化后将数据保存在JVM运行时数据区中的文件。
类装入子系统
JVM的类加载器为ClassLoader采用双亲委派模型机制进行加载类。
双亲委派模型机制:
根据父子关系一直往顶层找是否被其他父级parent类加载器加载过,如果找到加载过,返回;如果没有找到,在返回一个一个查找是否有加载的权限,如果有就加载;如果这个时候所有的父级parent类加载器都没有加载过而且没有权限加载,那么自己去加载。
注意:父级parent类加载器不是继承的父子关系,而是一个ClassLoader中的一个parent变量代表父级类加载器。
执行引擎
Java的执行引擎包括解释器,JIT即时编译器,垃圾回收器。解释器不用介绍了,将Java字节码指令翻译为机器能够识别的指令;JIT即时编译器是因为光靠解释运行的效率低,所以对于热点代码进行编译为机器指令以加快运行效率(刚开启的时候得先需要通过解释器解释运行,当符合热点代码的特征时,会直接编译成本地代码之后热点代码就可以直接运行不用在经过解释再运行了。即解释器先运行一段时间才能够真正提升效率);
垃圾回收器,Java能够流行的一个原因还有这个特点:他不用管理内存,由JVM的垃圾回收器自动进行回收垃圾,达到管理内存的目的。
垃圾回收器
JVM运行时内存结构为:PC程序计数器、本地方法栈、虚拟机栈、堆、方法区。其中程序计数器,本地方法栈、虚拟机栈等都是线程私有的,也就是每条线程都拥有这三块区域,而且会随着线程的创建而创建,线程的结束而销毁,因此这三个区域不需要垃圾回收。
堆中的内存回收
这里讲解下垃圾回收器如何判定一个对象是否为垃圾的算法:
1.引用计数器:
之前Java中判断垃圾的算法使用引用计数器进行判定的,对象中有一个字段保存着计数器,只要这个对象被引用了那么计数器就加1;释放了这个引用就计数器减1。垃圾回收器回收的时候看这个对象的引用是否为0,如果为0那么代表这个对象是垃圾,该被回收。
即A引用B,假如这个时候没有其他引用指向A,那么A的计数器为0,B的计数器是1;当A把B的引用释放之后,B也为0,下次垃圾回收的时候就会将A,B回收。
这么看好像也没有问题,但是如果我的两个对象互相引用对方,A要销毁依赖于B,而B销毁依赖于A,这个时候垃圾回收器就不会回收这两个对象,也就是无法解决循环引用的问题。
举例:创建A对象a,创建B对象b。
这个时候A的成员变量引用b,B的成员变量引用a。即a和b的计数器都为1,销毁a的时候发现b在引用a,销毁b的时候发现a在引用b。垃圾回收的时候就不会回收这两个对象,但是除此之外没有其他引用指向这两个对象。
循环引用会导致即使外界已经没有任何指针能够访问他们了,但是他们所占资源仍然无法释放的情况。
优点:不需要STW,找到计数器为0的对象直接进行清除
缺点:维护计数器空间和时间上都有所牺牲,而且无法解决循环引用。
2.GC Root(根可达算法)
上面的算法无法解决循环引用问题,如果这个时候规定一些永远不会被回收的对象只要能被这些对象引用,那么就不是垃圾,这个时候就是GC Root的算法。
一个对象只要一直往上找最终能找到GC Root那么就不是垃圾,如果找不到就是垃圾。这个向上找的链叫做引用链,和GcRoot没有任何链接的称为“垃圾”
**一般而言可以作为Gc Root根节点的有:方法区静态信息,方法区常量信息,Java虚拟机栈所引用的对象,本地方法栈所引用对象。 **
方法区中的内存回收
方法区用于存储已被虚拟机加载的类型信息,常量,静态变量,被即时编译器编译后的代码缓存。
也就是存放类型信息、常量、静态变量、即时编译器编译后的代码缓存、域信息、方法信息等。
如下图:
类型信息:
域信息:
方法信息
方法区中清除垃圾常量和垃圾类
1.常量:
常量不被引用,就会从常量池中清除
2.类:
需要满足以下条件:
1.该类的所有对象都已被清除
2.该类的java.lang.Class对象没有被任何对象或变量引用。只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除。
3.加载该类的ClassLoader已经被回收
关于回收的那些事
1.当触发GC时,对象被标记为垃圾就一定会被回收吗?
2.GCRoots就是固定的那些值吗?有没有可能会随着程序的运行而增加新的GCRoots?
1.当对象被标记为垃圾时,要清除其实还有一次标记过程,也就是说对象要被清除得经过两次标记过程:
当对象经过可达性分析后发现没有与GCRoots相关的引用链,他会被第一次标记,接着会进行判断是否要进行调用对象的finalize()方法,【判断条件:对象重写该方法并且该对象没有执行过该方法】。
第一种情况:
如果有必要执行finalize()方法的话,会将该对象添加到一个F-Quene的队列里面,稍后会有一个虚拟机自己建立并且调度优先级很低的Finalizer线程去执行对象的finalize()方法,由于该方法可能会陷入死循环或者执行缓慢导致队列中的其他对象永远不被调用finalizy方法,所以该方法并不一定保证能够执行完。接着稍后会对该队列中的对象进行第二次标记,如果还是没有对象引用它,那么将会被回收;如果有对象引用了它那么会进行将该对象移除队列中。
第二种情况:
如果没有必要执行【有两种情况:1.该对象没有重写该方法 2.该对象的finalizy方法已经执行过了】也就是说一个对象只有第一次GC被标记的时候可能逃逸不被回收,但是第二次GC标记的话就不行了。
提示:JAVA中并不提倡重写这个方法,最初是因为C和C++的人更容易接受JAVA。
2.GCRoots通常一般是以下对象:
1.在虚拟机栈的本地变量表中引用的对象
2.方法区中静态属性引用的对象
3.方法区中常量池中引用的对象
4.本地方法JNI中引用的对象
5.Class对象,常驻的异常对象(NullPointException,OOM等),系统类加载器
6.同步锁中持有的对象(比如Synchronized)
7.本地代码缓存
这些是固定的GCRoots集合,但是根据不同的垃圾收集器和当前回收区域内存的不同,还可以有其他对象“临时”加入GCRoots集合。
垃圾收集器中将堆划分为了不同的内存区域,这部分后面详解。当出现这么一种情况:老年代的对象引用年轻代的对象的时候,年轻代中的对象回收的时候不仅要遍历GCRoots是否有引用链之外,还需要遍历是否老年代有引用。这就意味着要把老年代全部遍历一遍才能确认是垃圾。
因此出现了记忆集这个概念:在新生代上建立一个数据结构(记忆集),这个结构里面将老年代的内存划分开,标识哪部分内存是跨域访问的对象(比如访问年轻代里面的对象)。当发生年轻代的GC时,会将这个数据结构里面的老年代对象标识为GC Roots进行扫描,而不用进行遍历整个老年代。【只有在记忆集中包含的小块内存里的对象才会被加入到GC Roots中】