jvm性能调优 - 09白话新生代垃圾回收算法

简介: jvm性能调优 - 09白话新生代垃圾回收算法

Pre

上一篇文章我们重新梳理了一下什么时候触发垃圾回收,以及到底哪些对象可以垃圾回收

另外,对新生代填满,GC Roots对象,软引用、弱引用,还有finalize()等概念进行了比较细致的梳理。

那么这篇文章,我们就来看看在对新生代进行垃圾回收的时候,到底是采取一种什么样的算法进行的呢?


复制算法的背景引入

针对新生代的垃圾回收算法,他叫做复制算法

简单来说,就是如下图所示,首先把新生代的内存分为两块。

接着假设有如下代码,在“loadReplicasFromDisk()”方法中创建了对象,此时对象就就会分配在新生代其中一块内存空间里

而且是由“main线程”的栈内存中的“loadReplicasFromDisk()”方法的栈帧内的局部变量来引用的,如下图所示。

接着大家想象一下,假设与此同时,代码在不停的运行,然后大量的对象都分配在了新生代内存的其中一块内存区域里,也只会分配在那块区域里

而且分配过后,很快就失去了局部变量或者类静态变量的引用,成为了垃圾对象

此时如下图所示。

接着这个时候,新生代内存那块被分配对象的内存区域基本都快满了,再次要分配对象的时候,发现里面内存空间不足了。

那么此时就会触发Minor GC去回收掉新生代那块被使用的内存空间的垃圾对象。

那么回收的时候是怎么做的呢?


一种不太好的垃圾回收思路

假设现在采用的垃圾回收思路,就是直接对上图中被使用的那块内存区域中的垃圾对象进行标记

也就是根据上篇文章讲的那套思路,标记出哪些对象是可以被垃圾回收的,然后就直接对那块内存区域中的对象进行垃圾回收,把内存空出来。

试想一下 这种思路好吗?

这种思路去垃圾回收,可能会在回收完毕之后造成那块内存区域看起来跟下图一样。

看上面的图,不知道大家发现什么没有,在那块被使用的内存区域里,回收掉了大量的垃圾对象,但是保留了一些被人引用的存活对象

但是呢,存活对象在内存区域里东一个西一个,非常的凌乱,而且造成了大量的内存碎片。

那么什么是内存碎片呢?

我们再看下面的图我用红线标记出来的区域,那些就是所谓的内存碎片。

看到了吗?在各种凌乱的存活对象的中间,出现了大量的红圈圈出来的内存碎片

这些内存碎片的大小不一样,有的可能很大,有的可能很小。

那么内存碎片太多会造成什么问题呢?

内存浪费

啥意思?比如现在打算分配一个新的对象,尝试在上图那块被使用的内存区域里去分配

此时如下图所示,可能因为内存碎片太多的缘故,虽然所有的内存碎片加起来其实有很大的一块内存,但是因为这些内存都是碎片式分散的,所以导致没有一块完整的足够的内存空间来分配新的对象。

所以这种直接对一块内存空间回收掉垃圾对象,保留存活对象的方法,绝对是不可取的

因为内存碎片太多,就是他最大的问题,会造成大量的内存浪费,很多内存碎片压根儿是没法使用的。


一个合理的垃圾回收思路

那么能不能用一种合理的思路来进行垃圾回收呢?

可以!这个时候上图中一直没派上用场的另外一块空白的内存区域就出场了。

首先,并不是按照上述思路直接对已经使用的那块内存把垃圾对象全部回收掉,然后保留全部存活对象。

而是先对那块在使用的内存空间标记出里面哪些对象是不能进行垃圾回收的,就是要存活的对象

然后先把那些存活的对象转移到另外一块空白的内存中,如下图。

不知道大家发现这里的玄机没有?

没错,通过把存活对象先转移到另外一块空白内存区域,我们可以把这些对象都比较紧凑的排列在内存里

这样就可以让被转移的那块内存区域几乎没有什么内存碎片,对象都是按顺序排列在这块内存里的。

