面经手册 · 第27篇《JVM 判断对象已死,实践验证GC回收》

简介: 先动手验证垃圾回收JVM 垃圾回收知识框架1. 判断对象已死2. 垃圾回收算法3. 垃圾回收器

目录


  • 一、前言
  • 二、面试题
  • 三、先动手验证垃圾回收
  • 四、JVM 垃圾回收知识框架
  • 1. 判断对象已死
  • 2. 垃圾回收算法
  • 3. 垃圾回收器
  • 五、总结
  • 六、系列推荐


一、前言

提升自身价值有多重要?

经过了风风雨雨,看过了男男女女。时间经过的岁月就没有永恒不变的!

在这趟车上有人下、有人上,外在别人给你点评的标签、留下的烙印,都只是这趟车上的故事。只有个人成长了、积累了、沉淀了,才有机会当自己的司机。

可能某个年龄段的你还看不懂,但如果某天你不那么忙了,要思考思考自己的路、自己的脚步。看看这些是不是你想要的,如果都是你想要的,为什么你看起来不开心?

好!加油,走向你想成为的自己!

二、面试题

谢飞机,小记!,中午吃饱了开始发呆,怎么就学不来这些知识呢,它也不进脑子!

「谢飞机」:喂,面试官大哥,我想问个问题。

「面试官」:什么?

「谢飞机」:就是这知识它不进脑子呀!

「面试官」:这....

「谢飞机」:就是看了忘,忘了看的!

「面试官」:是不是没有实践?只是看了就觉得会了,收藏了就表示懂了?哪哪都不深入!?

「谢飞机」:好像是!那有什么办法?

「面试官」:也没有太好的办法,学习本身就是一件枯燥的事情。减少碎片化的时间浪费,多用在系统化的学习上会更好一些。哪怕你写写博客记录下,验证下也是好的。

三、先动手验证垃圾回收

说是垃圾回收,我不引用了它就回收了?什么时候回收的?咋回收的?

没有看到实际的例子,往往就很难让理科生接受这类知识。我自己也一样,最好是让我看得见。代码是对数学逻辑的具体实现,没有实现过程只看答案是没有意义的。

「测试代码」

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
     * 这个成员属性的唯一意义就是占点内存, 以便能在GC日志中看清楚是否有回收过
     */
    private byte[] bigSize = new byte[2 * _1MB];
    public static void main(String[] args) {
        testGC();
    }
    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 假设在这行发生GC, objA和objB是否能被回收?
        System.gc();
    }
}

例子来自于《深入理解Java虚拟机》中引用计数算法章节。

例子要说明的结果是,相互引用下却已经置为null的两个对象,是否会被GC回收。如果只是按照引用计数器算法来看,那么这两个对象的计数标识不会为0,也就不能被回收。但到底有没有被回收呢?

这里我们先采用 jvm 工具指令,jstat来监控。因为监控的过程需要我手敲代码,比较耗时,所以我们在调用testGC()前,睡眠会 Thread.sleep(55000);。启动代码后执行如下指令。

E:\itstack\git\github.com\interview>jps -l
10656
88464
38372 org.itstack.interview.ReferenceCountingGC
26552 sun.tools.jps.Jps
110056 org.jetbrains.jps.cmdline.Launcher
E:\itstack\git\github.com\interview>jstat -gc 38372 2000
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0   1288.0 65536.0    0.0     175104.0     8.0     4864.0 3982.6 512.0  440.5       1    0.003   1      0.000    0.003
10752.0 10752.0  0.0    0.0   65536.0   437.3    175104.0    1125.5   4864.0 3982.6 512.0  440.5       1    0.003   1      0.012    0.015
10752.0 10752.0  0.0    0.0   65536.0   437.3    175104.0    1125.5   4864.0 3982.6 512.0  440.5       1    0.003   1      0.012    0.015
  • S0C、S1C,第一个和第二个幸存区大小
  • S0U、S1U,第一个和第二个幸存区使用大小
  • EC、EU,伊甸园的大小和使用
  • OC、OU,老年代的大小和使用
  • MC、MU,方法区的大小和使用
  • CCSC、CCSU,压缩类空间大小和使用
  • YGC、YGCT,年轻代垃圾回收次数和耗时
  • FGC、FGCT,老年代垃圾回收次数和耗时
  • GCT,垃圾回收总耗时

