一文学会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给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。
相关文章
|
3天前
|
监控 算法 Java
Java虚拟机(JVM)的垃圾回收机制深度解析####
本文深入探讨了Java虚拟机(JVM)的垃圾回收机制,旨在揭示其背后的工作原理与优化策略。我们将从垃圾回收的基本概念入手,逐步剖析标记-清除、复制算法、标记-整理等主流垃圾回收算法的原理与实现细节。通过对比不同算法的优缺点及适用场景,为开发者提供优化Java应用性能与内存管理的实践指南。 ####
|
11天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
16天前
|
程序员 开发者
分代回收和手动内存管理相比有何优势
分代回收和手动内存管理相比有何优势
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
28天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
算法 Java 程序员
内存回收
【10月更文挑战第9天】
43 5
|
1月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
51 2
|
1月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
1月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。