然后那块被转移的内存区域,是不是多出来一大块连续的可用的内存空间?

此时就可以将新对象分配在那块连续内存空间里了,如下图。

这个时候,再一次性把原来使用的那块内存区域中的垃圾对象全部一扫而空,全部给回收掉,空出来一块内存区域,如下图。

这就是所谓的“复制算法“,把新生代内存划分为两块内存区域,然后只使用其中一块内存

待那块内存快满的时候,就把里面的存活对象一次性转移到另外一块内存区域,保证没有内存碎片

接着一次性回收原来那块内存区域的垃圾对象,再次空出来一块内存区域。两块内存区域就这么重复着循环使用。


复制算法有什么缺点?

复制算法的缺点其实非常的明显,如果按照上述的思路,大家会发现,假设我们给新生代1G的内存空间,那么只有512MB的内存空间是可以用的

另外512MB的内存空间是一直要放在那里空着的,然后512MB内存空间满了,就把存活对象转移到另外一块512MB的内存空间去

从始至终,就只有一半的内存可以用,这样的算法显然对内存的使用效率太低了。


复制算法的优化:Eden区和Survivor区

之前分析过,系统运行时,对JVM内存的使用模型,大体上就是我们的代码不停的创建对象然后分配在新生代里,但是一般很快那个对象就没人引用了,成了垃圾对象。

接着一段时间过后,新生代就满了,此时就会回收掉那些垃圾对象,空出来内存空间,给后续其他的对象来使用。

但是我们之前分析过,其实绝大多数的对象都是存活周期非常短的对象,可能被创建出来1毫秒之后就没人引用了,他就是垃圾对象了。

所以大家可以想象一下,可能一次新生代垃圾回收过后,99%的对象其实都被垃圾回收了,就1%的对象存活了下来,可能就是一些长期存活的对象,或者还没使用完的对象。

所以实际上真正的复制算法会做出如下优化,把新生代内存区域划分为三块:

1个Eden区,2个Survivor区,其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说Eden区有800MB内存,每一块Survivor区就100MB内存,如下图。

平时可以使用的,就是Eden区和其中一块Survivor区,那么相当于就是有900MB的内存是可以使用的,如下图所示。

但是刚开始对象都是分配在Eden区内的,如果Eden区快满了,此时就会触发垃圾回收

此时就会把Eden区中的存活对象都一次性转移到一块空着的Survivor区。接着Eden区就会被清空,然后再次分配新对象到Eden区里,然后就会如上图所示,Eden区和一块Survivor区里是有对象的,其中Survivor区里放的是上一次Minor GC后存活的对象。

如果下次再次Eden区满,那么再次触发Minor GC,就会把Eden区和放着上一次Minor GC后存活对象的Survivor区内的存活对象,转移到另外一块Survivor区去。

所以这里大家就能体会到,为啥是这么分配内存空间了。

因为之前分析了,每次垃圾回收可能存活下来的对象就1%,所以在设计的时候就留了一块100MB的内存空间来存放垃圾回收后转移过来的存活对象

比如Eden区+一块Survivor区有900MB的内存空间都占满了,但是垃圾回收之后,可能就10MB的对象是存活的。

此时就把那10MB的存活对象转移到另外一块Survivor区域就可以,然后再一次性把Eden区和之前使用的Survivor区里的垃圾对象全部回收掉,如下图。

接着新对象继续分配在Eden区和另外那块开始被使用的Survivor区,然后始终保持一块Survivor区是空着的,就这样一直循环使用这三块内存区域。

这么做最大的好处,就是只有10%的内存空间是被闲置的,90%的内存都被使用上了

无论是垃圾回收的性能,内存碎片的控制,还是说内存使用的效率,都非常的好。


新生代垃圾回收的各种“万一”怎么处理?

比如:

  • 万一垃圾回收过后,存活下来的对象超过了10%的内存空间,在另外一块Survivor区域中放不下咋整?
  • 万一我们突然分配了一个超级大的对象,大到啥程度?新生代找不到连续内存空间来存放,此时咋整?
  • 到底一个存活对象要在新生代里这么来回倒腾多少次之后才会被转移都老年代去?

