【JVM深度解析】GC与分代回收机制

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 你真的了解Java的分代收集吗?什么是垃圾呢?垃圾会马上被回收吗?JVM中哪些区域会被回收呢?Java/C++垃圾收集有什么不同?不懂?一文带你搞懂gc基础!

 本文思维导图:

image.gif编辑

GC概述

gc全拼Garbage Collection,顾名思义垃圾回收的意思,它的主要作用就是回收程序中不再使用的内存。

那我们首先需要知道是否可以主动通知jvm进行垃圾回收?在Java中不能实时调用垃圾回收器对某个对象或者所有对象进行垃圾回收,但是可以通过System.gc()方法来通知垃圾回收器运行,当然,jvm也并不保证垃圾回收器马上就会运行。由于System.gc()方法的执行会停止所有的响应,去检查内存是否有可回收的对象,对程序的正常运行和性能造成了威胁,所以该方法不能频繁使用而且使强烈不推荐使用。正所谓用人不疑疑人不用,那么了解GC回收机制至关重要。下面会提出几个问题作为本文的开篇。

Java 与 C++等语言最大的技术区别?自动化的垃圾回收机制(GC)

为什么要了解 GC 和内存分配策略?1、面试需要 2、GC 对应用的性能是有影响的; 3、写代码有好处

JVM中哪些地方会被回收?栈:栈中的生命周期是跟随线程,所以一般不需要关注

堆:堆中的对象是垃圾回收的重点

方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是我们关注的重点

问题来了虽然Java不需要开发人员手动管理内存回收,但是你有没有想过,这些内存使如何被回收的?什么是需要被回收的?回收的算法它们各有什么优势?下面我将重点讲解这几个问题

分代回收理论

垃圾回收主要发生在堆区,我们先留下垃圾回收的整体框架,在对每一个点进行剖析。

当前商业虚拟机的垃圾回收器,大多遵循“分代收集”的理论来进行设计,这个理论大体上是这么描述的: 1、 绝大部分的对象都是朝生夕死。 2、 熬过多次垃圾回收的对象就越难回收。 根据以上两个理论,朝生夕死的对象放一个区域,难回收的对象放另外一个区域,这个就构成了新生代(eden,from,to)和老年代(tenured)。

image.gif编辑

GC种类

市面上发生垃圾回收的叫法很多,我大体整理了一下:

1、 新生代回收(MinorGC / YoungGC):指只是进行新生代的回收。

2、 老年代回收(MajorGC / OldGC):指只是进行老年代的回收。目前只有 CMS 垃圾回收器会有这个单独的回收老年代的行为。 (MajorGC 定义是比较混乱,有说指是老年代,有的说是做整个堆的收集,这个需要你根据别人的场景来定,没有固定的说法)

3、 整堆回收(FullGC):收集整个 Java 堆和方法区(注意包含方法区)

什么是垃圾

在堆里面存放着几乎所有的对象实例,垃圾回收器在对对进行回收前,要做的事情就是确定这些对象中哪些还是“存活”着,哪些已经“死去”(死去 代表着不可能再被任何途径使用得对象了) 什么是垃圾?

C 语言申请内存:mallocfree C++: newdelete C/C++ 手动回收内存 Java:new Java 是自动内存回收,编程上简单,系统不容易出错。 手动释放内存,容易出两种类型的问题: 1、忘记回收 2、多次回收 没有任何引用指向的一个对象或者多个对象(循环引用)

引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1,当引用失效时,计数器减 1.

Python 在用,但主流虚拟机没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理,这样做影响效率。

/**
 * VM Args: -XX: +PrintGC
 * 判断对象存活
 */
public class Isalive {
    public object instance =null;
    //占据内存,便于判断分析GC
    private byte[] bigSize = new byte [10*1024*1024];
    public static void main(String[] args) {
        Isalive objectA = new Isalive();
        Isalive objectB = new Isalive();
        //相互引用
        objectA.instance = objectB;
        objectB.instance = objectA;
        //切断可达
        objectA =null;
        objectB =null;
        //强制垃圾回收
        System.gc();
    }
}

image.gif

image.gif编辑

在代码中看到,只保留相互引用的对象还是被回收掉了,说明 JVM 中采用的不是引用计数法。

根可达性分析算法

来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GCRoots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为 引用链(ReferenceChain),当一个对象到 GCRoots 没有任何引用链相连时,则证明此对象是不可用的。

作为 GCRoots 的对象包括下面几种(重点是前面 4 种):

一、虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。

二、方法区中类静态属性引用的对象;java 类的引用类型静态变量。

三、方法区中常量引用的对象;比如:字符串常量池里的引用。

四、本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。

五、JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)。(非重点)

六、所有被同步锁(synchronized 关键)持有的对象。(非重点)  JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等(非重点)

七、JVM 实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代的对象,这个后续会细讲,先大致了解概念)(非重点)

以上的回收都是对象,类的回收条件: 注意 Class 要被回收,条件比较苛刻,必须同时满足以下的条件(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):

1、该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。

2、加载该类的 ClassLoader 已经被回收。

3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

4、参数控制:废弃的常量和静态变量的回收其实就和 Class 回收的条件差不多。

Finalize 方法

即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是 没有找到与 GCRoots 的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了 finalize),我们可以在 finalize 中去拯救。

代码演示:

/**
 * @author Mac
 * @date 2020/8/15 - 11:36
 */
