如何减慢一个对象进入老年代的速度,如何降低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及之后的版本中,默认情况下,元空间是没有硬性限制的(即没有默认的最大值),它会根据需要扩展,直到受限于系统内存。
垃圾收集的流程
常规情况
- 对象最初分配在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
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中都是会被作为垃圾被回收的
栈中的引用被回收,它所引用的对象会被垃圾回收吗
要看情况,当这个实例对象栈中的引用被回收时,要看下这个对象是否还能GC ROOT的起始点可达,比如局部变量,栈中的引用被回收时,那就是不可达,下次GC被垃圾回收,如果是静态变量,栈中的引用被回收时,这个对象依然还能通过GC ROOT的起始点可达