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

简介: 新生代内存管理包含了内存的分配和回收,这与新生代内存布局密切相关。新生代被划分为3个空间:Eden、From和To空间。这3个空间的作用如下:

新生代内存管理

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

新生代被划分为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垃圾回收器详解:串行回收,新生代内存管理内存分配

相关文章
|
关系型数据库 MySQL 数据库
一个 MySQL 数据库死锁的案例和解决方案
本文介绍了一个 MySQL 数据库死锁的案例和解决方案。
801 3
|
XML Java 开发者
论面向方面的编程技术及其应用(AOP)
【11月更文挑战第2天】随着软件系统的规模和复杂度不断增加,传统的面向过程编程和面向对象编程(OOP)在应对横切关注点(如日志记录、事务管理、安全性检查等)时显得力不从心。面向方面的编程(Aspect-Oriented Programming,简称AOP)作为一种新的编程范式,通过将横切关注点与业务逻辑分离,提高了代码的可维护性、可重用性和可读性。本文首先概述了AOP的基本概念和技术原理,然后结合一个实际项目,详细阐述了在项目实践中使用AOP技术开发的具体步骤,最后分析了使用AOP的原因、开发过程中存在的问题及所使用的技术带来的实际应用效果。
312 5
|
数据可视化 安全 数据挖掘
streamlit (python构建web)之环境搭建
在微信订阅号中发现了一篇关于Streamlit的文章,激发了我的兴趣。Streamlit是一款专为数据科学家设计的开源Python库,能迅速将数据分析脚本转变为功能完备的Web应用。它简化了开发流程,支持轻松添加交互组件及动态展示图表、图像等,非常适合开发安全扫描工具。Streamlit基于Jupyter Notebook原理,通过Python脚本创建可视化和交互式的Web应用,易于部署分享。安装方法多样,可通过`pip install streamlit`快速安装,或通过Anaconda环境管理依赖。启动示例应用只需运行简单命令,即可体验自带的动画、绘图和数据展示等功能。
1453 1
streamlit (python构建web)之环境搭建
|
缓存 JavaScript 前端开发
拿下奇怪的前端报错(三):npm install卡住了一个钟- 从原理搞定安装的全链路问题
本文详细分析了 `npm install` 过程中可能出现的卡顿问题及解决方法,包括网络问题、Node.js 版本不兼容、缓存问题、权限问题、包冲突、过时的 npm 版本、系统资源不足和脚本问题等,并提供了相应的解决策略。同时,还介绍了开启全部日志、使用替代工具和使用 Docker 提供 Node 环境等其他处理方法。
10398 2
|
安全 UED
麒麟的版本 V10 (Lance) V10 (Tercel) 有什么区别
【6月更文挑战第26天】麒麟的版本 V10 (Lance) V10 (Tercel) 有什么区别
11657 2
|
存储 缓存 数据库
InfluxDB性能优化:写入与查询调优
【4月更文挑战第30天】本文探讨了InfluxDB的性能优化,主要分为写入和查询调优。写入优化包括批量写入、调整写入缓冲区、数据压缩、shard配置优化和使用HTTP/2协议。查询优化涉及索引优化、查询语句调整、缓存管理、分区与分片策略及并发控制。根据实际需求应用这些策略,可有效提升InfluxDB的性能。
3464 1
|
存储 缓存 负载均衡
图解一致性哈希算法,看这一篇就够了!
近段时间一直在总结分布式系统架构常见的算法。前面我们介绍过布隆过滤器算法。接下来介绍一个非常重要、也非常实用的算法:一致性哈希算法。通过介绍一致性哈希算法的原理并给出了一种实现和实际运用的案例,带大家真正理解一致性哈希算法。
26743 66
图解一致性哈希算法,看这一篇就够了!
|
XML 数据格式 Python
yq:命令行操作yaml文件
yq:命令行操作yaml文件
808 2
|
Android开发
Android 音乐APP(五)音乐通知栏、后台播放音乐
Android 音乐APP(五)音乐通知栏、后台播放音乐
1616 0
Android 音乐APP(五)音乐通知栏、后台播放音乐
|
存储 监控 安全
JVM系列之:MAT工具使用教程
JVM系列之:MAT工具使用教程
2138 0
JVM系列之:MAT工具使用教程