Java GC的那些事(1)

作者: 美团Java | 来源:发表于2016-08-27 10:53 被阅读16408次

    简书 占小狼
    转载请注明原创出处,谢谢!

    前言

    与C语言不同,Java内存(堆内存)的分配与回收由JVM垃圾收集器自动完成,这个特性深受大家欢迎,能够帮助程序员更好的编写代码,本文以HotSpot虚拟机为例,说一说Java GC的那些事。

    Java堆内存

    JVM内存的那些事 一文中,我们已经知道Java堆是被所有线程共享的一块内存区域,所有对象实例和数组都在堆上进行内存分配。为了进行高效的垃圾回收,虚拟机把堆内存划分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3个区域。

    新生代

    新生代由 Eden 与 Survivor Space(S0,S1)构成,大小通过-Xmn参数指定,Eden 与 Survivor Space 的内存大小比例默认为8:1,可以通过-XX:SurvivorRatio 参数指定,比如新生代为10M 时,Eden分配8M,S0和S1各分配1M。

    Eden:希腊语,意思为伊甸园,在圣经中,伊甸园含有乐园的意思,根据《旧约·创世纪》记载,上帝耶和华照自己的形像造了第一个男人亚当,再用亚当的一个肋骨创造了一个女人夏娃,并安置他们住在了伊甸园。

    大多数情况下,对象在Eden中分配,当Eden没有足够空间时,会触发一次Minor GC,虚拟机提供了-XX:+PrintGCDetails参数,告诉虚拟机在发生垃圾回收时打印内存回收日志。

    Survivor:意思为幸存者,是新生代和老年代的缓冲区域。
    当新生代发生GC(Minor GC)时,会将存活的对象移动到S0内存区域,并清空Eden区域,当再次发生Minor GC时,将Eden和S0中存活的对象移动到S1内存区域。

    存活对象会反复在S0和S1之间移动,当对象从Eden移动到Survivor或者在Survivor之间移动时,对象的GC年龄自动累加,当GC年龄超过默认阈值15时,会将该对象移动到老年代,可以通过参数-XX:MaxTenuringThreshold 对GC年龄的阈值进行设置。

    老年代

    老年代的空间大小即-Xmx 与-Xmn 两个参数之差,用于存放经过几次Minor GC之后依旧存活的对象。当老年代的空间不足时,会触发Major GC/Full GC,速度一般比Minor GC慢10倍以上。

    永久代

    在JDK8之前的HotSpot实现中,类的元数据如方法数据、方法信息(字节码,栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32位默认永久代的大小为64M,64位默认为85M,可以通过参数-XX:MaxPermSize进行设置,一旦类的元数据超过了永久代大小,就会抛出OOM异常。

    虚拟机团队在JDK8的HotSpot中,把永久代从Java堆中移除了,并把类的元数据直接保存在本地内存区域(堆外内存),称之为元空间。

    这样做有什么好处?
    有经验的同学会发现,对永久代的调优过程非常困难,永久代的大小很难确定,其中涉及到太多因素,如类的总数、常量池大小和方法数量等,而且永久代的数据可能会随着每一次Full GC而发生移动。

    而在JDK8中,类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间,可以避免永久代的内存溢出问题,不过需要监控内存的消耗情况,一旦发生内存泄漏,会占用大量的本地内存。

    ps:JDK7之前的HotSpot,字符串常量池的字符串被存储在永久代中,因此可能导致一系列的性能问题和内存溢出错误。在JDK8中,字符串常量池中只保存字符串的引用。

    如何判断对象是否存活

    GC动作发生之前,需要确定堆内存中哪些对象是存活的,一般有两种方法:引用计数法和可达性分析法。

    1、引用计数法
    在对象上添加一个引用计数器,每当有一个对象引用它时,计数器加1,当使用完该对象时,计数器减1,计数器值为0的对象表示不可能再被使用。

    引用计数法实现简单,判定高效,但不能解决对象之间相互引用的问题。

    public class GCtest {
        private Object instance = null;
        private static final int _10M = 10 * 1 << 20;
        // 一个对象占10M,方便在GC日志中看出是否被回收
        private byte[] bigSize = new byte[_10M];
    
        public static void main(String[] args) {
            GCtest objA = new GCtest();
            GCtest objB = new GCtest();
    
            objA.instance = objB;
            objB.instance = objA;
    
            objA = null;
            objB = null;
    
            System.gc();
        }
    }
    

    通过添加-XX:+PrintGC参数,运行结果:

    [GC (System.gc()) [PSYoungGen: 26982K->1194K(75776K)] 26982K->1202K(249344K), 0.0010103 secs]
    

    从GC日志中可以看出objA和objB虽然相互引用,但是它们所占的内存还是被垃圾收集器回收了。

    2、可达性分析法
    通过一系列称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索路径称为 “引用链”,以下对象可作为GC Roots:

    • 本地变量表中引用的对象
    • 方法区中静态变量引用的对象
    • 方法区中常量引用的对象
    • Native方法引用的对象

    当一个对象到 GC Roots 没有任何引用链时,意味着该对象可以被回收。


    在可达性分析法中,判定一个对象objA是否可回收,至少要经历两次标记过程:
    1、如果对象objA到 GC Roots没有引用链,则进行第一次标记。
    2、如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法。finalize()方法是对象逃脱死亡的最后机会,GC会对队列中的对象进行第二次标记,如果objA在finalize()方法中与引用链上的任何一个对象建立联系,那么在第二次标记时,objA会被移出“即将回收”集合。

    看看具体实现

    public class FinalizerTest {
        public static FinalizerTest object;
        public void isAlive() {
            System.out.println("I'm alive");
        }
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("method finalize is running");
            object = this;
        }
    
        public static void main(String[] args) throws Exception {
            object = new FinalizerTest();
    
            // 第一次执行,finalize方法会自救
            object = null;
            System.gc();
    
            Thread.sleep(500);
            if (object != null) {
                object.isAlive();
            } else {
                System.out.println("I'm dead");
            }
    
            // 第二次执行,finalize方法已经执行过
            object = null;
            System.gc();
    
            Thread.sleep(500);
            if (object != null) {
                object.isAlive();
            } else {
                System.out.println("I'm dead");
            }
        }
    }
    

    执行结果:

    method finalize is running
    I'm alive
    I'm dead
    

    从执行结果可以看出:
    第一次发生GC时,finalize方法的确执行了,并且在被回收之前成功逃脱;
    第二次发生GC时,由于finalize方法只会被JVM调用一次,object被回收。

    当然了,在实际项目中应该尽量避免使用finalize方法。

    END。
    我是占小狼。
    在魔都艰苦奋斗,白天是上班族,晚上是知识服务工作者。
    如果读完觉得有收获的话,记得关注和点赞哦。
    非要打赏的话,我也是不会拒绝的。

    相关文章

      网友评论

      • amourling:永久代 其实就是所谓的方法区,方法区又叫非堆,你说 “虚拟机团队在JDK8的HotSpot中,把永久代从Java堆中移除了”,不知道是我记错了还是这里笔误了
        amourling:@会吐烟de鱼 我就是这个意思
        4f7035c1ea0e:@amourling 应该是方法区吧
      • 阿飞的博客:“当Eden没有足够空间时,会触发一次Minor GC”,这句话有问题吧?Eden区域没有足够空间,应该尝试从Old区分配,Old区不够才触发Minor GC,如果MinorGC后还不够,就触发FullGC
        美团Java:@阿飞滴简书 minor gc是ygc,和old区没有关系,有关系的也是major gc
        阿飞的博客: @占小狼 如果Old空间足够就不会MinorGC呀,怎么没问题!
        美团Java:@阿飞滴简书 我说的没有问题
      • 右丶羽:觉得 引用计数法 这里有点歧义,
        案例意思应该是 虽然变量A,B最后置为空, 但是new出来的对象并没有删除彼此间的引用关系,所以 两个对象的引用计数器均不为0 ,
        按道理,假如使用引用计数法是无法被GC的, 但实际上被GC了 这也是正确的做法 ,因为没有变量指向这两个对象了 , 即便这两个对象之间存在互相引用也是没有意义的
        至于为什么会被gc,是因为jvm不是使用引用计数法来实现gc吧 ?
        不知道理解得有没有错误
        Joseph_Mok:@右丶羽 对 即使引用计数不为0,也被回收了。说明不是用的引用计数算法。
        美团Java:@右丶羽 对的
      • 015a9776a1e7:”在可达性分析法中,判定一个对象objA是否可回收,至少要经历两次标记过程”
        如果对象没有重写finalize()方法呢
        美团Java:@不怕死的蚊子 那就只有一次了
      • c30c53893dff:“当再次发生Minor GC时,将Eden和S0中存活的对象移动到S1内存区域”
        难道不是 将s0中存活下来的移到s1中,将eden存活下来的移到s0中 吗
        八九七十二块五:@占小狼 相当于空间使用率90%?
        美团Java:@恶魔的佑翼 不是
      • mathsfan:写的不错:+1:
      • 5f3572e6068d:记得在书上看到过,如果子类重写了父类的finalize方法,父类的finalize方法是不会执行,这样就会导致如果父类有资源释放的操作就会导致资源无法释放。
        白马王朗:@仙橙子 应该是这样的,所以一般在重写finalnize方法的时候,会显示调用父类的方法,super();再添加自己的逻辑?不知道对不对?~
        5f3572e6068d:简书中搜索 Effective Java 2.0_中英文对照_Item 7
        美团Java:@仙橙子 什么书?
      • icyage:知识整理的很好
        美团Java:@icyage 嗯,知识太多太乱,需要一个整理的过程,在这个过程中会发现平时没注意到的问题

      本文标题:Java GC的那些事(1)

      本文链接:https://www.haomeiwen.com/subject/iqeasttx.html