JVM 对象内存分配
对象的内存分配,就是在堆上分配(也可能经过 JIT 编译后被拆散为标量类型并间接在栈上分配),对象主要分配在新生代的 Eden 区上,少数情况下可能直接分配在老年代,分配规则不固定,取决于当前使用的垃圾收集器组合以及相关的参数配置。
常见的内存分配规则
对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
Minor GC vs Major GC/Full GC:
Minor GC:回收新生代(包括Eden和Survivor区域),因为 Java 对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。Major GC/Full GC:回收老年代,出现了Major GC,经常会伴随至少一次的Minor GC,但这并非绝对。Major GC的速度一般会比Minor GC慢 10 倍 以上。
在 JVM 规范中,
Major GC和Full GC都没有一个正式的定义,所以有人也简单地认为Major GC清理老年代,而Full GC清理整个内存堆。
大对象直接进入老年代
大对象是指需要大量连续内存空间的 Java 对象,如很长的字符串或数据。
一个大对象能够存入 Eden 区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及大量的复制,就会造成效率低下。
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。
长期存活的对象将进入老年代
JVM 给每个对象定义了一个对象年龄计数器。当新生代发生一次 Minor GC 后,存活下来的对象年龄 +1,当年龄超过一定值时,就将超过该值的所有对象转移到老年代中去。
使用 -XXMaxTenuringThreshold 设置新生代的最大年龄,只要超过该参数的新生代对象都会被转移到老年代中去。
动态对象年龄判定
如果当前新生代的 Survivor 中,相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄 >= 该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
空间分配担保
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。
通过清除老年代中的废弃数据来扩大老年代空闲空间,以便给新生代作担保。
以下是一些触发 JVM 进行 Full GC 的场景:
System.gc()方法的调用:此方法的调用是建议 JVM 进行Full GC,注意这只是建议而非一定,但在很多情况下它会触发Full GC,从而增加Full GC的频率。通常情况下我们只需要让虚拟机自己去管理内存即可,我们可以通过-XX:+ DisableExplicitGC来禁止调用System.gc()。- 老年代空间不足:老年代空间不足会触发
Full GC操作,若进行该操作后空间依然不足,则会抛出如下错误:java.lang.OutOfMemoryError: Java heap space - 永久代空间不足:JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中也称为永久代(Permanet Generation),存放一些类信息、常量、静态变量等数据,当系统要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,会触发
Full GC。如果经过Full GC仍然回收不了,那么 JVM 会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space - CMS GC 时出现
promotion failed和concurrent mode failure:promotion failed就是上文所说的担保失败,而concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。 - 统计得到的 Minor GC 晋升到旧生代的平均大小大于老年代的剩余空间。