深入理解JVM—JVM垃圾回收机制[通俗易懂]

深入理解JVM—JVM垃圾回收机制[通俗易懂]垃圾回收是指不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收,垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用System.gc方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。先谈一下新生代与老年代根据垃圾回…

大家好,又见面了,我是你们的朋友全栈君。

垃圾回收是指不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的不能强制执行,程序员唯一能做的就是通过调用System.gc 方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。

先谈一下新生代与老年代

根据垃圾回收机制的不同,Java堆有可能拥有不同的结构,最为常见的就是将整个Java堆分为
新生代和老年代。其中新生带存放新生的对象或者年龄不大的对象,老年代则存放老年对象。

深入理解JVM---JVM垃圾回收机制[通俗易懂]

  • 新生代分为eden区、s0区、s1区,s0和s1也被称为from和to区域,他们是两块大小相等并且可以互相角色的空间。
  • 绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次
  • 新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。

一、为什么需要垃圾回收

如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。

✍上代码

public class GCTest { 
   
    public static void main(String[] args) { 
   
        Object object = new Object();
        object = null;
        //System.gc(); 没用不会执行finalize()
        GCTest gcTest = new GCTest();
        gcTest=null;
        System.gc();// 手动回收垃圾
    }

    /** * Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前, * 做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。 * 它是在Object类中定义的,因此所有的类都继承了它。 * 子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。 * finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。 * * @throws Throwable */
    @Override
    protected void finalize() throws Throwable { 
   
        System.out.println("开始垃圾回收");
    }
}

二、哪些内存需要回收

哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象。那么如何找到这些对象?

引用计数法

这个算法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能再被使用的。这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。

深入理解JVM---JVM垃圾回收机制[通俗易懂]
✍上代码


public class ReferenceCountingGC
{ 
   
   private Object instance = null;
   private static final int _1MB = 1024 * 1024;
   
   /** 这个成员属性唯一的作用就是占用一点内存 */
   private byte[] bigSize = new byte[2 * _1MB];
   
   public static void main(String[] args)
   { 
   
       ReferenceCountingGC objectA = new ReferenceCountingGC();
       ReferenceCountingGC objectB = new ReferenceCountingGC();
       objectA.instance = objectB;
       objectB.instance = objectA;
       objectA = null;
       objectB = null;
       
       System.gc();
   }
}
结果:
[GC 4417K->288K(61440K), 0.0013498 secs]
[Full GC 288K->194K(61440K), 0.0094790 secs]

看到,两个对象相互引用着,但是虚拟机还是把这两个对象回收掉了,这也说明虚拟机并不是通过引用计数法来判定对象是否存活的。

根搜索算法

这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

  • 方法区中的类静态属性引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中JNI(Native方法)引用的对象。

下面给出一个GCRoots的例子,如下图,为GCRoots的引用链。
深入理解JVM---JVM垃圾回收机制[通俗易懂]
由图可知,obj8、obj9、obj10都没有到GCRoots对象的引用链,即便obj9和obj10之间有引用链,他们还是会被当成垃圾处理,可以进行回收。

如果不好理解请看下图:
深入理解JVM---JVM垃圾回收机制[通俗易懂]
深入理解JVM---JVM垃圾回收机制[通俗易懂]

.
五、垃圾收集算法

标记清除算法

该算法有两个阶段。

  1. 标记阶段:找到所有可访问的对象,做个标记
  2. 清除阶段:遍历堆,把未被标记的对象回收

应用场景:

该算法一般应用于老年代,因为老年代的对象生命周期比较长。深入理解JVM---JVM垃圾回收机制[通俗易懂]
关于回收后碎片化的理解:
在这里插入图片描述
该算法的优缺点:

优点

  • 是可以解决循环引用的问题
  • 必要时才回收(内存不足时)

缺点

  • 回收时,应用需要挂起,也就是stop the world。
  • 标记和清除的效率不高,尤其是要扫描的对象比较多的时候
  • 会造成内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一些的对象无法做到),
标记压缩算法

标记清除算法和标记压缩算法非常相同,但是标记压缩算法在标记清除算法之上解决内存碎片化.让所有存活对象都向一端移动,然后直接清理掉边界以外的内存

深入理解JVM---JVM垃圾回收机制[通俗易懂]

  • 任意顺序 : 即不考虑原先对象的排列顺序,也不考虑对象之间的引用关系,随意移动对象;
  • 线性顺序 : 考虑对象的引用关系,例如a对象引用了b对象,则尽可能将a和b移动到一块;
  • 滑动顺序 : 按照对象原来在堆中的顺序滑动到堆的一端。

优点:解决内存碎片问题,缺点压缩阶段,由于移动了可用对象,需要去更新引用。

深入理解JVM---JVM垃圾回收机制[通俗易懂]

复制算法

如果JVM使用了复制算法,一开始就会将可用内存分为两块,from域和to域, 每次只是使用from域,to域则空闲着。当from域内存不够了,开始执行GC操作,这个时候,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。

应用场景

复制算法一般是使用在新生代中,jvm将Heap 内存划分为新生代老年代,又将新生代划分为Eden(伊甸园) 与2块Survivor Space(幸存者区) ,然后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行Copying 算法。

不过jvm在应用复制算法时,并不是把内存按照1:1来划分的,这样太浪费内存空间了。一般的jvm都是8:1。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。

深入理解JVM---JVM垃圾回收机制[通俗易懂]

  • 当Eden区满的时候,会触发第一次young gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发young gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。

  • 当后续Eden又发生young gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。

  • 可见部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
    注意: 万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间。

优缺点

优点:在存活对象不多的情况下,性能高,能解决内存碎片和java垃圾回收算法之-标记清除 中导致的引用更新问题。

缺点: 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制的性能会变得很差。

分代算法

这种算法,根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。可以用抓重点的思路来理解这个算法。
新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。

  • 新生代
    在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集.
  • 老年代
    而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。
Minor GC和Full GC区别

新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。

  • Minor GC触发机制:
    • 当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC
  • Full GC触发机制:
    • 当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代,当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。
在这里插入图片描述

JVM的永久代中会发生垃圾回收么?

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
(注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)


总结

还是感觉得有书学着才踏实,看视频看博客总感觉缺少一点信服力,先就以此对垃圾回收机制做个基础的了解.后期如果遇到更好的资料再做个深入的学习.?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/143799.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号