该系列博客暂且定义为《深入理Java解虚拟机》的笔记,有些坑等后续看完书再填,有不对的地方多指教
引言:上一节介绍了Java的内存运行时的各个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随属于线程私有,生命周期与线程相同。
如果对内存模型不熟悉请移步:上一节:【回头学Java-虚拟机篇】——1.内存模型
这几个区域的内存的分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法或线程结束,内存就自然回收了。而Java堆和方法区属于共享区域。一个接口中多个实现类需要的内存可能不一样,一个方法中不同分支需要的内存也不同,只有在程序处于运行期间才能知道创建那些对象,这部分内存的分配和回收都是动态的,垃圾回收器也关心这部分区域。
1.垃圾回收的三个问题:
1)哪些内存需要回收?
2)什么时候回收?
3)如何回收?
1)哪些内存需要回收
a.内存回收的对象主要为Java堆和方法区,堆主要回收对象,方法区主要回收废弃常量和无用的类。
b.垃圾收集器在对堆进行回收之前,先判断对象是否"死去"(也就是对象不可能再被任何途径使用)。
c.判断对象死去的两个方法:
1.引用计数法:即给对象添加一个引用计数器,每当有一个地方引用到它,计数器值就+1,引用失效时,计数器值-1;任何时刻计数器值为0的对象就是不可以再被使用的对象。缺点:对于循环引用的对象,即使已经没有其他引用,但是计数器值一直不为0,导致不能够清除。A a = new A();B b = new B();a.setB(b);B.setA(a);A=null;B=null;System.gc();对象A()和B()也不会被清除。
2.可达性分析算法:基本思想就是通过一些列的称为“GC Roots”的对象为起始点,从这些节点向下搜索,做过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就说对象是不可用的。目前大多数商用语言都用这种方式(Java,C#等)。
3.可达性分析算法中的GC Roots 通常指以下几个:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
2)方法区中类静态属性引用的对象。
3)方法区中常量引用的对象。
4)本地方法栈中Native引用的对象
4.对于方法区中无用类的定义:
1)该类的实例都已经被回收,也就是java堆中不存在该类的任何实例。
2)加载该类的ClassLoader已经被回收。
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
2)什么时候回收
a.无论是新生代还是老年代的垃圾收集,设置阈值,在内存不够使用的时候进行垃圾收集。
b.也有可以设置的收集间隔时间,每隔一段时间即做垃圾收集
3)如何回收
a.垃圾回收算法:
1.标记-清除算法:算法分为标记和清除两个阶段:首先标记处所有需要回收的对象,在标记完成之后统一回收被标记的对象(标记的对象就是上面讲的需要回收的对象)。如下图所示,有两个不足的地方:
1)效率问题,标记和清除两个过程效率都不高
a.标记效率不高是因为堆中新生代每次回收会有70%-95%,所以要标记大部分的对象
b.清除效率不高也是需要遍历标记的对象去删除,需要寻址。
2)空间问题,标记清除之后会产生大量不连续的内存碎片,可能会导致之后大对象没有足够内存分配,不得不提前触发GC。
2.复制算法:将内存分为大小相同的两块,如A,B,每次使用其中一块比如A,待A内存不足需要回收的时候,将还存活的对象复制到B上,然后把A的内存一次清理掉(效率高)。之后分配的对象就在B上,待B不足的时候做同样的操作到A,周而复始。这个算法虽然高效,但是的缺点很明显即可用内存缩小为原来的一半...
3.增强版复制算法:现在的商业虚拟机都采用复制算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%都是“朝生夕死”的,所以并不需要1:1的比例来划分内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor内存,当需要回收的时候,将活着的对象一次性复制到另一块Survivor上,Hotpot默认的内存大小比例为8:1:1,这样内存有90%利用率(80%Eden + 10%Survivor),回收前和回收后的效果图如下图所示,但是我们不能保证每次回收只有不多于10%的对象存活,当Survivor空间2内存不足时,需要将对象写到其他的内存中,如老年代。
缺点1.当回收的时候存活的对象过多时,会导致使用较多的复制操作,效率会变低,所以该算法比较适合每次回收时存活对象较少的新生代。
缺点2.当回收后的对象所需要的内存,大于survivor空间内存,需要额外的内存来担保。
4.标记-整理算法:和标记清除第一步一样,先对需要回收的对象做标记,并不将其直接删除,而是将所有存活的对象都向一端移动,然后将存活对象尾部到内存边界的整个内存一次性清理。这样回收后的内存是整理过的,大对象也好分配内存。但是需要标记和移动两个过程比较耗时,回收过程如下图所示:
5.分代收集算法:只是将上述不同的算法应用到不同的堆中,
1)在新生代,每次垃圾收集时都会有大批对象死去,只有少量存活,所以采用复制算法,如第3个。
2)老年代中,因为对象存活率高、没有额外的空间对它进行分配担保,所以使用“标记-清除”或者“标记-整理”算法。
2.Hotspot虚拟机的对于算法的实现:
1).Stop the world:
上面我们讲了如何判断对象是否可回收的时候,讲了需要从GC Roots出发,来找那些可回收的内存,无论是标记回收对象还是可用对象都需要。对于Stop the world(将整个世界停止)的理解,网上有个童鞋举的例子很好,垃圾回收如同打扫房间,你不希望有人在你打扫的过程中,再去乱扔东西。所以虚拟机在GC的时候几乎会将所有线程暂停执行,直到垃圾回收完成,即使是号称不会发生停顿的CMS(后面会讲)收集器,在枚举GC Roots的过程中也要停顿,线程何时停顿呢?
a.安全点:目前主流虚拟机会设置一个标志位表示将要进行GC了,所有线程在执行的过程中都需要去轮询这个标志位,可是如果每次执行一个指令就轮询一次这样是非常耗资源的,所以只在某些指令执行的时候去轮询,这个点被称为安全点(Safepoint), 安全点的选择的标准通常为“是否具有让程序长时间执行的特征”。通俗的讲就是执行这个指令之后,后面会不会通常跟着长时间的执行逻辑,比如方法调用,循环跳转,异常跳转等,它们通常要比int a=4;这种指令后还会执行很多指令的概率大很多。这些安全点在类加载的时候就选择好了。
如果GC的时候,线程还没有执行到安全点怎么办,目前有两种方案:
1.抢断时中断(Preemptive Suspension):这种方式不需要线程配合,在GC发生的时候,将所有线程中断,如果发现有线程中断的地方不是安全点,则恢复线程让其继续执行到安全点,目前很少有虚拟机采取这个方式。
2.主动式中断(Voluntary Suspension):上面说过的设置标志位,线程轮询的模式就是这种方式,轮询时机于到达安全点的时间一致。
b.安全区域:安全点似乎已经完美解决了如何进入GC的问题,但是实际情况中,如果存在程序不执行的情况,就不会走到安全点,例如线程处于sleep状态或者blocked状态,这时候线程无法执行到安全点。就需要安全区域来解决。
安全区域是指一段代码之中,引用关系不会发生变化。在这个区域内任何地方开始GC都是安全的。线程执行到安全区域的代码时,首先标识自己已经进入安全区,当在这段时间GC时,就JVM就不需要管已经标识过的线程了。在线程要离开安全区域时,它要检查系统是否已经完成了根节点枚举(或者整个GC过程,算法实现不同),如果完成了就继续执行,否则就等待到直到收到可以离开安全区的信号为止。
2)垃圾收集器:
下图为主要的几款垃圾收集器,上面为新生代收集器,下面为老年代收集器,有连线相连表示何以配合使用。
a.Serial/Serial Old收集器:
1.整个GC过程,所有线程暂停。
2.单线程处理GC这个过程。
3.新生代使用复制算法,老年代使用标记-整理算法。
b.ParNew收集器:
1.整个GC过程,所有线程暂停。
2.新生代多线程处理GC,老年代单线程。
3.新生代使用复制算法,老年代使用标记-整理算法。
c.CMS(Concurrent Mark Sweep)收集器:老年代垃圾收集器,它是以获取最短回收停顿时间为目标的收集器。流程如下:
1.初始标记:标记一下GC Roots能直接关联到的对象。
2.并发标记:根据初始标记的对象跟踪到所有对象。
3.重新标记:为了修正在并发标记期间因用户程序继续执行而导致标记产生变动的那一部分对象的标记记录。
4.并发清理:垃圾回收的过程。
5.CMS缺点:
1)对CPU资源非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部CPU资源而导致应用程序变慢,总吞吐量降低。
2)CMS无法收集浮动垃圾,即收集的过程中,用户线程还会继续产生垃圾,CMS无法在这次GC中处理他们,会将他们留到下次GC中处理。所以要预留一部分空间,在JDK1.6后,CMS收集器启动的阈值为92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure,这是会启用后备预案,临时启动Serial Old来重新手机老年代。
3)CM采用标记-清除算法实现,这个算法会有大量的空间碎片。
d.G1收集器:
1.使用G1收集器,堆内存布局就有很大变化,它将这个堆分为多个大小相等的独立区域(Region),虽然也保留新生代和老年代的概念,但是他们不再是物理隔离的了,都是一部分Region的集合。
2.G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
3.G1为每个Region都有一个Remembered Set,虚拟机发现程序在对引用类型的数据进行写操作时,会检查引用的对象是否处于不同的Region中,如果是,则把相关引用的信息写入Remembered Set中,保证了不需要全堆扫描。
4.执行过程:
1)初始标记:标记一下GC Roots能直接关联到的对象。
2)并发标记:根据初始标记的对象跟踪到所有对象。
3)最终标记:在并发标记的过程中,系统会记录期间对象变化,最终标记会将这部分记录合并到Remembered Set中。
4)根据各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。