文章目录
- 1. 引用计数法
- 优点
- 缺点
- 2. 可达性分析 Root Search
- 2.1 那些对象是GC Roots
- 2.2 引用的分类
- 2.3 回收方法区
- 3. 实现细节
- 3.1 GC的起点:节点枚举
- OopMap:帮助高效的根节点枚举
- 3.2 何时开始GC:安全点与安全区域
- 如何选取安全点
- 如何让程序进入安全点
- 3.3 程序不执行时的安全点:安全区域
- Reference
最近上班地铁上偶尔看看书,周末有空理一下,做个笔记。
下面说说GC过程中如何判断对象是否存活。
1. 引用计数法
用于微软COM(Component Object Model)计数、Python语言等,进行内存管理。原理就是在对象中添加一个引用计数器:
- 每当有地方引用他时,计数器加一;
- 引用失效时,计数器减一;
- 计数器为零时,对象就是不可能再被使用的。
优点
原理简单,判断效率高
缺点
单纯的引用计数很难解决对象间的相互循环引用问题,需要考虑很多额外情况
另外,Java虚拟机并不用引用计数来判断对象是否存活
2. 可达性分析 Root Search
Java、C#等主流商用程序语言凑采用可达性分析(Reachability Analysis)算法判定对象是否存活:
- 枚举一系列对象作为GC Roots
- 从GC Roots出发,搜索引用链,某个对象不可达时,则该对象判定为不可能再被使用(死亡)
2.1 那些对象是GC Roots
Java中,可作为GC Roots的对象包括:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,也就各个线程调用的对象;
- 方法区中,类静态属性引用的对象;
- 方法区中,常量引用的对象,比如字符串常量池中的引用;
- 本地方法栈(Native方法)引用的对象;
- JVM内部的引用,比如
- 基本数据类型(float、double、int、long、byte、char、boolean)对应的Class对象
- 常驻的异常对象(NullPointException、OutOfMemoryError)
- 类加载器(BootstrapClassLoader、ExtensionClassLoader、AppClassLoaderfab)等等;
- synchronized关键字持有的锁对象(一般是static对象,或者是当前类的Class对象);
- 反应JVM内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
在分代回收和局部回收过程中,可能会有临时对象加入GC Roots集合,来解决跨代引用等问题。
2.2 引用的分类
JDK1.2之后,Java扩充了引用的定义,不同引用在GC时的行为有所差异:
-
强引用(Strongly Reference):在代码中通过
Object o = new Object()
形势产生的引用关系。只要强引用存在,GC就不会回收被引用对象(哪怕OOM)。 -
软引用(Soft Reference):通过
SoftReference
类实现的引用。系统内存发生内存溢出前,会将软引用对象列入回收范围进行回收,如果回收后还没有足够内存,就抛出OOM。 -
弱引用(Weak Reference):通过
WeakReference
类实现的引用。无论内存是否足够,弱引用对象都会被GC。 -
虚引用(Phantom Reference):通过
PhantomReference
类实现的引用。虚引用无法对对象实际的生存时间造成影响,也不能通过虚引用来获得对象实例。虚引用的作用就是追踪对象的GC信息,在被回收时加入到关联的引用队列,可以得到一个通知或做些其他事。import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; public class PhantomReferenceExample {public static void main(String[] args) {Object obj = new Object();ReferenceQueue<Object> queue = new ReferenceQueue<>();PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);// 执行对象的清理操作Thread cleanupThread = new Thread(() -> {try {while (true) {PhantomReference<?> reference = (PhantomReference<?>) queue.remove();// 执行清理操作,比如释放资源System.out.println("Cleanup: " + reference);}} catch (InterruptedException e) {e.printStackTrace();}});cleanupThread.setDaemon(true);cleanupThread.start();obj = null; // 取消强引用System.gc(); // 手动触发垃圾回收// 在这里可以进行一些其他操作} }
根可达算法中判定为不可达的对象,可以在finallize()方法中重新增加引用关联来复活(只能复活一次,try…finally也行)
2.3 回收方法区
《Java虚拟机规范》:可以不要求虚拟机在方法区中实现垃圾回收(比如JDK11 的ZGC收集器就不支持类卸载)。因为方法区回收的性价比通常较低。
方法区允许(不是必须)回收的对象一般是废弃的常量和不再使用的类:
- 废弃的常量:类似堆中的对象,即没引用的常量。比如常量池中有个字符串对象,值为“java”,其回收条件为
- 没用其他任何对象引用“java”常量;
- 虚拟机中没有“java”字面量的引用。
- 不再被使用的类需要同时满足三个条件才会允许(不是必须)被回收(即类卸载):
- 该类的所有实例都已经被回收(堆中不存在该类及其子类的实例);
- 加载该类的类加载器已经被回收;
- 该类对应的Class对象也没被引用(即不能通过反射访问该类的method跟filed)。
Hotspot虚拟机提供了-Xnoclassgc参数进行类卸载控制,还有一系列参数查看类可查看类加载和类卸载信息。
在动态代理大量使用的场景(大量使用反射、CGLib等),或者需频繁自定义类加载器等场景中,通常需要JVM具备类卸载能力,以保证生成的代理类对象不会对方法区造成过大压力。
3. 实现细节
简单来说就是,在安全点进行GC roots枚举,然后开始可达性分析(比如三色标记),标记完后由垃圾收集器采用特定的回收算法进行内存回收。
3.1 GC的起点:节点枚举
由于用户进程运行过程中,根节点集合的对象引用关系会不断变化,为了保证一致性,在根节点枚举过程中需要停顿所有用户线程,不可避免地面临“Stop The World”(但由GC Roots查找引用链的过程已经可以与用户线程并发了)
OopMap:帮助高效的根节点枚举
实际上,停止用户线程进行根节点枚举的时候,不需要一个不漏地检查所有执行的上下文和全局引用。HotSpot使用了OopMap()来直接得到引用信息,在类加载完成后,HotSpot会计算对象内某个偏移量上时什么类型的数据,也会在即时编译的过程中记录栈帧和寄存器里的引用位置。
3.2 何时开始GC:安全点与安全区域
OopMap可以帮助JVM快速完成根节点枚举。但是程序不同线程是按一条条指令执行的,许多指令都可能导致对象引用关系的变化,如果为每条指令都生产OopMap会消耗大量的内存空间,因此可以在“特定位置”生产OopMap。前面有提到,根节点枚举会有STW来避免对象引用关系的变化,现在的问题就是:
在何时让这些线程停止?(即在什么时候生成OopMap/开始GC)
在HotSpot中,这个“特殊位置”就是安全点(Safe Point),用户程序执行时,强制要求执行到安全点后才能暂停下来,开始GC。
再明确一下,安全点就是GC的起点,程序需要GC时,所有线程需要跑到对应的安全点,安全点处有对应的OopMap,可以帮助虚拟机进高效地搜索GC root集合,然后开始GC流程。
如何选取安全点
安全点选的太少,会让收集器等待时间太久;安全点选的太频繁,会增大运行时的内存负荷(每个安全点都会有对应的OopMap)。
安全点的选取标准:**能否让程序长时间执行。**即指令序列的复用,比如方法调用、循环跳转、异常跳转等就会产生安全点。
为什么安全点需要让程序一直跑/为什么安全点要考虑指令序列的复用?
可以理解为,在安全点的程序指令运行时间较长(复用较多),需存活的对象相对重复,不能让过多的死对象占用内存,因此适合作为GC的起点。
如何让程序进入安全点
即如何让GC发生时,让所有线程都跑到最近的安全点。一般包括两种方案:
- 抢先式中断:GC发生时,中断所有线程,如果有线程没到安全点,就恢复执行(现在几乎没有虚拟机用抢先式中断);
- 主动式中断:需要中断线程时,垃圾收集器设置一个标志位,各个线程不断主动轮询该标志位,一旦标志位位true,线程在自己最近的安全点刮起。
- 轮询的标志位与安全点重合,且需要包括创建对象等需要在堆上分配内存的地方,避免没有足够内存分配新对象(是否有必要GC)。
3.3 程序不执行时的安全点:安全区域
如果程序当前没分配执行时间(Sleep或Block状态),就走不到安全点,然后挂起,等待开始GC。因此可以把这些没分配执行时间的位置作为安全区域(Safe Region)。与安全点类似,在安全区域中,引用关系不会发生变化。
- 当用户线程执行到安全区域时,会标识自己进入安全区域,当虚拟机需要GC时就不用管这些标识进入安全区域的线程。
- 当线程要离开安全区域时,需检查根节点枚举是否完成根节点枚举(或其他STW阶段),如果完成则继续执行,如果未完成则一直等待知道收到完成的信号。
Reference
《深入理解java虚拟机:JVM高级特性与最佳时间(第3版)》 周志明
ps:虚引用的例子是也是网上找的,但原文找不到了