在堆里面存放着Java世界中几乎所有的对象实例,下面就让我们了解下垃圾收集器在对堆进行回收前,如何判断哪些对象还存活,哪些已经死去
实现的原理:大致是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。但是Java虚拟机没有选择引用计数算法来管理内存,其中最主要的原因是它很难处理对象之间相互循环引用的问题。
通过一系列的称为’GC Roots‘的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当从GC Roots到这个对象不可达时,则证明此对象是不可用的,将会被判定为是可回收的对象。
在Java中,可作为GC Roots的对象包含下面几种:
虚拟机中引用的对象
方法去中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用
无论是哪种方式,判定对象是否存活都与引用有关。
强引用: 指在程序代码之中普遍存在,类似Object obj = new Object();这类的引用,只要强一弄还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用:用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够,才会抛出内存溢出异常。Java提供了SoftReference类来实现软引用。
弱引用:用来描述非必需对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够都会回收掉。Java提供了WeakReference类来实现弱引用。
虚引用: 唯一目的是能在这个对象呗收集器回收时收到一个系统通知。Java提供了PhantomReference类
下面让我们比较细致的了解下可达性分析算法是如何回收对象的。
判断一个对象是否死亡,至少要经历两次标记过程:
在判断没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选条件是:是否有必要执行finalize()方法,当对象没有覆盖这个方法,或者方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行
如果被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中。并由一个虚拟机自动建立,低优先级的线程去执行这个方法。finalize()方法使对象逃脱死亡命运的最后一次机会。在GC对F-Queue中对象进行第二次小规模的标记,如果对象哟啊在finalize()中成功拯救自己–(只要重新与引用链上的任何对象建立关联即可),否则就会被回收。
注:不建议使用这样的方式来防止对象被回收
很多人认为方法区是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且进行收集的效率也比较低。
收集的部分也分为两部分:废弃常量和无用的类。常量的回收方式也是通过判断是否有引用,在发生内存回收时,有必要的话就会被系统清理出常量池。
而判定无用的类的条件比较复杂,需满足:
1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,即没有通过反射访问该类的方法
满足这三个条件只是可以被回收,并不会在不使用就必然会回收。HotSpot虚拟机提供了-Xnoclassgc参数进行控制。
在大量使用使用反射、动态代理、CGLib等框架,都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块;
当这块内存用完,就将还存活的对象复制到另外一块上
再把已使用过的内存空间一次清理掉
让所有存活的对象都向一端移动
然后直接清理掉端边界之外的内存
其实是复制算法和标记清除的结合
根据对象存活周期的不同将内存划分为为几块
一般把Java堆分为新生代和老年代
在新生代中,每次垃圾收集时都发现大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
而老年待中因为对象存活率高,没有额外空间对他进行分配担保,就必须使用标记-清理算法进行回收
单一线程的收集器,同时在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
是虚拟机运行在Client模式下的默认新生代收集器
Serial收集器的多线程版本
在Server模式下的虚拟机首选新生代收集器
新生代收集器,也是使用复制算法的收集器, 也是并行的
目标是达到一个可控制的吞吐量(吞吐量= 运行用户代码时间/垃圾收集时间)
提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数还有自动动态调整-XX:UseAdaptiveSizePolicy
Serial收集器的老年代版本
也是单线程收集器,使用标记-整理算法
也是在给Client模式下的虚拟机使用
如果在Server模式下,它的用途:1:在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;2:作为CMS收集器的后备预案
Parallel Scavenge收集器的老年代版本
使用多线程和标记-整理算法
以获取最短回收停顿时间为目标的收集器
基于标记-清除算法实现的
收集的步骤:
初始标记 依然需要暂停线程,仅仅只是标记一些GC Roots能直接关联到对象
并发标记 进行GC RootsTracing的过程
重新标记 依然需要暂停线程
并发清除
以Region为单位进行垃圾回收
并行与并发
分代收集
空间整合: 与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部是基于“复制”算法实现的。
可预测的停顿
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(新生代GC)
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多数具备朝生夕灭的特性,so新生代GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/FULL GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。速度一般会比Minor GC 慢10倍以上。
所谓的大对象,是指需要大量连续内存空间的Java对象。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,当大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。
虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些应放在老年代中。–>为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后依然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它年龄增加到一定程度,就将会被晋升到老年代中。可以通过设置-XX:MaxTenuringThreshold来改变
虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
如果这个条件成立,那么Minor GC可以确保是安全的。
如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。
如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这是也要改为进行一次Full GC.