一、如何判定对象是否还存活
1.引用计数算法
具体内容:在对象中增加一个引用计数器(占很小一部分内存),当一个对象被引用时加1,去除引用时减1。简单高效。
缺点:当两个对象相互引用时,计数器各不为0,无法进行垃圾回收,但是这两个对象没有实际用处,则会导致内存泄漏。
拓展:python在管理内存时使用的是引用计数算法,如何解决循环引用?
2.可达性分析法
具体内容:以GC Roots作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何的引用链相连,则证明此对象是不可能再被使用的。
固定可作为GC ROOTS的对象包括一下几种:
①在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如当前正在运行的方法所使勇到的参数、局部变量、临时变量等。
②在方法区中,类静态属性引用的对象,譬如Java类的引用类型静态变量。
③在方法区中,常量引用的对象,譬如字符串常量池里的引用。
④在本地方法栈中,JNI引用的对象。
⑤Java虚拟机内部的引用,如基本数据类型对应的Class对象(Interger),一些常驻的异常对象(NullPointException)等,还有系统类加载器。
⑥所有被同步锁(synchronized)持有的对象。
⑦反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
⑧分代收集中跨代引用的对象。
3.四种引用
①强引用(Strong):最传统的引用的定义,指在代码之中普遍存在的引用赋值。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
②软引用(Soft):还有用,但是非必须的对象。内存不够时回收。
③弱引用(Weak):不管用不用,只要在进行gc时就会自动回收。
④虚引用(Phantom):没什么用处,基本是用来进行垃圾回收时的事件监测。跟埋点一样。
4.对象复活
要宣告一个对象真正死亡,需要两次标记过程。
第一次标记之后,如果是不可达对象,则在进行finalize方法时会将对象放到一个队列(F-Queue)当中,排队等待回收。如果在回收的过程中有引用连接到该对象,则对象复活。但是一个对象只能调用一次finalize方法,在下一次变成不可达对象时无法再次调用finalize方法,直接对该对象进行回收,不会再放到队列中了。
5.回收方法区
方法区主要回收两个内容:废弃的常量和不再使用的类。
如何判定一个常量是否可回收。
①该类所有的实例都已经被回收。
②加载该类的类加载器已经被回收。
③该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问。
二、垃圾收集算法
HotSpot主要是用追踪式垃圾收集的方式
1.分代收集理论
①弱分代学说:绝大多数对象都是朝生夕灭的。
②强分代学说:熬过越多次垃圾回收的对象就越难消亡。
结论:将垃圾收集分为不同的区域,方便进行收集部分区域,HotSpot一般将内存区域分为新生代和老年代。新生代进行Minor GC、老年代进行Major GC,Full GC则表示全内存回收。
③跨代引用假说:跨代引用相对于同代引用来说占少数。
结论:存在相互引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。若老年代中存在对新生代的引用,我们不必大费周章的遍历整个老年代,只需要在新生代中建立一个全局的Remembered Set,把老年代划分为若干小块,标识出老年代的哪一块内存会存在跨代引用。
2.标记-清除算法(Mark-Sweep)
定义:首先标记出所有需要回收的对象,在标记完成之后,统一回收掉所有被标记的对象。也可以反过来标记所有存活的对象,统一回收掉未被标记的对象。CMS用于老年代中。
缺点:标记清除执行效率会随着对象数量的增长而降低、容易造成碎片化内存。
3.标记-整理算法(Mark-Compact)
定义:针对老年代对象的存亡特征,标记整理算法会将所有存活的内存移动至同一块区域,直接清理掉区域以外的内存。解决了标记清除算法造成的内存碎片问题。
缺点:移动操作是STW操作,会导致垃圾回收效率低。
4.标记-复制算法(Mark-Copy)
定义:将内存分为大小相等的两块,每次只使用其中一块。先标记出所有需要回收的对象,将不需要回收的对象复制到另一块内存空间中,然后进行垃圾回收。主要用于新生代中,因为新生代中的大部分对象要被回收。
缺点:虽然很高效但是浪费空间。
三、算法细节实现
1.根节点枚举(第一次标记)
必须暂停用户线程。虽然目前耗时的查找引用链的过程已经可以和用户线程并发执行,但是根节点枚举始终还是必须在一个能保证一致性的快照中才得以进行。也就是先拍照,根据照片上的情况查找引用链。
在暂停用户线程后,HotSpot虚拟机使用一组成为OopMap的数据结构来直接得到哪些地址存放着引用的。在类加载动作完成的时候,HotSpot会把对象内 偏移量上的数据类型计算出来,也会在特定的位置(安全点)记录下栈里和寄存器里哪些位置是引用。
2.安全点(SafePoint)
安全点一般是在指令序列的复用处,如方法调用、循环跳转、异常跳转等。
例子:比如要进行垃圾回收了,HotSpot将FLAG设为TRUE,各线程轮询到FLAG为TRUE之后会在安全点挂起,遍历OopMap,找到未存活的对象,进行清理。
3.安全区域
由于各种原因,线程无法正常走到安全点进行垃圾回收操作,则将一个个安全点连接起来,形成安全区域。在这个安全区域中,确保引用关系不会变化,因此在这个安全区域中任意地方开始垃圾回收都会是安全的。直到完成根节点枚举时方可离开。
4.记忆集和卡表
记忆集:为了解决对象跨代引用产生的。是一种用于记录非收集区域指向收集区域的指针集合的抽象数据结构。空间占用和维护成本高昂。
卡表:记忆集的实现形式,定义了记忆集的记录精度、与堆内存的映射关系等。简单来说,用卡表来表示哪个内存区域(卡页)含有跨代指针,如果有跨代指针,则目标卡页则会"变脏",也就是修改了标识。
5.写屏障
维护卡表状态。
6.并发的可达性分析
在老年代的标记-整理或者标记-清除算法中并发标记。减少STW时间。
三色标记法:
白色是未被访问过的对象。
黑色是已经被访问过的对象,且这个对象的所有引用都已经扫描过。
灰色是已经被访问过的对象,但这个对象上至少存在一个引用还没被扫描过。
有并发就会有线程安全的问题。导致“对象消失”。
如何解决对象消失:
1.增量更新(CMS)
黑色对象一旦新插入了只想白色对象的引用之后它就变回灰色对象。
2.原始快照(G1、Shenandoah)
无论引用关系删除与否,都会按照刚开始扫描那一刻的对象图快照来进行搜索。
四、经典的垃圾回收器
1.Serial
HotSpot最基础、历史最悠久的收集器。单线程。STW会造成卡顿。
优点:简单高效。在新生代收集器中额外内存消耗最小的。没有线程交互的开销。适合运行在老系统的客户端模式下。
2.ParNew
实质上是Serial的多线程并行版本。新生代收集器,使用多线程并行进行标记复制。
最大的用处是能够配合CMS收集器(主要作用于老年代)一起工作。是第一款退出历史舞台的垃圾收集器。
3.Parallel Scavenge
新生代收集器。采用复制算法。并行收集的多线程收集器。
优点:Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。被称为吞吐量优先收集器。在这点上优于ParNew。
4.Serial Old
老年代收集器。是Serial的老年代版本。使用标记整理算法。
用处一是在JDK5以及之前搭配Parallel Scavenge收集器工作。用处二是作为CMS收集器发生收集失败时的后备预案。
5.Parallel Old收集器
老年代收集器。是Parallel Scavenge的老年代版本。支持多线程并行收集。基于标记整理算法实现。在JDK 6后开始提供,搭配Parallel Scavenge共同完成吞吐量优先的工作。
6.CMS收集器
CMS(concurrent Mark Sweep)并发标记清除收集器是一种以获取最短回收停顿时间为目标的收集器。
优点:并发收集、低停顿。
缺点:对处理器资源很敏感,吃资源。无法处理浮动垃圾,如果垃圾回收的速度赶不上垃圾产生的速度,则会进行一次Full GC,临时启用Serial Old收集器来重新进行老年代的垃圾收集。 标记清除算法会造成内存碎片,对大对象很不友好。
7.Garbage First
G1收集器面向服务端应用。在JDK9时成为服务端模式下的默认垃圾收集器。G1可以面向堆内存任何部分来组成回收集进行回收。哪块内存中存放的垃圾数量越多,回收收益最大,这就是Mixed GC模式。
G1将堆内存划分为多个大小相等的独立区域,每一个区域都可以根据需要,扮演新生代的Eden空间、Survivor空间、或者老年代空间。Humongous区域用于存放大对象,大对象默认为老年代。
如何解决跨代引用:每个region都有一个记忆集(哈希表)用于记录别的region指向自己的指针。双向的卡表结构(记录谁指向我,我指向谁),因此需要耗费更多的内存来维持工作。
在并发标记阶段如何与用户线程互不干扰的运行:CMS采用增量更新算法,G1通过原始快照算法实现。G1为每一个region设计了两个名为TAMS的指针,把region中的一部分空间划分出来用于并发回收过程中的新对象分配。如果内存回收的速度赶不上内存分配的速度,G1也要被迫冻结用户线程执行,导致Full GC而产生长时间STW。
G1和CMS比较并不是完全胜出的,G1在内存占用和程序运行负载都比CMS要高。目前在小内存应用CMS比较有利,大内存(6-8GB)G1会比较有利。
以上总结
五、新生代垃圾收集器
1.Shenandoah
与G1的不同之处:支持并发的整理算法。不使用分代收集。摒弃了记忆集,使用连接矩阵(connection matrix)来记录跨region的引用关系。
Shenandoah收集器的工作过程:
①初始标记:和G1一样,首先标记与GC Roots直接关联的对象。STW操作。
②并发标记:遍历对象图,标记全部可达对象。
③最终标记:处理剩余的SATB扫描,统计出回收价值最高的region,构成一组回收集。小停顿。
④并发清理:清理无存活对象的region。
⑤并发回收:并发复制过程,通过读屏障和转发指针解决并发过程中出现的问题。
⑥初始引用更新:把堆中指向旧对象的引用修正到复制后的新地址。小停顿。
⑦并发引用更新:真正开始进行引用更新操作。
⑧最终引用更新:修正存在于GC Roots中的引用。
⑨并发清理:回收垃圾region。
Shenandoah收集器的并发回收的核心是,转发指针。转发指针分散存放在每一个对象头前面。
缺点一:每一次对象访问会带来一次额外开销,不可避免。
缺点二:线程同步问题,用CAS解决。
2.ZGC
和shenandoah高度相似,是一款基于Region内存布局的,暂时不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
ZGC的Region具有动态性--动态创建和销毁,以及动态的区域内存大小。三种类型:
小型Region(2MB)
中型Region(32MB)
大型Region(容量不固定,可动态变化,但必须为2MB的整数倍)
ZGC染色指针:直接把标记信息记在引用对象的指针上,将少量额外的信息存储在指针上的技术。
三大优势:
可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用。
可以大幅减少在垃圾收集过程中内存屏障的使用数量。
可以作为一种可拓展的存储结构用来记录更多与对象标记、重定位过程相关的数据。
ZGC工作过程:
并发标记:遍历对象图做可达性分析
并发预备重分配:这个阶段需要根据特定的查询条件统计得出本次收集过程要清理的region
并发重分配:把重分配集中的存活对象复制到新的region上,并为他们维护转发表,记录新的转向关系。
并发重映射:修正整个堆中指向重分配集中旧对象的所有引用,