public class FinalizeGC {
    public static FinalizeGC instance = null;
    public void isAlive() {
        System.out.println("I am still alivel");
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGC.instance = this;
    }
    public static void main(String[] args) throws Throwable {
        instance = new FinalizeGC();
        // 对象进行第1次GC
        instance = null;
        System.gc();
        Thread.sleep(1000); // Finolizer 方法优先级很低。需要等待
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead! ");
        }
        // 对象进行第2 XGC
        instance = null;
        System.gc();
        Thread.sleep(1000);
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead! ");
        }
    }
}

image.gif

运行结果:

finalize method executed

I am still alivel

I am dead!  

可以看到,对象可以被拯救一次(finalize 执行第一次,但是不会执行第二次) 代码改一下,再来一次。

代码演示:

/**
 * @author YXH
 * @date 2020/8/15 - 11:36
 */
public class FinalizeGC {
    public static FinalizeGC instance = null;
    public void isAlive() {
        System.out.println("I am still alivel");
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGC.instance = this;
    }
    public static void main(String[] args) throws Throwable {
        instance = new FinalizeGC();
        // 对象进行第1次GC
        instance = null;
        System.gc();
        // Thread.sleep(1000); // Finolizer 方法优先级很低。需要等待
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead! ");
        }
        // 对象进行第2 XGC
        instance = null;
        System.gc();
        // Thread.sleep(1000);
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead! ");
        }
    }
}

image.gif

运行结果:

finalize method executed

I am dead!

I am dead!

这次对象没有被拯救,这个就是 finalize 方法执行缓慢,还没有完成拯救,垃圾回收器就已经回收掉了。 所以建议大家尽量不要使用 finalize,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,建议大家忘了 finalize 方法!因为在 finalize 方法能做的工作,java 中有更好的,比如 try-finally 或者其他方式可以做得更好

四种引用

强应用

一般的 Objectobj=newObject() ,就属于强引用。在任何情况下,只有有强引用关联(与根可达)还在,垃圾回收器就永远不会回收掉被引用的对象。

软引用

一些有用但是并非必需,用软引用关联的对象,系统将要发生内存溢出(OuyOfMemory)之前,这些对象就会被回收(如果这次回收后还是没有足够的 空间,才会抛出内存溢出)。参见代码: VM 参数 -Xms10m -Xmx10m-XX:+PrintGC.

例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费 内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都 要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。

弱引用

一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC 发生时,不管内存够不够,都会被回收。

虚引用

幽灵引用,最弱(随时会被回收掉) 垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作。

代码演示:

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.List;
/**
 * @author Mac
 * @date 2020/8/15 - 11:36
 */
public class GC {
    public static void main(String[] args) {
        User u = new User(1, "Mac"); //new是强引用
        SoftReference<User> userSoft = new SoftReference(u);
        u = null;   // 干掉强引用,确保这个实例只有userSoft的软引用
        System.out.println(userSoft.get());
        System.gc();    // 进行一次GC垃圾回收
        System.out.println("After gc");
        System.out.println(userSoft.get());
        // 往堆中填充数据,导致OOM
        List<byte[]> list = new LinkedList();
        try {
            for (int i = 0; i < 100; i++) {
                System.out.println("**********+userSoft.get()");
                list.add(new byte[1024 * 1024 * 1]); //1M的对象
            }
        } catch (Throwable e) {
            // 抛出了00M异常时打印软引用对象
            System.out.println("Except*********" + userSoft.get());
        }
        WeakReference<User> userWeak = new WeakReference(u);
        u = null;//干掉强引用,确保这个实例只有userWeak的弱引用
        System.out.println(userWeak.get());
        System.gc();//进行一次GC垃圾回收
        System.out.println("After gc");
        System.out.println(userWeak.get());
    }
}

image.gif

垃圾回收算法

复制算法(Copying

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使 用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可, 实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。

但要注意:内存移动是必须实打实的移动(复制),所以对应的引用(直接指针)需要调整。 复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。

image.gif编辑

Appel 式回收

一种更加优化的复制回收分代策略:具体做法是分配一块较大的 Eden 区和两块较小的 Survivor 空间(你可以叫做 From 或者 To,也可以叫做 Survivor1 和 Survivor2) 专门研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较 小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上, 最后清理掉 Eden 和刚才用过的 Survivor 空间。 HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存会被 “浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要 依赖其他内存(这里指老年代)进行分配担保(HandlePromotion)

image.gif编辑

标记-清除算法(Mark-Sweep

算法分为“标记”和“清除”两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。 回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。 它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连 续内存而不得不提前触发另一次垃圾回收动作。 回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。

image.gif编辑

标记-整理算法(Mark-Compact

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端 边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低。 我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用 对象的地方都需要更新(直接指针需要调整)。 所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。

image.gif编辑


相关文章
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
66 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
21天前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
1月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
46 3
|
1月前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
46 3
|
1月前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
1月前
|
Java
JVM进阶调优系列(5)CMS回收器通俗演义一文讲透FullGC
本文介绍了JVM中CMS垃圾回收器对Full GC的优化,包括Stop the world的影响、Full GC触发条件、GC过程的四个阶段(初始标记、并发标记、重新标记、并发清理)及并发清理期间的Concurrent mode failure处理,并简述了GC roots的概念及其在GC中的作用。
|
1月前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
12天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
10天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
10 1

推荐镜像

更多
下一篇
无影云桌面