算法 自动采集列表(垃圾回收相关算法标记阶段(一)(1)_根集合就是 )
优采云 发布时间: 2021-11-23 04:06算法 自动采集列表(垃圾回收相关算法标记阶段(一)(1)_根集合就是
)
垃圾采集相关算法
打标阶段概述
Reference Counting Algorithm Reference Counting 循环引用,例如变量指针P指向A,其中A指BB指CC引用A,内部形成一个引用链。当一个P指针不再指向A时,本质上A将不再被使用,但是由于循环的依赖关系导致A的计数器永远不会减为0,从而导致永远无法回收的内存泄漏
证明 HotSpot 不使用引用计数算法
public class ProveJavaNotUseReferenceCounting {
private byte[] data = new byte[1024 * 1024 * 5];
Object ref = null;
public static void main(String[] args) {
ProveJavaNotUseReferenceCounting obj1 = new ProveJavaNotUseReferenceCounting();
ProveJavaNotUseReferenceCounting obj2 = new ProveJavaNotUseReferenceCounting();
obj1.ref = obj2;
obj2.ref = obj1;
obj1 = null;
obj2 = null;
System.gc();
}
}
[GC (System.gc()) [PSYoungGen: 13578K->744K(38400K)] 13578K->752K(125952K), 0.1038107 秒] [时间:用户=0.@<>00 sys= @0.00,真实=0.11 秒]
[Full GC (System.gc()) [PSYoungGen: 744K->0K(38400K)] [ParOldGen: 8K->642K(87552K)] 752K->642K(125952K), [Metaspace: 3277K->8K(38400K)] ], 0.0081173 秒] [时间:用户=0.00 sys=0.00, real=0.01 秒]
堆
PSYoungGen 总共 38400K,使用了 333K [0x00000000d5900000, 0x00000000d8380000, 0x00000)
伊甸园空间 33280K,已使用 1% [0x00000000d5900000,0x00000000d59534a8,0x00000000d7980000)
从空间 5120K, 0% 使用 [0x00000000d7980000,0x00000000d7980000,0x00000000d7e80000)
到空间 5120K, 0% 使用 [0x00000000d7e80000,0x00000000d7e80000,0x00000000d8380000)
ParOldGen 总计 87552K,已用 642K [0x0000000080a00000, 0x0000000085f80000, 0x00000000d5900000)
对象空间 87552K,0% 使用 [0x0000000080a00000,0x0000000080aa0bb0,0x0000000085f80000)
Metaspace使用3283K,容量4496K,提交4864K,保留1056768K
已用类空间 359K,容量 388K,已提交 512K,保留 1048576K
汇总可达性分析算法
概念
想法
GCRoots 根集合是一组必须处于活动状态的引用
基本思想
GC Roots 可以是 JNI 引用的对象(通常称为本地方法)在本地方法栈中。类的方法区中的对象经常被属性引用。方法区中的常量引用的对象。java虚拟机内部同步的同步锁持有的所有对象参考JMXBean JVMTI中反映java虚拟机内部情况的注册回调,本地代码缓存
总结提示
由于Root使用栈来存储变量和指针,如果一个指针将对象保存在堆中,但没有保存在堆内存中,那么他就是一个Root
注意对象的终结机制
永远不要主动调用对象的 finalize() 方法!应该交给GC调用
一个糟糕的 finalize 方法会严重影响 GC 性能。在功能上,finalize方法类似于C++的析构函数,但是Java使用了基于垃圾采集器的自动内存管理机制,所以finalize方法与C++的析构函数有着本质的区别。虚拟机对象的三种状态
由于finalize方法的存在,虚拟机对象可能有三种状态
如果所有的根节点都无法访问一个对象,则表示该对象不再被使用。一般来说,这个时候需要回收对象。但实际上,并不是“必须死”。一个不可触及的物体可能会在某种条件下复活自己。如果真是这样,那他的恢复是不合理的。
定稿过程
判断一个对象obj是否可以回收,至少要经过两次标记过程
如果对象 obj 和 GC Roots 之间没有引用链,则执行第一个标记进行过滤,以确定该对象是否需要执行 finalize 方法。如果对象obj没有重写finalize()方法,或者finalize方法已经被虚拟机调用,则认为虚拟机没有必要执行,obj被判断为不可达。如果obj重写了finalize方法并没有被执行,则将obj插入到F-Queue队列中,由虚拟机自动创建的低优先级Finalizer线程触发其finalize方法执行
复活finazlie中的代码演示
public class ReviveTest {
// 类成员 是GC Roots的集合一员
public static ReviveTest data;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize 方法调用");
// 这句话使得对象在finalize中被复活
data = this;
}
public static void main(String[] args) throws InterruptedException {
data = new ReviveTest();
data = null;
System.out.println("第一次执行GC============");
System.gc();
TimeUnit.SECONDS.sleep(2);
if (data == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is alive");
}
System.out.println("第二次执行GC============");
data = null;
System.gc();
TimeUnit.SECONDS.sleep(2);
if (data == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is alive");
}
}
}
第一次执行GC ============
完成方法调用
obj 还活着
第二次执行GC ============
obj死了
在第一次清理中,我们会执行finalize方法,然后对象进行自助操作,但是因为finalize()方法只会被调用一次,所以对象会被第二次垃圾清理。
MAT 和 JProfiler GC Roots 的可追溯性
MAT 是 Memory Anlayzer 的缩写,是 Ecplise 开发的一款免费且功能强大的 java 堆内存分析器,用于发现内存泄漏和查看内存消耗
使用 JVisualVM 捕获堆转储文件。JVisualVM捕获的是一个临时文件,关闭JVisualVM后会自动删除,需要保存为文件。
应用程序 右键单击相应的应用程序,选择 Heap Dump,然后单击 Monitor 子选项卡中的 Heap Dump 按钮。本地应用程序的堆转储作为应用程序选项卡的子选项卡打开。同时Heap Dump对应左侧Application列中对应的Save as node with timestamp
使用MAT打开Dump文件查看GC Roots
使用 JProfiler 的 GC Roots 溯源
选择 JProfiler 打开转储堆文件。在类或最大对象中,选择一个类并进入参考视图。选择传入引用以查看对象的整个链接
如何确定导致OOM的原因
模拟OOM代码
// -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
public class HeapOOM {
// 创建1M的文件
byte [] buffer = new byte[1 * 1024 * 1024];
public static void main(String[] args) {
ArrayList list = new ArrayList();
int count = 0;
try {
while (true) {
list.add(new HeapOOM());
count++;
}
} catch (Exception e) {
e.getStackTrace();
System.out.println("count:" + count);
}
}
}
上面代码不断向链表中添加1m的数据,JVM参数-Xms8m -Xmx8m限制最多8M内存
-XX:+HeapDumpOnOutOfMemeoryError 发生OOM时生成hprof文件
使用 JProfiler 定位内存溢出代码
用JProfiler打开hprof,点击Biggest Object定位大对象
通过线程转储文件定位出现OOM的那一行
清除节点标记清除算法
在完成标记阶段确定哪些对象是垃圾后,GC 会进行垃圾回收,释放无用对象占用的内存空间,以便有足够的内存空间为新对象分配内存。主流的三种垃圾回收算法如下
标记去除算法
当堆中的可用内存耗尽时,整个程序(Stop The World)就会停止,GC会进行标记和清理。
什么是清理?标记清除算法的优缺点 复制算法的优缺点 复制算法的优缺点
高效的
低的
高的
中间
空间开销
低的
高(空间的1/2)
低(原尺寸)
移动物体
不
是的
是的
不同场景选择不同算法
老年代的分代采集算法Tenured Gen以HotSpot中的cms采集器为例,cms基于Mark-Sweep,对象的回收效率非常高。对于碎片问题,采用cms基于Mark-Compact算法的Serial Old采集器作为补偿措施。当内存回收不良时(当分片导致Concurrent Mode Failure时),会使用serial old执行FullGC来实现对旧内存进行排序和生成的思路。虚拟机应用广泛,几乎所有垃圾采集器都区分新生代和老年代增量采集算法
使用上述现有算法,在进行垃圾回收、所有用户线程挂起、所有正常工作挂起、垃圾回收完成时会导致STW状态。如果垃圾回收时间过长,会严重影响用户体验和系统稳定性。为了解决这个问题,堆实时垃圾采集的研究导致了增量采集(Incremental Collecting)算法的诞生
如果一次处理所有垃圾,需要很长的STW,那么垃圾采集线程和应用线程可以交替执行。每次垃圾采集线程只采集一小块内存,然后切换到应用线程。依次重复直到垃圾采集完成
增量采集算法的基础仍然是传统的标记-清除和复制算法。只是增量采集算法正确处理了线程之间的冲突,让垃圾采集线程分阶段完成标记、清除和复制操作。
缺点是使用了这种方法。因为应用程序代码在垃圾回收过程中间歇性执行,减少了STW时间,但是由于线程切换和上下文切换的消耗,垃圾回收的整体成本增加,导致系统吞吐量下降
分区算法
一般来说,相同的堆空间越大,一次GC所需的时间越长,与GC生成相关的暂停时间也越长。为了更好地控制GC产生的暂停时间,将一块大内存区域划分为多个Small blocks,根据目标暂停的事件,每次合理回收几个cell,而不是真正的堆空间,从而减少由 GC 引起的暂停
分代算法会根据对象生命周期的长短分为两部分。分区算法将整个堆空间划分为连续的不同cell,每个cell使用独立的recovery。这种算法的优点是可以控制一次恢复多少。社区间