别着急,下一篇文章就会来分析这些新生代的各种“万一”情况,以及新生代的对象是如何转移到老年代的,然后老年代是如何触发垃圾回收的,垃圾回收的算法又是什么样的。


思考

还记得之前教给过大家的那个系统对内存使用压力的估算方法么?

可以借助那个方法估算一下,每秒钟系统会使用多少内存空间,然后多长时间会触发一次垃圾回收?

垃圾回收之后,你们系统内大体会有多少对象存活下来?为什么?

然后都有哪些对象会存活下来?存活下来的对象会占多少内存空间?

希望大家多结合自己负责的系统来思考,养成一个核心能力,能够从JVM的角度去考虑系统运行时的模型

这样在真正发生JVM内存问题的时候,就能有一个非常深入的思考能力去解决问题。


相关文章
|
2天前
|
Prometheus 监控 算法
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
本文介绍了CMS(Concurrent Mark-Sweep)垃圾回收器的工作原理、优缺点及常见问题,并通过具体案例分析了其优化策略。重点探讨了CMS的各个阶段,包括标记、并发清理和重标记
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
|
12天前
|
缓存 算法 Java
JVM实战—4.JVM垃圾回收器的原理和调优
本文详细探讨了JVM垃圾回收机制,包括新生代ParNew和老年代CMS垃圾回收器的工作原理与优化方法。内容涵盖ParNew的多线程特性、默认线程数设置及适用场景,CMS的四个阶段(初始标记、并发标记、重新标记、并发清理)及其性能分析,以及如何通过合理分配内存区域、调整参数(如-XX:SurvivorRatio、-XX:MaxTenuringThreshold等)来优化垃圾回收。此外,还结合电商大促案例,分析了系统高峰期的内存使用模型,并总结了YGC和FGC的触发条件与优化策略。最后,针对常见问题进行了汇总解答,强调了基于系统运行模型进行JVM参数调优的重要性。
JVM实战—4.JVM垃圾回收器的原理和调优
|
16天前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
13天前
|
消息中间件 存储 算法
JVM实战—3.JVM垃圾回收的算法和全流程
本文详细介绍了JVM内存管理与垃圾回收机制,涵盖以下内容:对象何时被垃圾回收、垃圾回收算法及其优劣、新生代和老年代的垃圾回收算法、Stop the World问题分析、核心流程梳理。
JVM实战—3.JVM垃圾回收的算法和全流程
|
3月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
98 0
|
12天前
|
消息中间件 算法 Java
JVM实战—5.G1垃圾回收器的原理和调优
本文详细解析了G1垃圾回收器的工作原理及其优化方法。首先介绍了G1通过将堆内存划分为多个Region实现分代回收,有效减少停顿时间,并可通过参数设置控制GC停顿时长。接着分析了G1相较于传统GC的优势,如停顿时间可控、大对象不进入老年代等。还探讨了如何合理设置G1参数以优化性能,包括调整新生代与老年代比例、控制GC频率及避免Full GC。最后结合实际案例说明了G1在大内存场景和对延迟敏感业务中的应用价值,同时解答了关于内存碎片、Region划分对性能影响等问题。
|
3月前
|
算法 网络协议 Java
【JVM】——GC垃圾回收机制(图解通俗易懂)
GC垃圾回收,标识出垃圾(计数机制、可达性分析)内存释放机制(标记清除、复制算法、标记整理、分代回收)
|
3月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
4月前
|
机器学习/深度学习 监控 算法
Java虚拟机(JVM)的垃圾回收机制深度剖析####
本文深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法、性能调优策略及未来趋势。通过实例解析,为开发者提供优化Java应用性能的思路与方法。 ####
90 1
|
4月前
|
算法 Java
JVM有哪些垃圾回收算法?
(1)标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。 (2)复制算法: 将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。用在新生代 (3)标记整理算法: 与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。用在老年代
43 0