jvm - GC篇

如何减慢一个对象进入老年代的速度,如何降低GC的次数

堆内存细分

年轻代(Young Generation):

新创建的对象首先被分配在年轻代中。年轻代又被进一步划分为一个Eden区和两个Survivor区(通常称为S0和S1)。

当Eden区满时,会触发一次Minor GC(垃圾回收),存活的对象会被移动到一个Survivor区,不存活的对象会被清理。

老年代(Old Generation 或 Tenured Generation)

  • 经过多次GC后仍存活的对象会被移动到老年代。老年代的空间通常比年轻代大,因为它存储的是生命周期较长的对象。
  • 老年代的垃圾回收频率通常低于年轻代,但每次GC耗时更长,因为涉及到更多的对象和更大的内存区域。

永久代(Permanent Generation)或元空间(Metaspace)

  • 在早期版本的JVM中,永久代用于存储JVM内部结构,如类的元数据、方法的字节码等。从Java 8开始,永久代被元空间替代。
  • 元空间不在堆内存中,而是直接使用本地内存(即操作系统的内存),主要用于存储类的元数据。

大对象区(Large Object Space,或称为Huge Object Space)

  • 一些JVM实现可能会为大对象提供专门的内存区域。这些对象由于大小超过了某个阈值,直接在老年代或特定的大对象区进行分配,以避免在年轻代中频繁复制。

空间大小

Eden与Survivor空间的比例可能接近8:1,也就是说,当每个Survivor空间占用10%的新生代空间时,Eden空间占用80%的新生代空间。新生代的总大小通常是堆内存的1/3到1/4,但这也取决于具体的JVM配置和可用内存。Survivor空间虽然有两个区域,但总有一个区域是空,也不会对其计算大小

老年代通常占据堆内存的其余部分。如果新生代占用了堆的1/3,那么老年代则大约占用2/3。

元空间并不在堆内存中,而是使用本地内存(native memory),用于存储类元数据。

在JDK 8及之后的版本中,默认情况下,元空间是没有硬性限制的(即没有默认的最大值),它会根据需要扩展,直到受限于系统内存。

对象会直接分配到老年代的情况

  • 大对象:如果一个对象的大小超过了年轻代的某个阈值(在HotSpot JVM中通常是 8KB),这个对象会直接分配到老年代。大对象的典型例子包括大型数组或大的数据结构。
  • 年轻代空间不足:如果年轻代中的空间不足以容纳新的对象,并且进行了一次年轻代的垃圾回收后仍然无法满足分配请求,JVM可能会尝试直接在老年代中分配对象。
  • jvm设置

垃圾收集的流程

常规情况

  • 对象最初分配在Eden区。随着应用程序的运行,Eden区会逐渐填满。当我们Eden区满了后,就会触发GC操作,一般被称为 YGC / Minor GC操作,将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。
  • 存活的对象会被移动到一个幸存者from区
  • 随着应用程序的运行,Eden区会再次填满,执行Minor GC操作,将伊甸园区中的不再被其他对象所引用的对象进行销毁,伊甸园区依然存活的对象存放到幸存者to区,同时幸存者from区的对象也复制到幸存者to区,经过一次回收后还存在的对象,将其年龄加 1。如此循环往复,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion 晋升的操作,也就是将年轻代中的对象晋升到老年代中,在对伊甸园区做GC的时候,幸存者区满了,幸存对象会被直接提升到老年代(Old Generation)。这意味着即使对象的年龄没有达到通常提升的阈值,它们也会被移动到老年代,因为没有足够的空间在幸存者区容纳它们
  • 如果老年代也没有足够的空间来容纳这些被提升的对象,JVM可能会触发一个完整的垃圾收集(Full GC),这是一个更彻底的收集过程,涉及整个堆,包括年轻代和老年代。Full GC通常比仅针对年轻代的Minor GC要慢得多,因为它需要检查和清理整个堆空间。
  • 内存不足错误(OutOfMemoryError):如果老年代也已满,并且无法为对象提供更多空间,JVM将无法继续运行,并抛出java.lang.OutOfMemoryError。这通常是一个严重的错误,表明应用程序需要更多内存,或者有内存泄漏需要修复

存在大对象的情况

对象过大,Eden园区放不下了,此时要先对Eden园区做一下垃圾回收,如果还是放不下,说明这是一个大对象,可以直接往老年代去放,老年代还是放不下,进行FULL GC,还是放不下,报OOM,重复过程:这个过程会随着Eden区的再次填满而重复,Survivor From区和Survivor To区会在每次Minor GC后交换角色。

from/to区,翻转的目的/好处是什么?

  • 内存回收:垃圾收集的主要任务是识别并回收不再使用的对象所占用的内存。通过翻转,可以清空整个伊甸园区(Eden Space)和一个幸存者区("From"),这样能快速回收大量内存。
  • 减少碎片:通过将存活对象复制到一个连续的内存区域("To"区),可以避免内存碎片的产生。这样,存活对象在内存中保持紧凑排列,而不是散布在内存的各个角落。
  • 方便计数:在这个过程中,对象的年龄也被跟踪。每次对象在幸存者区之间移动时,它们的年龄就会增加

命令

-Xms:设置堆空间大小初始内存大小(年轻代 + 老年代),默认是服务器可用物理内存的1/64,但服务器可用物理内存比物理内存值要小,因为操作系统自身会占用一部分内存。所以想要一个精确值需要手动去指定,比如-Xms600m,最终实际值大约是575,因为俺默认大小来算,幸存者区占1/24,始终有一个是空的

