彻底理解对象内存分配及Minor GC和Full GC全过程

简介: 某数据计算系统,日处理亿级数据量。系统不断从各种数据源提读数据,加载到JVM内存进行计算处理

案例


某数据计算系统,日处理亿级数据量。系统不断从各种数据源提读数据,加载到JVM内存进行计算处理:


9.png


该系统不停通过SQL等方式从各数据存储中读数据到内存中进行计算,执行500次/min的数据提取和计算任务。这是套分布式系统,线上部署了多台机器:


每台机器大概负责执行100次/min的数据提取和计算任务

每次读约1w条数据到内存进行计算,平均每次计算约耗10s

每台机器4核8G,JVM内存4G:新生代、老年代分别1.5G

10.png


新生代多久会满?


每次1w条数据大概占多大内存呢?这里每条数据都较大,平均每条数据含20个字段,可认为平均每条数据1KB,则每次计算任务的1w条数据就是10MB。


若新生代按8:1:1分配Eden和两块Survivor区域,则Eden=1.2GB,每块Survivor=100MB:


11.png


则每次执行一个计算任务,就会在Eden分配10MB对象,约对应100次/min的计算任务。所以新生代里的Eden区,基本上1min左右就满。


Minor GC时,多少对象进入老年代?


假设新生代Eden在1min后满了,然后接着继续执行计算任务时,必然导致需要进行Minor GC,回收一部分垃圾对象。执行Minor GC之前会先进行检查:


第一步,老年代可用内存>新生代全部对象?


此时老年代空,有1.5G可用内存,新生代Eden大概算做有1.2G对象。

12.png



此时,即使一次Minor GC,全部对象都存活,老年代也能放下,则此时就会直接执行Minor GC。


那此时Eden有多少对象存活,无法被GC呢?

每个计算任务1万条数据,需计算10s,假设此时80个计算任务都执行结束,但还有20个计算任务共计200MB数据还在计算,此时就是200MB对象是存活的,不能被GC,然后有1G对象可GC:

13.png



此时一次Minor GC就会回收1G对象,然后200M对象能放入Survivor区吗?


**不能!**因为任一块Survivor区实际上就100M空间,此时就会通过空间担保机制,让这200MB对象直接进入老年代,占用里面200MB内存空间,然后Eden区清空:

14.png



系统运行多久,老年代大概就会填满?


这系统大概运行多久,老年代会填满?按上述计算,每min都是个轮回,大概就是每min都会把新生代Eden填满,然后触发一次Minor GC,然后大概都会有200MB数据进入老年代。


若2min过去了,此时老年代有400M被占用,只有1.1G可用,若第3分钟运行完毕,又要进行Minor GC,会做什么检查呢?

15.png



先检查老年代可用空间 > 新生代全部对象?

此时老年代可用空间1.1GB,新生代对象有1.2GB,此时假设一次Minor GC过后新生代对象全部存活,老年代是放不下的,就得看“-XX:-HandlePromotionFailure”参数,一般都会打开


此时会进入第二步检查


老年代可用空间 > 历次Minor GC过后进入老年代的对象的平均大小

大概每min执行一次Minor GC,每次大概200M对象进入老年代。那此时发现老年代1.1G,大于每次Minor GC后平均的200M。所以本次Minor GC后大概率还是有200MB对象进入老年代,1.1G可用空间足够。所以此时就会放心执行一次Minor GC,然后又是200MB对象进入老年代。


转折点大概在运行了7min后,7次Minor GC后,大概1.4G对象进入老年代,老年代剩余空间就不到100MB ,几乎快满:

16.png



系统运行多久,老年代会触发1次Full GC?

大概在第8min运行结束时,新生代又满,执行Minor GC前进行检查,发现老年代只有100M,比200M小,就会直接触发一次Full GC。


Full GC会把老年代的垃圾对象都给回收,假设此时老年代被占据的1.4G全都是可回收对象,则此时一次就会把这些对象都回收:

17.png



然后接着就会执行Minor GC,此时Eden区情况,200MB对象再次进入老年代,之前Full GC就是为这些新生代本次Minor GC要进入老年代的对象准备:

18.png



基本平均7、8分钟一次Full GC,这频率相当高。因为每次Full GC很慢, 性能很差。


如何JVM调优?

因为这数据计算系统,每次Minor GC的时候,必然会有一批数据没计算完,但按现有内存模型,最大问题是每次Survivor区放不下存活对象。


所以增加新生代内存比例,3GB堆内存,2GB分给新生代, 1GB给老年代。这样Survivor区大概200M,每次刚好能放下Minor GC后存活对象:

19.png



只要每次Minor GC过后200MB存活对象可以放Survivor区域,等下次Minor GC时,这个Survivor区的对象对应的计算任务早就结束了,都可以回收。此时比如Eden区1.6G被占满,然后Survivor1有200MB上一轮 Minor GC后存活的对象:

20.png



然后此时执行Minor GC,就会把Eden 1.6G对象回收,Survivor1里200MB对象也会回收,然后Eden区剩余200MB存活对象会放入Survivor2区:


21.png


以此类推,基本上就很少对象会进入老年代,老年代里的对象也不会太多。


通过这个优化,成功将生产系统的老年代Full GC频率从几min一次降低到几h一次,大幅提升系统性能,避免频繁Full GC对系统性能影响。


一个动态年龄判定升入老年代的规则,若:


S u r v i v o r 区 中 的 同 龄 对 象 > 超 过 S u r v i v o r 区 内 存 / 2 Survivor区中的同龄对象>超过Survivor区内存/2Survivor区中的同龄对象>超过Survivor区内存/2


就要直接升入老年代。所以此处优化仅是为说明:增加Survivor区大小,让Minor GC后的对象进入Survivor区中,避免进入老年代。


为避免动态年龄判定规则把Survivor区中的对象直接升入老年代,若新生代内存有限,可调整"- XX:SurvivorRatio=8"参数:默认Eden区比例80%,也可降低Eden区比例,给两块Survivor区更多内存空间,然后让每次Minor GC后的对象进入Survivor区中,还可避免动态年龄判定规则直接把他们送入老年代。


垃圾回收器


在新生代和老年代进行垃圾回收的时候,都是要用垃圾回收器进行回收的,不同的区域用不同的垃圾回收器。


**Serial和Serial Old垃圾回收器:**分别用来回收新生代和老年代的垃圾对象


工作原理就是单线程运行,垃圾回收的时候会停止我们自己写的系统的其他工作线程,让我们系统直接卡死不动,然后让他们垃圾回收,这个现在一般写后台Java系统几乎不用。


**ParNew和CMS垃圾回收器:**ParNew现在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,他们都是多线程并发的机制,性能更好,现在一般是线上生产系统的标配组合。下周会着重分析这两个垃圾回收器。


**G1垃圾回收器:**统一收集新生代 和老年代,采用了更加优秀的算法和设计机制,是下下周的重点,一周都会来分析G1垃圾回收器的工作原理和优缺点。

目录
相关文章
|
11天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
1月前
|
存储 Java
深入理解java对象的内存布局
这篇文章深入探讨了Java对象在HotSpot虚拟机中的内存布局,包括对象头、实例数据和对齐填充三个部分,以及对象头中包含的运行时数据和类型指针等详细信息。
28 0
深入理解java对象的内存布局
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(二)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
1月前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
2月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(三)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(一)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作