深入理解JVM虚拟机读书笔记——垃圾回收算法

简介: 注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复 001 获取。

1. 如何判断对象已死?

JVM 中判断对象是否已经死亡的算法主要有 2 种:引用计数法、可达性分析法。


1.1 引用计数法

如果一个对象被其他变量所引用,则让该对象的引用计数+1,如果该对象被引用2次则其引用计数为2,依次类推。

某个变量不再引用该对象,则让该对象的引用计数-1,当该对象的引用计数变为0时,则表示该对象没用被其他变量所引用,这时候该对象就可以被作为垃圾进行回收。

引用计数法弊端:循环引用时,两个对象的引用计数都为1,导致两个对象都无法被释放回收。最终就会造成内存泄漏!


image.png

image.png

1.2 可达性分析算法

可达性分析算法就是JVM中判断对象是否是垃圾的算法:该算法首先要确定GC Root(根对象,就是肯定不会被当成垃圾回收的对象)。


在垃圾回收之前,JVM会先对堆中的所有对象进行扫描,判断每一个对象是否能被GC Root直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收:


image.png

JVM中的垃圾回收器通过可达性分析来探索所有存活的对象。

扫描堆中的对象,看能否沿着GC Root为起点的引用链找到该对象,如果找不到,则表示可以回收,否则就可以回收。

**在Java技术体系里面,固定可作为GC Roots的对象包括以下几种 **:

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

在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

所有被同步锁(synchronized关键字)持有的对象。

Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

1.3 Java中的五种引用


image.png

image.png

强引用

上图实心线表示强引用:比如,new 一个对象M,将对象M通过=(赋值运算符),赋值给某个变量m,则变量m就强引用了对象M。


强引用的特点:只要沿着GC Root的引用链能够找到该对象,就不会被垃圾回收;只有当GC Root都不引用该对象时,才会回收强引用对象。


如上图B、C对象都不引用A1对象时,A1对象才会被回收。

软引用


image.png

上图中宽虚线所表示的就是软引用:

软引用的特点:当GC Root指向软引用对象时,若内存不足,则会回收软引用所引用的对象

  • 如上图如果B对象不再引用A2对象且内存不足时,软引用所引用的A2对象就会被回收。

软引用的使用

public class Demo1 {
  public static void main(String[] args) {
    final int _4M = 4*1024*1024;
        // 软引用对象内部包装new byte[_4M]对象
        SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
    // 这时List 跟 SoftReference之间是强引用,SoftReference 跟 byte[] 之间是软引用
    List 跟 <SoftReference<byte[]>> list = new ArrayList<>();
  }
}

如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理

如果想要清理软引用,需要使用引用队列

image.png

public class Demo04 {
    final static int _4M = 4 * 1024 * 1024;
    public static void main(String[] args) {
        // List和SoftReference是强引用,而SoftReference和byte数组则是软引用
        List<SoftReference<byte[]>> list = new ArrayList<>();
        // 引用队列,用于移除引用为空的软引用对象
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        for (int i = 0; i < 5; i++) {
            // 关联引用队列,当软引用所关联的 byte[]被回收时,软引用自己会假如到queue中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4M], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        // 遍历,从引用队列中获取无用的软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while (poll != null) {
            // 引用队列不为空,则从集合中移除该元素
            list.remove(poll);
            // 移动到引用队列中的下一个元素
            poll = queue.poll();
        }
        System.out.println("==========================");
        for (SoftReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }
    }
}

**大概思路为:**查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合)。

弱引用

image.png

只有当弱引用引用该对象,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象。

  • 如上图如果B对象不再引用A3对象,则A3对象会被回收。

弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference。

虚引用

image.png

当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中:

image.png

然后调用Cleaner的clean方法(Unsafe.freeMemory())来释放直接内存:

image.png

虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存。

如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存。

终结器引用


image.png

image.png

所有的类都继承自Object类,Object类有一个finalize()方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize()方法。调用以后,该对象就可以被垃圾回收了。

image.png

如上图,B对象不再引用A4对象。这是终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize()方法。调用以后,该对象就可以被垃圾回收了。

