23. java虚拟机总结-和OOM相关的 (六)
垃圾回收算法
可达性分析法(根搜索算法,GC ROOTS)
从 GC Roots 向下追溯、搜索,会产生一个叫作 Reference Chain 的链条。当一个对象不能和任何一个 GC Root 产生关系时,就会被无情的诛杀掉。
如图所示,Obj5、Obj6、Obj7,由于不能和 GC Root 产生关联,发生 GC 时,就会被摧毁。
GC ROOTS.png
GC Roots 有哪些
GC Roots 是一组必须活跃的引用。用通俗的话来说,就是程序接下来通过直接引用或者间接引用,能够访问到的潜在被使用的对象。
GC Roots 包括:
Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用。
所有当前被加载的 Java 类。
Java 类的引用类型静态变量。
运行时常量池里的引用类型常量(String 或 Class 类型)。
JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类。
用于同步的监控对象,比如调用了对象的 wait() 方法。
JNI handles,包括 global handles 和 local handles。
这些 GC Roots 大体可以分为三大类,下面这种说法更加好记一些:
1 活动线程相关的各种引用。(虚拟机栈(栈帧中的本地变量表)中引用的对象。)
2 类的静态变量的引用。(方法区中类静态属性引用的对象。方法区中常量引用的对象。)
3 JNI 引用。(本地方法栈中JNI(即一般说的Native方法)引用的对象。)
gc roots包括.png
有两个注意点:
1. 我们这里说的是活跃的引用,而不是对象,对象是不能作为 GC Roots 的。
2. GC 过程是找出所有活对象,并把其余空间认定为“无用”;而不是找出所有死掉的对象,并回收它们占用的空间。所以,哪怕 JVM 的堆非常的大,基于 tracing 的 GC 方式,回收速度也会非常快。
引用计数法
因为有循环依赖的硬伤,现在主流的 JVM,没有一个是采用引用计数法来实现 GC 的,所以我们大体了解一下就可以。引用计数法是在对象头里维护一个 counter 计数器,被引用一次数量 +1,引用失效记数 -1。计数器为 0 时,就被认为无效。你现在可以忘掉引用计数的方式了
引用类型
面试题:能够找到 Reference Chain 的对象,就一定会存活么?
不一定,看引用类型
强引用 Strong references
即使程序会异常终止,这种对象也不会被回收的。比如下边,这种情况我们需要手动将其引用关系断开才会被回收
Object obj = new Object()
软引用 Soft references
软引用用于维护一些可有可无的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,这种特性非常适合用在缓存技术上。比如网页缓存、图片缓存等。
有一个相关的 JVM 参数。它的意思是:每 MB 堆空闲空间中 SoftReference 的存活时间。这个值的默认时间是1秒(1000)。
-XX:SoftRefLRUPolicyMSPerMB=<N>
这里要特别说明的是,网络上一些流传的优化方法,即把这个值设置成 0,其实是错误的,这样容易引发故障,感兴趣的话你可以自行搜索一下。
这种比较偏门的优化手段,除非在你对其原理相当了解的情况下,才能设置一些比较特殊的值。比如 0 值,无限大等,这种值在 JVM 的设置中,最好不要发生。
弱引用 Weak references
当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用拥有更短的生命周期
它的应用场景和软引用类似,可以在一些对内存更加敏感的系统里采用
虚引用 Phantom References
形同虚设的引用,在现实场景中用的不是很多。虚引用必须和引用队列(ReferenceQueue)联合使用。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
Object object = new Object();
ReferenceQueue queue = new ReferenceQueue();
// 虚引用,必须与一个引用队列关联
PhantomReference pr = new PhantomReference(object, queue);
虚引用主要用来跟踪对象被垃圾回收的活动。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象之前,把这个虚引用加入到与之关联的引用队列中。
程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
下面的方法,就是一个用于监控 GC 发生的例子。
private static void startMonitoring(ReferenceQueue<MyObject> referenceQueue, Reference<MyObject> ref) {
ExecutorService ex = Executors.newSingleThreadExecutor();
ex.execute(() -> {
while (referenceQueue.poll()!=ref) {
//don't hang forever
if(finishFlag){
break;
}
}
System.out.println("-- ref gc'ed --");
});
ex.shutdown();
}
基于虚引用,有一个更加优雅的实现方式,那就是 Java 9 以后新加入的 Cleaner,用来替代 Object 类的 finalizer 方法。
典型 OOM 场景
内存区域有哪些会发生 OOM 呢?我们可以从内存区域划分图上,看一下彩色部分。
oom发生的内存区域.png
除了程序计数器,其他区域都有OOM溢出的可能。但是最常见的还是发生在堆上。
oom发生.pngOOM 到底是什么引起的呢?有几个原因:
内存的容量太小了,需要扩容,或者需要调整堆的空间。
错误的引用方式,发生了内存泄漏。没有及时的切断与 GC Roots 的关系。比如线程池里的线程,在复用的情况下忘记清理 ThreadLocal 的内容。
接口没有进行范围校验,外部传参超出范围。比如数据库查询时的每页条数等。
对堆外内存无限制的使用。这种情况一旦发生更加严重,会造成操作系统内存耗尽。
典型的内存泄漏场景,原因在于对象没有及时的释放自己的引用。比如一个局部变量,被外部的静态集合引用。
oom场景之一.png
你在平常写代码时,一定要注意这种情况,千万不要为了方便把对象到处引用。即使引用了,也要在合适时机进行手动清理。关于这部分的问题根源排查,我们将在实践课程中详细介绍。