JVM--垃圾回收机制算法分析

 2023-02-18
原文作者:ゞ浪人与酒丶 原文地址:https://juejin.cn/post/6911212203567759367

JVM--垃圾回收机制算法分析

202301011652356561.png

1. 什么是垃圾回收机制

不定时去堆内存中清理不可达对象,不可达达对象并不会马上就会回收,垃圾收集器在一个Java程序中达执行是自动的,不能强制执行,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存快,程序员唯一能做的就是通过调用 System.gc 方法来 “建议”执行垃圾回收器,但其是否可以执行,什么审核执行是不可知但,这也是垃圾收集器但最主要的缺点,当然相对于他给程序员带来巨大的方便,这个缺点是不不掩瑜的。

    /**
     * @author yxl
     * @version 1.0
     * @date 2020/12/27 下午4:48
     */
    public class Test {
        public static void main(String[] args) {
            Test test = new Test();
            test = null;
            System.gc(); // 手动回收垃圾
        }
    
        @Override
        protected void finalize() throws Throwable {
            // gc回收垃圾之前调用
            System.out.println("垃圾回收机制...");
        }
    }

202301011652361492.png

finalize方法作用

Java技术使用 finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的工作,这个方法是由垃圾收集器在确定这个对象没有引用时对这个对象调用的。它是在Object类中定义的,因此是有的类都继承类它,子类覆盖 finalize() 方法以整理系统资源活着执行其他清理工作,finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。


2. 新生代与老年代

    	Java 中 的堆是JVM 所管理的最大一块堆内存,主要用于存放各种类的实例对象。

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同数据区域

一块是非堆区,一块是堆区。堆区分为两大块,一个是Old区,一个是Young区。Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区。Eden:S0:S1=8:1:1 S0和S1一样大,也可以叫From和To。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

202301011652367153.png

根据垃圾回收机制的不同,Java堆有可能拥有不同的结构,最为常见的就是将整个Java堆分为:

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

对象创建所在区域

大家都知道对象是存在堆中的 ,上篇文章中有提到, **一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。**

比如有对象A,B,C等创建在Eden区,但是Eden区的内存空间肯定有限,比如有100M,假如已经使用了100M或者达到一个设定的临界值,这时候就需要对Eden内存空间进行清理,即垃圾收集(Garbage Collect),这样的GC我们称之为Minor GC,Minor GC指得是Young区的GC。经过GC之后,有些对象就会被清理掉,有些对象可能还存活着,对于存活着的对象需要将其复制到Survivor区,然后再清空Eden区中的这些对象。

由图解可以看出,Survivor区分为两块S0和S1,也可以叫做From和To。在同一个时间点上,S0和S1只能有一个区有数据,另外一个是空的。

接着上面的GC来说,比如一开始只有Eden区和From中有对象,To中是空的。此时进行一次GC操作,From区中对象的年龄就会+1,我们知道Eden区中所有存活的对象会被复制到To区,From区中还能存活的对象会有两个去处。若对象年龄达到之前设置好的年龄阈值,此时对象会被移动到Old区,没有达到阈值的对象会被复制到To区。此时Eden区和From区已经被清空(被GC的对象肯定没了,没有被GC的对象都有了各自的去处)。这时候From和To交换角色,之前的From变成了To,之前的To变成了From。也就是说无论如何都要保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,知道To区被填满,然后会将所有对象复制到老年代中。

从上面的分析可以看出,一般Old区都是年龄比较大的对象,或者相对超过了某个阈值的对象。在Old区也会有GC的操作,Old区的GC我们称作为Major GC, 每次GC之后还能存活的对象年龄也会+1,如果年龄超过了某个阈值,就会被回收。

## 对象的一辈子理解

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor 区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

为什么需要Survivor区?只有Eden不行吗?

如果没有Survivor,Eden区每进行一次MinorGC,存活的对象就会被送到老年代。这样一来,老年代很快被填满,触发MajorGC(因为MajorGC一般伴随着MinorGC,也可以看做触发了FullGC)。老年代的内存空间远大于新生代,进行一次FullGC消耗的时间比MinorGC长得多。执行时间长有什么坏处?频发的FullGC消耗的时间很长,会影响大型程序的执行和响应速度。

所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少FullGC的发生,Survivor的预筛选保证,只有经历16次MinorGC还能在新生代中存活的对象,才会被送到老年代。

为什么需要两个Survivor区?

最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:刚刚新建的对象在Eden中,一旦Eden满了,触发一次MinorGC,Eden中的存活对象就会被移动到Survivor 区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行MinorGC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。永远有一个Survivorspace是空的,另一个非空的Survivorspace无碎片。


Garbage Collect(垃圾回收)

如何确定一个对象是垃圾?

引用计数法 对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。弊端:如果AB相互持有引用,导致永远不能被回收。

可达性分析 通过GC Root的对象,开始向下寻找,看某个对象是否可达

垃圾收集算法

已经能够确定一个对象为垃圾之后,接下来要考虑的就是回收,怎么回收呢?得要有对应的算法,下面介绍常见的垃圾回收算法。

标记-清除(Mark-Sweep)

找出内存中需要回收的对象,并且把它们标记出来 比较耗时,需要把堆中所有的对象都扫描一遍,才能确定那个是垃圾, 然后清除掉标记的对象,从而释放内存空间

复制(Copying)

将内存划分为两块相等的区域,每次只使用其中一块 当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。

标记-整理(Mark-Compact)

标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

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满不会引发GCFull GC触发机制:

当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代,当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载其中Minor GC如下图所示 虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值)来设置。

202301011652372114.png

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

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


分代算法

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

新生代

  • 在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;可以参看我之前写的java垃圾回收算法之-coping复制

老年代

  • 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。参看java垃圾回收算法之-标记_清除压缩 新创建的对象被分配在新生代,如果对象经过几次回收后仍然存活,那么就把这个对象划分到老年代。 老年代区存放Young区Survivor满后触发minor GC后仍然存活的对象,当Eden区满后会将存活的对象放入Survivor区域,如果Survivor区存不下这些对象,GC收集器就会将这些对象直接存放到Old区中,如果Survivor区中的对象足够老,也直接存放到Old区中。如果Old区满了,将会触发Full GC回收整个堆内存。