引用队列

软引用和弱引用可以配合引用队列(也可以不配合):

在弱引用和虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象。

虚引用和终结器引用必须配合引用队列:

虚引用和终结器引用在使用时会关联一个引用队列。

1.4 回收方法区

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。举个常量池中字面量回收的例子,假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。


判定一个常量是否“废弃”需要同时满足下面三个条件:


该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。

加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。

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

2. 垃圾回收算法

在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。


四种GC概念的介绍:


■ 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。


■ 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。


■ 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。


■ 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。


下面逐个介绍下4种回收算法:


2.1 标记-清除


image.png

image.png

定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识,清除相应的内容,给堆内存腾出相应的空间。


这里的腾出内存空间并不是将内存空间的字节清 0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存。

缺点:(容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc)。


第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过


程的执行效率都随对象数量增长而降低;


第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。


标记-清除算法的执行过程(书中配图):



image.png

image.png

2.2 标记-整理


image.png

标记-整理:会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。

标记-整理算法的执行过程(书中配图):

image.png

2.3 标记-复制

image.png

当需要回收对象时,先将GC Root直接引用的的对象(不需要回收)放入TO中:

image.png

image.png

然后清除FROM中的需要回收的对象:

image.png

最后 交换 FROMTO 的位置:(FROM换成TO,TO换成FROM)

image.png

复制算法:将内存分为等大小的两个区域,FROMTO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间

标记-复制算法的执行过程(书中配图):

image.png

2.4 分代回收

把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域,顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。


长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快),如下图所示:


image.png

image.png

回收流程

新创建的对象都被放在了新生代的伊甸园中:

image.png

image.png

当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

Minor GC 会将伊甸园和幸存区FROM仍需要存活的对象复制到 幸存区 TO中, 并让其寿命加1,再交换FROM和TO


image.png

伊甸园中不需要存活的对象清除:

image.png

交换FROM和TO

image.png

同理,继续向伊甸园新增对象,如果满了,则进行第二次Minor GC:

流程相同,仍需要存活的对象寿命+1:(下图中FROM中寿命为1的对象是新从伊甸园复制过来的,而不是原来幸存区FROM中的寿命为1的对象,这里只是静态图片不好展示,只能用文字描述了)

image.png

再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1!


如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中:


image.png

image.png

如果新生代老年代中的内存都满了,就会先触发Minor Gc,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收:


image.png

分代回收小结:


新创建的对象首先会被分配在伊甸园区域。

新生代空间不足时,触发Minor GC,伊甸园和 FROM幸存区需要存活的对象会被COPY到TO幸存区中,存活的对象寿命+1,并且交换FROM和TO。

Minor GC会引发 Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。

当对象寿命超过阈值15时,会晋升至老年代。

如果新生代、老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收。

后续会陆续更新,这本书的笔记记的差不多了,排版和格式需要花时间整理,文章都会同步到公众号上,也欢迎大家通过公众号加入我的交流qun互相讨论jvm这块的知识内容!


相关文章
|
3月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
1月前
|
Java
jvm复习,深入理解java虚拟机一:运行时数据区域
这篇文章深入探讨了Java虚拟机的运行时数据区域,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、元空间和运行时常量池,并讨论了它们的作用、特点以及与垃圾回收的关系。
62 19
jvm复习,深入理解java虚拟机一:运行时数据区域
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
60 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
22天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
存储 算法 Java
深入理解Java虚拟机(JVM)及其优化策略
【10月更文挑战第10天】深入理解Java虚拟机(JVM)及其优化策略
41 1
|
1月前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
1月前
|
存储 算法 Java
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
49 2
|
29天前
|
算法 JavaScript 前端开发
垃圾回收算法的原理
【10月更文挑战第13天】垃圾回收算法的原理
22 0
|
2月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
112 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
3月前
|
存储 算法 Java
JVM自动内存管理之垃圾收集算法
文章概述了JVM内存管理和垃圾收集的基本概念,提供一个关于JVM内存管理和垃圾收集的基础理解框架。
JVM自动内存管理之垃圾收集算法