在Java虚拟机(JVM)中,可达性分析算法(Reachability Analysis)用于垃圾收集,以确定哪些对象是"可达"的,即哪些对象仍然有用,哪些对象可以被回收。下面是对可达性分析算法及其底层实现的详细描述。
可达性分析算法概述
可达性分析算法通过从一组称为"根对象"(GC Roots)的对象开始,遍历对象引用图,确定哪些对象是可达的。那些无法从根对象访问到的对象被视为不可达的,可以被垃圾收集器回收。
根对象(GC Roots)
在JVM中,根对象包括以下几类:
- 虚拟机栈中的引用:包括各个线程的栈帧中的本地变量表中引用的对象。
- 方法区中的类静态属性引用的对象:即静态字段引用的对象。
- 方法区中的常量引用的对象:如字符串常量池中的引用。
- 本地方法栈中的引用:即JNI(Java Native Interface)引用的对象。
- 所有被同步锁synchronized持有的对象
所以!GC Roots是很多很多个的,而不要看到Roots就认为只有一个
可达性分析过程
可达性分析的过程可以概括为以下几个步骤:
- 标记阶段 :
- 从GC Roots开始,将所有可达对象标记为活跃。
- 使用一种图遍历算法(如深度优先搜索DFS或广度优先搜索BFS)遍历对象引用图,找到所有可达对象。
- 清除阶段 :
- 在标记阶段之后,所有未被标记的对象被视为不可达,可以被垃圾收集器回收。
- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。否则分析结果的准确性就无法保证
- 这也是导致GC时必须"Stop the World" 的一个重要原因!!
底层实现
-
对象引用图:
- JVM内部使用一种称为"对象图"的数据结构来表示对象及其相互引用关系。对象图中的每个节点表示一个对象,每条边表示一个对象对另一个对象的引用。
-
标记-清除算法(Mark-Sweep Algorithm):
- 标记阶段:Collector从GC Roots开始,遍历对象图,对所有访问到的对象进行标记。标记通常通过在对象头部设置标志位来实现。
- 清除阶段:Collector遍历堆中的所有对象,回收那些未被标记的对象。
- 缺点 :
- 效率不高
- GC的时候需要stop整个应用程序
- 清理出来的内存不连续,即不会整合内存,而是复用垃圾对象的内存。会存在大量内存碎片
-
并发标记-清除算法:
- 在现代JVM中,为了减少垃圾收集带来的暂停时间(stop-the-world),通常会采用并发标记-清除算法。这些算法允许应用线程和垃圾收集线程同时工作。
- 三色标记法 :是并发标记的一种实现方式,通过将对象标记为白色、灰色和黑色来实现。
- 白色:尚未访问的对象。
- 灰色:已访问但未处理完引用的对象。
- 黑色:已访问且引用已处理完的对象。
- 并发标记阶段:
- 初始标记:标记从根对象直接可达的对象,通常会短暂暂停所有应用线程。
- 并发标记:与应用线程并发运行,标记所有可达对象。
- 最终标记:处理并发标记期间产生的引用变化,通常会有短暂停顿。
- 清除阶段:回收未被标记的对象。
-
分代垃圾回收:
- JVM通常采用分代垃圾回收策略,将堆分为年轻代和老年代。不同代使用不同的回收算法以优化性能。
- 年轻代使用复制算法(复制存活对象到新的区域,回收旧区域的所有对象)。
- 老年代使用标记-清除或标记-整理算法(将存活对象压缩到一侧,回收剩余空间)。
根对象和其他对象的连接
-
引用链:
- 根对象通过引用链(Reference Chain)连接到其他对象。每个对象都有字段指向其他对象,这些字段形成了对象图中的边。
- 在标记阶段,算法通过引用链从根对象递归或迭代地访问其他对象。
-
栈和静态变量的引用:
- 虚拟机栈中的本地变量表和方法区中的静态变量表保存着对对象的直接引用。这些引用成为可达性分析的起点。
通过以上步骤和机制,JVM能够有效地识别和回收不可达的对象,从而管理堆内存并保证应用程序的正常运行和性能。
对象的 finalization 机制-原文链接:https://blog.csdn.net/chengqiuming/article/details/119006432
对象的 finalization 机制
一 点睛
Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的 finalize() 方法。
finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
从功能上来说,finalize() 方法与 c++ 中的析构函数比较相似,但是 Java 采用的是基于垃圾回收器的自动内存管理机制,所以 finalize() 方法在本质上不同于C++中的析构函数。
二 注意
永远不要主动调用某个对象的 finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
- 在 finalize() 时可能会导致对象复活。
- finalize() 方法的执行时间是没有保障的,它完全由 GC线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会。因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收。
- 一个糟糕的 finalize() 会严重影响 GC 的性能。
三 对象是生存还是死亡?
由于 finalize() 方法的存在,虚拟机中的对象一般处于三种可能的状态。
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是"非死不可"的,这时候它们暂时处于"缓刑"阶段。一个无法触及的对象有可能在某一个条件下"复活"自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在 finalize() 中复活。
- 不可触及的:对象的 finalize() 被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为 finalize() 只会被调用一次。
以上3种状态中,是由于 finalize() 方法的存在而进行的区分。只有在对象不可触及时才可以被回收。
四 具体过程
判定一个对象 objA 是否可回收,至少要经历两次标记过程:
- 如果对象 objA 到 GC Roots 没有引用链,则进行第一次标记。
- 进行筛选,判断此对象是否有必要执行 finalize() 方法。
- 如果对象 objA 没有重写 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,则虚拟机视为"没有必要执行",objA 被判定为不可触及的。
- 如果对象 objA 重写了 finalize() 方法,且还未执行过,那么 objA 会被插入到 F-Queue 队列中,由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其 finalize() 方法执行。
- finalize() 方法是对象逃脱死亡的最后机会,稍后 GC 会对 F-Queue 队列中的对象进行第二次标记。如果 objA 在 finalize() 方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA 会被移出"即将回收"集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize() 方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的 finalize() 方法只会被调用一次。
在进行第一次清除的时候,我们会执行 finalize方法,对象进行了一次自救操作,但是因为 finalize() 方法只会被调用一次,因此第二次该对象将会被垃圾清除。