转载

小朋友,你是否有很多的 GC?

GC 即 Garbage Collection,中文 意思“垃圾回收”,在有GC之前,我们手动去管理内存,如果你忘记标记某一处已经不再使用的内存,那么这块内存将永远不会被系统回收,也就是常说的 “内存泄露”。

以下所有的 GC 介绍,全部基于主流 JVM 虚拟机 Hotspot。

GC 是如何判断一个对象是存亡?

GC判断一个对象存活或死亡就是判断这个对象还存不存在它的引用,常见的两种方式如下

  • 引用计数法

每个对象从创建开始,都会维护一个引用计数器,每当引用一次,那么计数器增加1,引用失效一次,那么计数器减去1,这样实现优点是高效、简单,但是缺点也很明显:无法解决循环依赖,比如下面的代码,虽然 A引用B,B引用A,但是就没有其他地方引用了,因此它们是无效引用,造成内存泄露。Java 自然不会选择这种方式作为判断方式。

A=B
    B=A
复制代码
  • 引用链(可达性分析法)代表语言:Java、C# 相比引用计数法,可达性分析法就复杂的多,也安全的多了,分为三步

1.可达性分析

将一系列的 GC Roots 对象作为起点,开始向下搜索。可作为 GC Root 的起点有

  • Java 虚拟机栈(栈桢本地变量表中)引用的对象

  • 本地方法栈中JNI(也就是常说的 Native 方法)

  • 方法中的常量、类静态属性引用的对象

注意: 向下搜索的路径就是引用链

为了方便理解,我画了下面的图片

小朋友,你是否有很多的 GC?

特别注意:可达性分析仅仅是判断对象是否可达,但还不足以判断对象是否存活或者死亡。可达性分析中判断为不可达的对象,只是被判刑 ≠ 死亡。

不可达对象会存放在 「即将回收」集合中,要判断一个对象是否真正的死亡,还需要经过下面的两个步骤。

2.第一次标记 & 筛选

可达性分析中标记为不可达的对象,会经历第一次筛选。

筛选标准:判断对象是否需要执行 finalize() 方法,若有必要执行,则筛选进行下一阶段分析,若没必要执行,那么该对象判定为死亡,不筛选,等待系统回收。

当对象无 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过一次的情况下,那么被标记为没必要执行,等待回收。

3.第二次标记 & 筛选

当对象经过了第一次筛选后没有被回收,将进行第二次筛选。

该对象会被放在一个 F-Queue 的队列中,并由虚拟机自动创建一个名为 Finalizer 的低优先级的线程去执行队列中所有对象的 finalize 方法,这里需要注意的是,finalize 方法只会被执行一次,并且 JVM 并不承诺 finalize 会执行完毕,原因是为了防止 finalize 执行时间过长或者停止执行,导致的内存溢出。

筛选标准: 在执行 finalize 方法的过程中,如果该对象依旧没有和 GC Root 关联起来,那么该对象被判断为死亡,留在即将回收集合,等待回收。

Full GC 和 Partial GC

简单理解就是,全局GC(Full GC)和局部GC(Partial GC),分别看一下:

Partial GC(局部GC)

Young GC :只收集 Young Gen(年轻代)的 GC, Young GC 还有一个叫法 叫 Minor GC。

old GC : 只收集 Old Gen(年老代) 的GC 只有垃圾回收器 CMS 的 concament colletton 有这个模式。

mixed GC : 收集整个Young GC的GC和部分的old Gen的GC,只有垃圾回收器 G1 有这个模式。

Full GC

Full GC是对整个堆来说的,执行Full GC 的时候会回收所有代,包括永久代、年老代、年轻代等等所有的 GC。Full GC 的触发条件有以下几种

System.gc()方法的调用

此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。

老年代空间不足

旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

方法区空间不足

JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

通过Minor GC后进入老年代的平均大小大于老年代的可用内存

如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC

为了方便理解上述枯燥又无味的理论,我写了几行代码,实验了一下

/**
 * GCRoots 测试:虚拟机栈(栈帧中的局部变量)中引用的对象作为GCRoots
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 * <p>
 * 扩展:虚拟机栈中存放了编译器可知的八种基本数据类型,对象引用,returnAddress类型(指向了一条字节码指令的地址)
 *
 * @author wangjie
 */
public class TestGCRoots01 {
    private int _10MB = 10 * 1024 * 1024;
    private byte[] memory = new byte[8 * _10MB];

    public static void main(String[] args) {
        method01();
        System.out.println("返回main方法");
        System.gc();
        System.out.println("第二次GC完成");
    }

    public static void method01() {
        TestGCRoots01 t = new TestGCRoots01();
        System.gc();
        System.out.println("第一次GC完成");
    }
}
复制代码

当运行上述代码控制台会输出如下内容

小朋友,你是否有很多的 GC?

首先该类声明了一个 80M 的数组

private byte[] memory = new byte[8 * _10MB];
复制代码

然后调用了 method01,method01创建了一个TestGCRoots01的实例,该实例存放在 PSYoungGen 也就是年轻代

method01();
TestGCRoots01 t = new TestGCRoots01();
复制代码

这时调用了 System.gc(),该实例从PSYoungGen到了ParOldGen也就是年老代

System.gc();
System.out.println("第一次GC完成");
复制代码

但是并没有被回收,method01执行完毕后返回到了 main 方法,这时又执行了一次 System.gc(),该实例被回收 。

System.out.println("返回main方法");
System.gc();
System.out.println("第二次GC完成");
复制代码

至此,GC 的基础知识你应该了解了,但是这篇文章仅仅简单分析了一下 GC 和 JVM 的关系,并不涉及到引用链,如果对你理解 GC 有帮助,点赞转发是对我最大的支持。

另外,我为你找到了以下资料,并翻译成了中文供你查阅

  • 关于 GC 的 Oracle给出的文档
原文  https://juejin.im/post/5e7a051df265da57082a1130
正文到此结束
Loading...