JVM 如何自动完成垃圾回收

一、简介

        思考一个问题,在 java 里面我们 new 一个对象,等到程序结束后,这个对象就被自动回收了,完成这项工作只需要确定:哪些内存需要回收?什么时候回收?如何回收?接下来我们详细的解释下这三个问题。

二、哪些内存需要回收

        由于程序计数器,虚拟机栈,本地方法栈随线程而生,随线程而死,故这几个区不需要过多考虑回收的问题。而 Java 堆和方法区则不一样一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,所以我们考虑回收的是这部分内存

2.1 堆内存回收

        如果想要回收 Java 堆内存,首先需要判断对象是否存活,可以采用引用计数算法(无法解决循环引用的问题)和正向可达算法;其次还要判断对象的引用情况(强软弱虚);即使是正向可达算法不可达的对象,也不是非死不可,宣告一个对象死亡,还要经历两次标记过程,在这个过程中对象还可能发生自救,真正死亡的对象才会被回收。

2.2 方法区内存回收

        方法区主要回收两部分内存:废弃的常量无用的类,假如一个字符串 abc 进入了常量池,但是没有任何 String 对象引用常量池的 abc 常量,也没有其他地方引用这个字面量,若此时发生内存回收,而且必要的话,abc 常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

        判定一个类是否是 ”无用的类“ 需要满足 3 个条件:

        第一个条件:该类所有的实例都被回收,也就是 Java 堆中不存在该类的任何实例;

        第二个条件:加载该类的 ClassLoader 已经被回收;

        第三个条件:该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

三、什么时候进行回收

3.1 GC 分类

        在堆内存中,内存分为新生代和老年代,所以 GC 分为新生代 GC 和老年代 GC 。新生代 GC 又名 Minor GC,是指发生在新生代动的垃圾收集动作,因为 java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也很快。

        老年代 GC 又名 Major GC / Full GC,是指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GCMajor GC 的速度一般比 Minor GC10 倍以上。

        当新生代或老年代的内存满了的时候就会发生 GC 的操作,那么内存什么时候会满呢?参照下面所说的对象分配内存问题。

3.2 对象内存分配问题

        1、对象优先在 Eden 分配Eden 区没有足够的空间进行分配时,虚拟机将进行一次 Minor GC
        2、大对象直接进入老年代大对象是指需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。
        3、长期存活的对象将进入老年代虚拟机给每个对象定义了一个对象年龄 (Age) 计数器,如果对象在 Eden 出生且经过一次 Minor GC 存活且被 Survivor 容纳,此时对象年龄设为 1,对象熬过一次 Minor GC,年龄就增加 1 岁,当年龄到达一定的程度(默认15,可调),就会被晋升到老年代。
        4、动态对象年龄判断如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到 15 岁。

四、如何进行回收

        JVM 采用的是分代的垃圾收集算法,即新生代采用复制的垃圾收集算法,老年代采用标记清除标记压缩的垃圾收集算法不同的垃圾收集器采用不同的垃圾收集算法。

4.1 复制算法 Copying

        以前(现在不这么干了):将内存按容量划分为大小相等的两块,每次只使用其中的一块,当一块用完,就把还存活的对象复制到另外一块上,再把上一块内存清理干净,大多数的 JVM 都采用这种算法回收新生代。

        现状(现在这么干):将内存分为一块较大的 Eden 区和两块较小的 Survivor 区,每次使用 Eden 和其中一块SurvivorHotSpot 默认的 Eden Survivor 大小比例是 8:1。即每次只浪费 10%,当 Survivor 空间不够用时,需要依赖其他内存(老年代)进行分配担保。

        空间分配担保:Survivor 无法容纳的对象直接进入老年代

4.2 标记清除算法 Mark-Sweep

        首先标记处所有需要回收的对象,然后统一回收所有标记的对象,缺点是效率低,并且标记清除后会产生大量不连续的内存碎片,碎片太多会导致为大对象分配内存时,因无法找到足够的连续内存而不得不提前触发一次垃圾收动作。

4.3 标记压缩算法 Mark-compact

        把存活的对象往一端压缩(替换位置),然后清理掉可回收的对象。

五、判断对象是否存活        

        判断一个对象是否存活有两种基本的算法,一种是引用计数算法,一种是正向可达算法,下面分别来介绍下。

5.1 引用计数算法

        给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一,当引用失效时,计数器减一,当该对象的引用计数器为 0 时,我们认为该对象就不能被使用了。

        特点:效率高,但是无法解决循环引用的问题(即 A 引用 BB 引用 A)。

5.2 正向可达算法

        以一个 GC Roots 为根节点,从这个节点往下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链时,就证明此对象是不可用的。可作为 GC Roots 的对象有:

        1、虚拟机栈中引用的对象

        2、本地方法栈中引用的对象

        3、方法区中类的静态属性引用的对象

        4、方法区中常量引用的对象

六、垃圾收集器

6.1 分代模型

        把内存分为新生代和老年代,新生代采取复制算法,老年代采用标记压缩或标记算法

6.2 分区模型

把内存里面分成一个一个的小格

G1

ZGCjdk11 以上)

6.1 新生代垃圾收集器

        Serial 收集器:单线程收集器,即只会使用一个 CPU 或一条收集线程去完成垃圾收集,且它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束;但他仍然是 jvm 运行在 Client 模式下默认的新生代的收集器。

        Parallel Scavenge 收集器:并行的多线程收集器,它关注的点是达到一个可控制的吞吐量;而像 CMS 收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间。(jdk1.8 默认的新生代)

        ParNew 收集器:其实就是 Serial 收集器的多线程版本,即使用多条线程进行垃圾收集,它是在 Server 模式下首选的新生代垃圾收集器。

6.2 老年代垃圾收集器

        Serial Old 收集器:Serial 收集器的老年代版本,也是一个单线程的收集器,使用“标记-整理”算法;也是给 Client 模式下的虚拟机使用的

        Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本,在注重吞吐量和 CPU 资源敏感的场合可以优先考虑。(jdk1.8 默认的老年代)

        CMS 收集器:一种获取最短回收停顿时间为目标的收集器(停顿时间最短),响应速度快,基于“标记-清除”算法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值