「注意」:观察后面三行,S1U = 1288.0GCT = 0.003,说明已经在执行垃圾回收。

接下来,我们再换种方式测试。在启动的程序中,加入GC打印参数,观察GC变化结果。

-XX:+PrintGCDetails  打印每次gc的回收情况 程序运行结束后打印堆空间内存信息(包含内存溢出的情况)
-XX:+PrintHeapAtGC  打印每次gc前后的内存情况
-XX:+PrintGCTimeStamps 打印每次gc的间隔的时间戳 full gc为每次对新生代老年代以及整个空间做统一的回收 系统中应该尽量避免
-XX:+TraceClassLoading  打印类加载情况
-XX:+PrintClassHistogram 打印每个类的实例的内存占用情况
-Xloggc:/Users/xiaofuge/Desktop/logs/log.log  配合上面的使用将上面的日志打印到指定文件
-XX:HeapDumpOnOutOfMemoryError 发生内存溢出将堆信息转存起来 以便分析

这回就可以把睡眠去掉了,并添加参数 -XX:+PrintGCDetails,如下:

image.gif25.jpg

「测试结果」

[GC (System.gc()) [PSYoungGen: 9346K->936K(76288K)] 9346K->944K(251392K), 0.0008518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 936K->0K(76288K)] [ParOldGen: 8K->764K(175104K)] 944K->764K(251392K), [Metaspace: 3405K->3405K(1056768K)], 0.0040034 secs] [Times: user=0.08 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b500000,0x000000076b6eb9e0,0x000000076f500000)
  from space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
  to   space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
 ParOldGen       total 175104K, used 764K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000)
  object space 175104K, 0% used [0x00000006c1e00000,0x00000006c1ebf100,0x00000006cc900000)
 Metaspace       used 3449K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K
  • 从运行结果可以看出内存回收日志,Full GC 进行了回收。
  • 也可以看出JVM并不是依赖引用计数器的方式,判断对象是否存活。否则他们就不会被回收啦

「有了这个例子,我们再接着看看JVM垃圾回收的知识框架!」

四、JVM 垃圾回收知识框架

垃圾收集(Garbage Collection,简称GC),最早于1960年诞生于麻省理工学院的Lisp是第一门开始使用内存动态分配和垃圾收集技术的语言。

垃圾收集器主要做的三件事:哪些内存需要回收什么时候回收、怎么回收。

而从垃圾收集器的诞生到现在有半个世纪的发展,现在的内存动态分配和内存回收技术已经非常成熟,一切看起来都进入了“自动化”。但在某些时候还是需要我们去监测在高并发的场景下,是否有内存溢出、泄漏、GC时间过程等问题。所以在了解和知晓垃圾收集的相关知识对于高级程序员的成长就非常重要。

垃圾收集器的核心知识项主要包括:判断对象是否存活、垃圾收集算法、各类垃圾收集器以及垃圾回收过程。如下图;

26.jpg图 27-1 垃圾收集器知识框架

原图下载链接:http://book.bugstack.cn/#s/6jJp2icA

1. 判断对象已死

1.1 引用计数器

  1. 为每一个对象添加一个引用计数器,统计指向该对象的引用次数。
  2. 当一个对象有相应的引用更新操作时,则对目标对象的引用计数器进行增减。
  3. 一旦当某个对象的引用计数器为0时,则表示此对象已经死亡,可以被垃圾回收。

从实现来看,引用计数器法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但是它的实现方案简单,判断效率高,是一个不错的算法。

也有一些比较出名的引用案例,比如:微软COM(Component Object Model) 技术、使用ActionScript 3的FlashPlayer、 Python语言等。

「但是」,在主流的Java虚拟机中并没有选用引用技术算法来管理内存,主要是因为这个简单的计数方式在处理一些相互依赖、循环引用等就会非常复杂。可能会存在不再使用但又不能回收的内存,造成内存泄漏

1.2 可达性分析法

Java、C#等主流语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

它的算法思路是通过定义一系列称为 GC Roots 根对象作为起始节点集,从这些节点出发,穷举该集合引用到的全部对象填充到该集合中(live set)。这个过程教过标记,只标记那些存活的对象 好,那么现在未被标记的对象就是可以被回收的对象了。