-Xmx:设置堆空间大小最大内存大小(年轻代 + 老年代),默认是服务器物理内存的1/4

-XX:Survivor:设置幸存者区在新生代的比例,默认是8

-XX:NewRatio:设置新生代和老年代的比例,默认是2,即新生代占总内存的1/3

-XX:printGCDetail

jinfo -flag NewRatio

java 复制代码
public static void main(string[] args){
	//返回Java虚拟机中的堆内存总量
	long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
	//返回Java虚拟机试图使用的最大堆内存量
	long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
	System.out.println("-Xms : " + initialMemory + "M");
	System.out.println("-Xmx : " + maxMemory + "M");
}

开发环境设置,建议堆空间大小初始内存大小和堆空间大小最大内存大小相等,避免频繁的扩容与释放

堆的细分

开发中,不同的对象生命周期不同,有的对象转瞬即逝,有的对象甚至伴随整个jvm的运行周期

  • 转瞬即逝,比如局部变量,方法执行完毕对应的引用出栈,后续会被回收
  • 生命周期非常长的对象,比如静态变量,集合,例如我们比较熟悉的Spring使用一级缓存来存储单实例类型的bean,这个一级缓存本质上也是一个静态的map,此外还有各种池类资源比如线程池,链接池,他们的生命周期都很长,往往伴随整个jvm的运行周期

生命周期非常长的对象,没有必要对他们持续的做GC,所以当一个对象年龄超过15的时候,jvm认为这是一个稳定的对象,晋升老年代,老年代的GC频率是比较低的

伊甸园区

-XX:Survivor:设置幸存者区在新生代的比例,默认是8,但有一个自适应机制,所以需要显示声明这个值为8,才能获得正确的比例

-XX maxTenuringThreshold:

-Xmn:设置新生代大小

超过了80%对象都是朝生夕死的

几户所有对象都是在伊甸园区创建的,除非是

如何使对象更快的从新生代晋升老年代

在JVM的垃圾回收机制中,对象的晋升主要是通过两种方式实现的:

通过设置对象的年龄阈值。当对象在新生代中经历了一定次数的垃圾回收后,就会被晋升到老年代中。这个次数可以通过-XX:MaxTenuringThreshold参数设置。默认情况下,这个值是15。如果你想让对象更快的晋升到老年代,你可以降低这个值。

通过设置新生代的大小。如果新生代的空间不足以容纳所有的存活对象时,那么还没有达到年龄阈值的对象也会被晋升到老年代。因此,你也可以通过减小新生代的大小来加速对象的晋升。这个可以通过-XX:NewSize参数设置。

以上两种方式都可以使对象更快的从新生代晋升到老年代,但是也需要注意,过快的晋升可能会导致老年代的空间占用增加,从而影响到垃圾回收的效率。所以,在设置这些参数时,需要根据实际的应用情况进行权衡。

可达性分析算法

可达性分析算法的基本思路是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到"GC Roots"没有任何引用链相连时,则证明此对象是不可用的,可作为GC Roots引用链的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。
  • 方法区中常量引用的对象。
  • 方法区中类静态属性引用的对象。
  • 被同步锁synchronized持有的对象

被堆中某个实例所引用的对象会被垃圾回收吗

如果只是被堆中某个实例锁引用,那要看这个引用它的实例对象是否由GC ROOT的起始点可达,如果不可达,如果说明这只是实例对象之间的引用,那么这些实例对象在下次GC中都是会被作为垃圾被回收的

Minor GC如何再只扫描年轻代的情况下完成全堆扫描

当做Minor GC的时候,从GC Roots出发,如果发现老年代的对象,那就不往下走了,Minor GC会避开堆老年代对象的扫描,HotSpot虚拟机下 有卡表来避免全局扫描老年代对象候选者,在堆内存的一小块区域形成卡页,卡表实际上就是卡页的集合。当判断一个卡页中有存在对象的跨代引用时,将这个页标记为脏页候选者:那知道了「卡表」之后,就很好办了。每次Minor GC 的时候只需要去「卡表」找到「脏页」,找到后加入至GC Root,而不用去遍历整个「老年代」的对象了。

栈中的引用被回收,它所引用的对象会被垃圾回收吗

要看情况,当这个实例对象栈中的引用被回收时,要看下这个对象是否还能GC ROOT的起始点可达,比如局部变量,栈中的引用被回收时,那就是不可达,下次GC被垃圾回收,如果是静态变量,栈中的引用被回收时,这个对象依然还能通过GC ROOT的起始点可达

相关推荐
流星52112214 小时前
GC 如何判断对象该回收?从可达性分析到回收时机的关键逻辑
java·jvm·笔记·学习·算法
JanelSirry14 小时前
我的应用 Full GC 频繁,怎么优化?
jvm
JH307315 小时前
jvm,tomcat,spring的bean容器,三者的关系
jvm·spring·tomcat
DKPT18 小时前
JVM直接内存和堆内存比例如何设置?
java·jvm·笔记·学习·spring
siriuuus19 小时前
JVM 垃圾收集器相关知识总结
java·jvm
小满、21 小时前
什么是栈?深入理解 JVM 中的栈结构
java·jvm·1024程序员节
百花~1 天前
JVM(Java虚拟机)~
java·开发语言·jvm
每天进步一点点dlb1 天前
JVM中的垃圾回收算法和垃圾回收器
jvm·算法
漫漫不慢.2 天前
蓝桥杯-16955 岁月流转
java·jvm·蓝桥杯
boy快快长大3 天前
【JVM】线上JVM堆内存报警,占用超90%
jvm