判断对象已死的方法
- 引用计数器法 : 在对象中添加一个引用计数器,每当有一个地方引用它,计数器值加一,当引用失效时,计数器值减一;在任意时刻计数器值为0时,对象不能再被使用。
扩展
Java 中不使用 引用计数器法,因为无法解决循坏引用的问题
Python 中使用 引用计数器法,解决方法是 使用弱引用weakref,weakref 是Python提供的标准库,旨在解决循环引用(只要发生了回收,弱引用都会被回收)
- 可达性分析算法:基本思路是枚举一系列被称为 “GC Roots” 的根对象作为起始点集,从这些节点出发根据引用关系向下搜索,如果一个对象到 “GC Roots" 没有任何引用链相连,说明该对象是不可达对象(被回收)。
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在 一个能保证一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致GC进行时必须“Stop The World"的一个重要原因。 (即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的)
扩展1
Java 中可以被作为 GC Roots 的对象有:
.1. 虚拟机栈中的引用对象(栈帧中的局部变量表),例如,方法的 参数,局部变量。
2. 方法区中类静态属性引用的对象,例如,Java 类中的引用类型静态变量。
3. 方法区常量引用的对象,例如,字符串常量池中的引用。
4. 本地方法栈中的引用对象。
5. Java 虚拟机内部的引用,如基本数据类型对应的 class 对象。
6. 所有被同步锁(synchronized 关键字)持有的对象。
扩展2.
引用的定义
在JDK1.2之前,引用的定义是,如果reference 类型的数据中存储的数值代表的是另一块内存的起始地址,就称该reference 数据代表某块内存,某个对象的引用。然而对于描述一些“食之无味,弃之可惜”的对象就显得无能无力了(例如,我们想要描述一些对象,这些对象在内存还足够的情况下,能保留在内存中,如果内存空间在垃圾回收后还紧张,就可以抛弃掉这些对象。
JDK 1.2之后,分为:
1)强引用:最传统的引用关系 ,例如, Object A = new Object() 这种引用关系。
2)软引用:还有用,但非必须存在的对象,被软引用关联的对象,在将要发生 OutOfMemoryError 之前,会将这些对象列进回收范围中进行二次回收。
3)弱引用:非必须对象,被弱引用关联的对象无论发不发生 OutOfMemoryError 都只能生存到下一次垃圾回收发生为止。
4)虚引用:一个对象是否有虚引用,完全不会对其生存时间有影响,虚引用的目的只是为了能在这个对象被回收时收到一个系统通知。
判断一个对象是否可以进行回收
扩展
finalize() 方法: Object 类中的方法
当垃圾收集器发现没有引用指向一个对象时,即垃圾收集器回收该对象之前,总会先调用这个对象的finalize( )方法。由于finalize()方法的存在,虚拟机中的对象可以分为三种状态:
1)可触及的对象:从GC Roots 到该对象没有任何引用链相连。
2)可复活的对象:对象的所有引用都被释放,但可能在finalize()方法中复活。
3)不可触及的对象:对象的finalize()方法被调用,并且没有复活,此时,对象进入不可触及的状态,不可触及的对象不能复活,因为finalize()只会调用一次。
判断一个对象是否可以回收最多会经历两次标记。
- 如果一个对象到GC Roots 没有任何引用链相连,则进行第一次标记。
- 进行筛选,筛选的条件是该对象有没有必要执行finalize()方法。
1)如果该对象没有重写finalize()方法,或者finalize()方法已经执行过一次,则判断该对象没有必要执行finalize()方法,对象进入不可触及的状态。
2) 如果一个对象重写了finalize()方法,且还没有执行过,那么该对象会被放到一个被称为 F-Queue的队列中,稍后虚拟机会自动创建一条低优先级的 FInalizer 线程去执行队列中对象的finalize()方法。 - 稍后收集器会对F-Queue 中的对象进行第二次标记,如果其中对象在finalize()方法中重新与引用链上的任意一个对象关联上,那么在第二次标记时它将移出“即将回收”的集合。
垃圾收集算法
- 标记清除算法(Mark Sweep)
1)标记:垃圾收集器从引用根节点出发开始遍历,标记所有被引用的对象,(一般在对象的Header 中标记为可达对象)
2)清除:垃圾收集器对堆内存从头到尾开始遍历,如果发现一个对象的Header 中没有被标记为可达对象,则将其回收。
扩展
优点:不需要额外的空间
缺点:1)执行效率不稳定,如果Java 堆中有大量对象,且大部分需要被回收,这是需要大量的标记和清除动作,耗时严重。
2)产生碎片内存,需要维护一个空闲列表进行内存分配。
- 复制算法:将内存空间分为两块,每次只使用其中的一块,,当这一块内存块使用完时,将还存活的对象复制到另一块内存中,将该内存空间一次清理掉。
扩展1
1)半区复制算法:1969年
2)Appel式回收:1989年,因为新生代对象的“朝生夕灭”的特点。内存空间划分:
新生代:1/3 堆空间,其中 8/10的Eden区,1/10的survivorFrom区,1/10 的survivorTo 区。
老年代:2/3 堆空间。(Hotspot VM)
扩展2
优点:不会产生碎片空间
缺点:1)会浪费掉一部分的有效内存,2)复制对象的同时,如果该对象被其他对象引用的话,还需更新引用地址。
- 标记整理算法(Mark Compact)
1). 标记:垃圾收集器从引用根节点出发开始遍历,标记所有被引用的对象,(一般在对象的Header 中标记为可达对象)
2)整理:将还存活的对象整理到内存的一端。
3)最后清理掉边界以外的所有空间。
扩展
优点:不会产生碎片内存,不会像复制算法浪费有效的空间。
缺点:1)效率比复制算法低。2)移动对象的同时,如果对象被其他对象引用的话,还需要更新引用地址。因此移动过程中需要暂停程序的工作线程。
HotSpot垃圾收集算法实现的细节
- 首先可达性分析算法中的根节点枚举过程,必须在一个保持一致性的快照中才能进行,不会出现分析过程中,根节点集合的对象引用关系还在发生变化的情况。否则分析结果的正确性不能保证。这也是在垃圾收集过程中必须暂停所有用户工作线程(STW)的一个重要原因。
- 当用户工作线程暂停下来以后,其实并不需要一个不漏的检查完所有的执行上下文和全局的引用位置,虚拟机有办法直接得到哪些地方存放着对象引用(准确式内存管理)。
准确式GC :JVM有办法知道哪些位置存放的是引用类型的数据
1)一旦类的加载动作完成,HotSpot 会把对象内什么偏移量上是什么类型的数据计算出来。(实现快速引用链查找)
2)即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。
当线程用户线程暂停下来的时候,收集线程会对栈上的内存进行扫描,看什么位置上存放的数据是引用类型的数据,被该引用指向的对象此次回收中不能被回收,但问题是栈中局部变量表中存放的数据不光有引用类型的数据,那些非引用类型的数据对垃圾回收没有任何用处,但是我们还是不得不对整个栈进行扫描,这无疑是一种资源的浪费。
一个自然的想法就是以空间换时间,也就是所谓的准确式内存管理。大概的思路就是:在某个时候把栈上哪些位置存放的是引用类型的数据记录下来,这样在GC 的时候就可以直接访问,而不必全都扫描一次了。JVM使用的是 OopMap 的数组结构实现的,它指出执行当前指令时栈中,寄存器中哪些位置存放的是引用类型的数据,以及在当前指令一直到某个指令之间,这些引用类型的数据不再发生改变。
然而导致引用关系发生变化的指令有很多,JVM如果为这样的指令都生成 OopMap 的话,这无疑是一种很大的开销,又因为每一条指令执行的时间其实都非常短暂,所以JVM选择只在一些具有长时间执行特征的指令(方法调用,循坏跳转,异常跳转等)后面也就是所谓的安全点生成 OopMap-。
- OopMap:保存GC Roots 节点,避免全局扫描去一一查找。
保守式GC:在进行GC的时候,JVM开始从一些已知位置(例如说JVM栈)开始扫描内存,扫描的时候每看到一个数字就看检查它是不是一个指向堆中的引用)
-
安全点(safe point):精简指令,为特定位置(安全点)上的指令生成对应的OopMap,工作线程暂停进行GC的位置也是在安全点。
-
安全区域 :在一段代码中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。处理没有被分配CPU时间的线程。
-
记忆集:记录从非收集区域指向收集区域的指针集合的抽象数据结构。作用:解决对象跨代引用带来的问题,缩减GC Roots 扫描范围。
为什么会有所谓的解决跨代引用造成的问题呢?如果按照前面 GC Roots 的定义的话,那么我们从 GC Roots 出发的话,按照引用关系进行遍历的话,那么肯定所有的存活对象都能遍历到,不会出现所谓的存活对象遍历不到的情况。
答案:如果是进行 Full GC 的话,那么上面的想法是正确的。不会出现所谓的跨代引用造成的存活对象遍历不到的情况。
其实根本的原因是:(这部分书中没有说到)进行 Young GC 时的GC Roots不是全部的GC Roots,而只是其中的位于年轻代的GC Roots,可以理解为 Young GC Roots (Young GC Roots 是 GC Roots 的一个子集),所以会出现 Young GC 时,因跨代引用造成的存活对象判定为死亡的情况。Old GC 同理。
卡表:记忆集的一种实现方式,最简单的形式可以只是一个字节数组,数组的索引标识着内存区域特定大小(HotSpot 虚拟机是512字节)的内存块(卡页),标识为1表明卡页中存在跨代引用的对象。标识为0 表示没有。
- 写屏障:解决卡表维护的问题,(例如,它们何时变脏(一般是在引用类型的字段赋值的那一刻))。
- 用户线程与收集器并发工作带来的问题:
1) 原本消亡的对象变成存活的
2)原本存活的对象变成消亡的(对象消失)
(三色标记工具)对象消失的条件,同时满足以下:
1)赋值器插入了一条或者多条从黑色对象到该白色对象的新引用
2)赋值器删除了所有从灰色对象到该白色对象的直接或间接引用
解决方法
1)增量更新:如果发生了条件1,将这个新插入的引用记录下来,并发扫描结束后,将这些记录过的引用关系中的黑色对象为根,重新扫描一次。(CMS)
2)原始快照:如果发生了条件2.,将这个要删除的引用记录下来,并发扫描结束后,将这些记录本过的引用关系中的灰色对象为根,重新扫描一次。(G1)
经典垃圾收集器
串行收集器
- serial 收集器(新生代收集器):采用复制算法、串行回收 和 “Stop the World” 机制的方式执行内存回收。
- serial old 收集器(老年代收集器):标记一整理算法,串行回收 和 “Stop the World” 机制。
并行收集器
- ParNew收集器(新生代收集器):复制算法、并行回收 和 “Stop the World” 机制,是Serial收集器的多线程版本
- Parallel Scavenge收集器 (新生代收集器):同样也采用了复制算法、并行回收 和 “Stop the World” 机制。目的是:达到一个可控制的吞吐量。(他提供了两个参数用于精准控制吞吐量,分别是设置最大垃圾收集停顿时间的 -XX:MAxGCPasueMIllis,与设置吞吐量大小的 -XX:GCTimeRatio)
吞吐量:处理器用于运行用户代码的时间/处理器的总消耗时间
- Parallel 0ld 收集器(老年代收集器):标记一整理算法,并行回收 和 ”Stop the World" 机制
并发收集器
- CMS 收集器(老年代收集器):标记一清除算法,并且也会 “Stop the World” 。它以最短回收停顿时间为目标
首先GC Roots 枚举阶段,因为 GC Roots 相对于堆中的所有对象来说,算是极少数,并且在各种优化技巧(OopMap)下,它带来的停顿时间非常短暂且固定(不随堆容量而增长),所以CMS只能想办法在查找引用链这一阶段去做文章了,使得这一阶段用户的工作线程能与垃圾收集线程并发工作
1)初始标记:仅仅只是标记出和GCRoots能直接关联到的对象,有stw现象
2)并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
3)重新标记:修正并发标记阶段,因为用户线程同时运行而导致标记发生变化的那部分对象的标记记录,有stw现象
4)并发清除:清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。(在该阶段,因为用户线程继续运行,所以会可能产生新的垃圾对象(浮动垃圾))
优点:并发收集,低延迟
缺点:
1)对CPU资源敏感,因为要分一部分处理器资源去执行收集器线程
2)会产生碎片空间(解决方法是,当碎片空间过多不得不进行FULL GC时,整理碎片空间)
3)因为在并发清除阶段,用户线程还在同时并发运行,所以要预留一部分空间给程序。所以不像其他收集器那样,等老年代空间几乎填满的情况下才进行GC,所以由于CMS 处理器无法处理浮动垃圾,当预留的内存无法满足程序分配新对象的需要,就会出现 ”并发失败“,此时虚拟机不得不启用 Serial Old 来重新进行老年代的垃圾回收。
- G1收集器(整堆收集器):面向局部收集的设计思路 和基于Region 的内存布局。它以回收尽可能多的垃圾为目标
1)初始标记:仅仅只是标记出和GCRoots能直接关联到的对象,有stw现象
2)并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
3)最终标记:暂停用户线程,用于处理并发阶段结束后遗留下来的STAB 记录。
4)筛选回收:负责更新 Region 的统计数据,对各个Region 的回收价值,成本进行排序,根据用户所期望的停顿时间来制定回收计划。使用复制算法进行垃圾回收。因为需要复制存活对象,所以要更新其引用地址,所以有stw现象。