GC Roots 包括;

  1. 全局性引用,对方法区的静态对象、常量对象的引用
  2. 执行上下文,对 Java方法栈帧中的局部对象引用、对 JNI handles 对象引用
  3. 已启动且未停止的 Java 线程

「两大问题」

  1. 误报:已死亡对象被标记为存活,垃圾收集不到。多占用一会内存,影响较小。
  2. 漏报:引用的对象(正在使用的)没有被标记为存活,被垃圾回收了。那么直接导致的就是JVM奔溃。(STW可以确保可达性分析法的准确性,避免漏报)

2. 垃圾回收算法

2.1 标记-清除算法(mark-sweep)

image.gif27.jpg标记-清除算法(mark-sweep)

  • 标记无引用的死亡对象所占据的空闲内存,并记录到空闲列表中(free list)。
  • 当需要创建新对象时,内存管理模块会从 free list 中寻找空闲内存,分配给新建的对象。
  • 这种清理方式其实非常简单高效,但是也有一个问题内存碎片化太严重了。
  • 「Java 虚拟机的堆中对象」,必须是连续分布的,所以极端的情况下可能即使总剩余内存充足,但寻找连续内存分配效率低,或者严重到无法分配内存。重启汤姆猫!
  • 在CMS中有此类算法的使用,GC暂停时间短,但存在算法缺陷。

2.2 标记-复制算法(mark-copy)

28.jpgimage.gif标记-复制算法(mark-copy)

  • 从图上看这回做完垃圾清理后连续的内存空间就大了。
  • 这种方式是把内存区域分成两份,分别用两个指针 from 和 to 维护,并且只使用 from 指针指向的内存区域分配内存。
  • 当发生垃圾回收时,则把存活对象复制到 to 指针指向的内存区域,并交换 from 与 to 指针。
  • 它的好处很明显,就是解决内存碎片化问题。但也带来了其他问题,堆空间浪费了一半。

2.3 标记-压缩算法(mark-compact)

image.gif29.jpg标记-压缩算法(mark-compact)

  • 1974年,Edward Lueders 提出了标记-压缩算法,标记的过程和标记清除算法一样,但在后续对象清理步骤中,先把存活对象都向内存空间一端移动,然后在清理掉其他内存空间。
  • 这种算法能够解决内存碎片化问题,但压缩算法的性能开销也不小。

3. 垃圾回收器

3.1 新生代

  1. Serial
  1. 算法:标记-复制算法
  2. 说明:简单高效的单核机器,Client模式下默认新生代收集器;
  1. Parallel ParNew
  1. 算法:标记-复制算法
  2. 说明:GC线程并行版本,在单CPU场景效果不突出。常用于Client模式下的JVM
  1. Parallel Scavenge
  1. 算法:标记-复制算法
  2. 说明:目标在于达到可控吞吐量(吞吐量=用户代码运行时间/(用户代码运行时间+垃圾回收时间));

3.2 老年代

  1. Serial Old
  1. 算法:标记-压缩算法
  2. 说明:性能一般,单线程版本。1.5之前与Parallel Scavenge配合使用;作为CMS的后备预案。
  1. Parallel Old
  1. 算法:标记-压缩算法
  2. 说明:GC多线程并行,为了替代Serial Old与Parallel Scavenge配合使用。
  1. CMS
  1. 算法:标记-清除算法
  2. 说明:对CPU资源敏感、停顿时间长。标记-清除算法,会产生内存碎片,可以通过参数开启碎片的合并整理。基本已被G1取代

3.3 G1

  1. 算法:标记-压缩算法
  2. 说明:适用于多核大内存机器、GC多线程并行执行,低停顿、高回收效率。

五、总结

  • JVM 的关于自动内存管理的知识众多,包括本文还没提到的 HotSpot 实现算法细节的相关知识,包括:安全节点、安全区域、卡表、写屏障等。每一项内容都值得深入学习。
  • 如果不仅仅是为了面试背题,最好的方式是实践验证学习。否则这类知识就像3分以下的过电影一样,很难记住它的内容。
  • 整个的内容也是小傅哥学习整理的一个过程,后续还会不断的继续深挖和分享。感兴趣的小伙伴可以一起讨论学习。


目录
相关文章
|
14天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
29 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
18天前
|
存储 监控 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日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
43 3
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
1月前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
1月前
|
存储 算法 Java
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
49 2
|
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