一文学会JVM垃圾回收器详解:串行回收,新生代内存管理内存分配

简介: 新生代内存管理包含了内存的分配和回收,这与新生代内存布局密切相关。新生代被划分为3个空间:Eden、From和To空间。这3个空间的作用如下:1)Eden:仅用于应用程序对象分配;GC工作线程不会在该空间进行对象分配。2)From:用于GC工作线程在执行垃圾回收时,在前一轮垃圾回收后活跃对象的存储。在特殊情况下,From空间也可以用于应用程序对象的分配(这是JVM在实现对象分配时的一种优化),但GC工作线程不会在该空间进行对象分配。3)To:用于在GC工作线程执行垃圾回收时,存储本轮垃圾回收过程中活跃的对象。垃圾回收过程将Eden空间和From空间中的活跃对象放入To空间。

新生代内存管理

新生代内存管理包含了内存的分配和回收,这与新生代内存布局密切相关。

新生代被划分为3个空间:Eden、From和To空间。

这3个空间的作用如下:

1)Eden:仅用于应用程序对象分配;GC工作线程不会在该空间进行对象分配。

2)From:用于GC工作线程在执行垃圾回收时,在前一轮垃圾回收后活跃对象的存储。在特殊情况下,From空间也可以用于应用程序对象的分配(这是JVM在实现对象分配时的一种优化),但GC工作线程不会在该空间进行对象分配。

3)To:用于在GC工作线程执行垃圾回收时,存储本轮垃圾回收过程中活跃的对象。垃圾回收过程将Eden空间和From空间中的活跃对象放入To空间。

只有GC工作线程能在该空间进行对象分配,应用程序不能使用该空间进行对象分配。

串行回收使用单线程进行垃圾回收。Java语言支持多线程应用,应用分配对象的空间通常是Eden空间(此处暂不讨论JVM中From空间的优化使用),多个线程同时在一个空间中分配对象,需要设计高效的分配算法来提高应用程序的运行效率。新生代的高速分配算法实际上不仅包含在堆空间中进行对象分配,还包含对新生代堆空间进行垃圾回收后内存的再访问机制(主要指回收后访存的效率)。整体分配算法包含:高速无锁分配、加锁慢速分配、内存不足情况下的垃圾回收后再分配。JVM的内存分配流程图如图3-5所示。

图3-5 JVM内存分配流程示意图

设计3种分配方式的目的如下:

1)优先进行高速无锁分配,这是我们期望的情况,在这种场景中效率最高,具体内容在3.2.1节讨论。

2)当内存不足时,会在进行垃圾回收之后重用内存空间并再次进行分配,这将在3.2.2~3.2.6节讨论。

3)加锁的慢速分配是一个中间状态,主要用于解决:当Mutator直接在堆空间进行内存分配时需要互斥锁(同时也要保证多个Mutator之间竞争的公平性,防止某一个Mutator因为并发锁一直无法成功分配);在整个JVM运行期间可能已经有其他Mutator因内存不足触发了垃圾回收,通常进行垃圾回收之后有大量可以使用的内存,在这种情况下,Mutator可以在加锁的情况下直接完成分配,该状态是设计和实现的一个优化点。

新生代内存分配

堆内存中供应用分配对象的空间只有一个(即Eden),而Mutator是多个同时执行,这意味着存在多个Mutator同时在Eden中分配对象的情况,因为Eden属于临界资源,在使用临界资源时需要互斥锁。使用互斥锁的结果就是多个Mutator需要按照内存分配请求的顺序串行执行,而这样的设计将导致Mutator的运行效率较低,所以JVM需要寻找一种高速的无锁内存分配方法来解决多个Mutator互斥访问Eden的问题。这种高速无锁分配在JVM中称为TLAB(
Thread-Local-Allocation-Buffer),在其他资料中也称为TLS(Thread-Local-Storage)或者TLH(Thread-Local-Heap)。

TLAB的设计思路就是为每个Mutator分配一个专有的本地缓冲区,每个Mutator在对象分配的时候,优先从本地的缓冲区进行分配,只有在第一次从堆空间中初始化TLAB时才需要加锁分配,这样将大大减少多个Mutator之间分配时的互斥问题。多个Mutator使用TLAB的示意图如图3-6所示。

图3-6 多Mutator使用TLAB进行对象分配示意图

线程(Thread1和Thread2)分别从Eden中分配一个TLAB,Thread1和Thread2的内存分配都是从自己的TLAB中分配的。

虽然使用TLAB的分配方式能减少多个Mutator之间的互斥锁,但是也带来了设计上的复杂性。有两个需要特别注意的地方:

1)TLAB的大小。如果TLAB太小,那么缓冲区很快被填满,需要再次从堆空间请求一个新的TLAB。频繁地从堆空间请求TLAB将导致潜在的锁冲突,从而导致性能下降。如果TLAB过大,虽然不会导致频繁的锁冲突,但是可能导致TLAB一直填不满,存在潜在的空间浪费。

2)何时申请新的TLAB。简单的回答是在TLAB使用完了之后就申请一个新的TLAB。但是判断TLAB是否使用完毕并不容易,原因在于TLAB的大小是固定的,而应用中请求的对象大小并不固定,这就意味着TLAB通常无法完美地被使用完毕,在TLAB即将使用完毕的时候,剩余的大小并不固定。也就是说在TLAB即将用完的时候,需要一个机制判断是否需要申请新的TLAB。通过一个简单的示意图演示该问题,如图3-7所示。

图3-7 TLAB无法满足分配请求示意图

图3-7中演示了TLAB剩余的空间不满足Mutator新对象的分配场景,此时该如何处理?这就需要一个机制来判断是否申请新的TLAB。通常做法如下:

当剩余空间比较少时,直接申请一个新的TLAB,丢弃原来TLAB中剩余的空间。

当剩余空间比较多时,如果直接申请一个新的TLAB,放弃原来的TLAB将导致空间浪费。在这种情况下,为了减少空间的浪费,通常不会申请一个新的TLAB,而是直接在堆空间进行对象分配(当然分配时需要对堆空间进行加锁)。这是典型的用时间(加锁分配)换空间(TLAB剩余空间)的做法。

Mutator从堆空间直接分配TLAB并使用TLAB响应应用的分配,当TLAB满了以后,无须进行额外的处理。因为TLAB来自堆空间,在进行垃圾回收的时候会对堆空间进行回收,所以无须进行额外的处理。唯一需要额外处理的是在丢弃TLAB中尚未使用的空间时,需要给剩余空间填充一个垃圾对象(也称为Dummy对象),这样做的目的是保持堆的可解析性(Heap Parsability)。

多线程使用TLAB过程中TLAB满的例子如图3-8所示。假设有两个线程Thread1(简称T1)和Thread2(简称T2),它们都是应用程序线程,在运行时都需要一个TLAB,应用程序线程分配对象都在TLAB中,T1的TLAB在分配对象的时候,因为剩余空间不足以满足对象的大小,所以直接在堆空间Eden中直接分配;此时T1的TLAB仍然指向最初的TLAB;T2的第一个TLAB已经满了(或者说剩余空间比较少,填充Dummy对象之后满了),重新分配一个新的TLAB供新的分配。

图3-8 多线程使用TLAB过程中TLAB满的例子

在JVM的实现中,TLAB的初始大小可以通过参数(TLABSize)调整。另外,JVM也可以通过反馈机制动态调整TLAB的大小(如果允许动态调整TLAB的大小,则需要确保参数ResizeTLAB为true),从而在时间(加锁耗时)和空间(高速无锁分配、空间浪费)之间寻找一个平衡。在判断是否可以丢弃当前TLAB剩余空间的时候,当发现剩余空间小于TLAB的一定比例时,就认为浪费比较少了,可以直接丢弃(参数为TLABRefillWasteFraction,默认值为64,即剩余空间小于等于TLAB的1/64时可以丢弃)。

最后简单解释一下JVM中堆可解性的概念。在JVM运行过程中存在很多需要对堆空间进行遍历的情况,遍历时会从一个起始地址(假设起始地址为heap_start)遍历到终止地址(假设终止地址为heap_end)之间的内存空间。假设在遍历堆空间时进行一些额外的处理,其具体的工作由do_object处理(具体的处理省略)。一个典型的代码如下所示:

HeapWord* cur = heap_start;

while (cur < heap_end) { //遍历整个空间

object o = (object)cur;

do_object(o);

cur = cur + o->size(); //在这里需要空间里面的对象连续

//如果存在空洞,将在此处导致遍历错误

}

在遍历的时候要求堆空间中的对象是连续分配的,如果堆空间中存在空洞(hole),那么上述代码就不能正常工作(空洞会被转化为对象,导致内存访问错误)。所以在处理TLAB剩余空间的时候必须填充一个对象让上述代码能正常运行,这种机制称为堆可解析性(通常填充一个int[]的对象,这个对象是JVM内部产生的,读者可能遇到应用中根本没有分配int[]对象,但是在转存(dump)堆内存时看到很多int[]对象的情况,原因之一就是JVM在处理TLAB时填充了大量死亡的int[]对象)。

本文给大家讲解的内容是JVM垃圾回收器详解:串行回收,新生代内存管理内存分配

  1. 下篇文章给大家讲解的内容是JVM垃圾回收器详解:串行回收,新生代内存管理,垃圾回收的触发机制
  2. 感谢大家的支持!
  3. 本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。
相关文章
|
5月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
491 55
|
11月前
|
算法 JavaScript 前端开发
新生代和老生代内存划分的原理是什么?
【10月更文挑战第29天】新生代和老生代内存划分是JavaScript引擎为了更高效地管理内存、提高垃圾回收效率而采用的一种重要策略,它充分考虑了不同类型对象的生命周期和内存使用特点,通过不同的垃圾回收算法和晋升机制,实现了对内存的有效管理和优化。
|
7月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
11月前
|
JavaScript 前端开发 Java
垃圾回收机制会导致内存泄漏吗?
【10月更文挑战第29天】虽然JavaScript的垃圾回收机制本身是为了有效地管理内存,但开发者在编写代码时需要注意上述这些可能导致内存泄漏的情况,遵循良好的编程习惯,及时释放不再使用的资源,以确保程序能够高效地利用内存资源,避免出现内存泄漏问题。
|
10月前
|
缓存 监控 算法
Python内存管理:掌握对象的生命周期与垃圾回收机制####
本文深入探讨了Python中的内存管理机制,特别是对象的生命周期和垃圾回收过程。通过理解引用计数、标记-清除及分代收集等核心概念,帮助开发者优化程序性能,避免内存泄漏。 ####
227 3
|
11月前
|
算法 Java 开发者
Java内存管理与垃圾回收机制深度剖析####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,特别是其垃圾回收机制的工作原理、算法及实践优化策略。不同于传统的摘要概述,本文将以一个虚拟的“城市环卫系统”为比喻,生动形象地揭示Java内存管理的奥秘,旨在帮助开发者更好地理解并调优Java应用的性能。 ####
|
11月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
179 6
|
11月前
|
程序员 开发者
分代回收和手动内存管理相比有何优势
分代回收和手动内存管理相比有何优势
|
12月前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
12月前
|
算法 Java 程序员
内存回收
【10月更文挑战第9天】
421 5