JVM 中存放对象的地方
大家好,我是程序员学长
今天我们来聊一下 JVM 中一个比较重要的概念 -- 堆
下图是我们通过 jdk自带的插件 jvisualvm 来查看堆空间的内容。
堆内存的细分
java7及之前堆内存在逻辑上划分为三部分:新生区、养老区和永久区。
-
Young/New Generation Space 新生区又被划分为 Eden 区和 Survivor 区。 -
Old/Tenure Generation Space 养老区。 -
Permanent Space 永久区
java8 及之后堆内存逻辑上分为三部分:新生区、养老区和元空间。
-
Young/New Generation Space 新生区又被划分为 Eden 区和 Survivor 区。 -
Old/Tenure Generation Space 养老区。 -
Meta Space 元空间
堆空间内部结构从JDK1.8开始,由之前的永久代变成了元空间。
设置堆内存的大小
-
-Xms 用于表示堆区的起始内存,等价于 -xx:InitialHeapSize。 -
-Xmx 用于表示堆区的最大内存,等价于 -xx:MaxHeapSize。
/**
*
* -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
* -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
*
*/
public class TestHeapSize {
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 );
System.out.println("-Xmx:" + maxMemory );
}
}
年轻代和老年代
存储在 JVM 中的 Java 对象可以被划分为两类。
-
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。 -
另外一类对象的生命周期却非常长,在某些极端的情况下还能与 JVM 生命周期保持一致。
对象分配过程
-
新new的对象先放在 Eden(伊甸园)区。 当 Eden(伊甸园)区的空间被填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收 (Minor GC),将伊甸园区中不再被其它对象所引用的对象进行销毁。
-
然后将伊甸园区中剩余的对象移动到 Survivor (幸存者)0 区。 如果再次触发垃圾回收,此时如果上次幸存下来的放置到幸存者 0区的对象,如果没有被垃圾回收掉,将会放入 幸存者 1 区中。
-
如果再次经历垃圾回收,此时会重新回到幸存者0区,接着再去幸存者1区。 -
那么什么时候进入老年代呢?默认情况下是经历 15 次的循环往复。 在老年代中,相对比较悠闲。当老年代内存不足时,触发垃圾回收(Major GC),进行老年代的内存清理。
若在老年代执行 Major GC 后,发现依然无法进行对象的保存,就会产生 OOM 的异常。
图解过程
总结
-
对于幸存者 S0、S1 来说,哪个区为空,哪个就是 to。
-
对于垃圾回收来说,频繁的在年轻代收集,很少在老年代收集,几乎不在永久代和元空间进行垃圾收集。
MinorGC、MajorGC、FullGC
部分收集:不是收集整个 Java 堆的垃圾收集。其中又分为:
-
年轻代收集,MinorGC,只是年轻代的垃圾收集。 老年代收集,MajorGC,只是老年代的垃圾收集。目前,只有 CMS GC 垃圾收集器会有单独收集老年代的行为。这里需要注意一点,在很多时候 MajorGC 会和 FullGC 混淆使用,需要分辨是老年代回收还是整堆回收。
混合收集(MixedGC),收集整个年轻代和老年代的垃圾收集,目前只有 G1 垃圾收集器有这种行为。
整堆收集:收集整个 Java 堆和方法区的垃圾收集。
MinorGC
Major GC
Full GC
触发 FullGC 执行的情况有如下五种:
-
调用 System.gc()时,系统建议执行 FullGC,但是不必然执行。 -
老年代空间不足时。 -
方法区空间不足时。 -
通过 MinorGC 后进入老年代的平均大小大于老年代的可用内存时。 由 Eden 区、Survivor 区(From区)向 Survivor 区(To 区)复制时,对象大小大于 To 区可用内存时,则把该对象转存到老年代,此时如果老年代的可用内存小于该对象大小时。
Full GC 是开发或调优中尽量要避免的。
堆空间的分代思想
老年代:存放新生代中经历多次 GC 仍存活的对象。
内存分配策略
针对不同年龄段的对象分配原则如下所示:
-
优先分配到 Eden
开发中比较长的字符串或者数组,会直接存在老年代。但是因为新创建的对象都是“朝生夕死”的,所以这个大对象可能也很快被回收。但是因为老年代触发 Major GC 的次数比 Minior GC 要更少,因此回收起来就会比较慢。 -
大对象直接分配到老年代
尽量要避免程序中出现过多的大对象。
-
长期存活的对象分配到老年代
-
动态对象年龄判断
如果 Survivor 区中相同年龄的所有对象的大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
空间分配担保 -xx:HandlePromotionFailure
为对象分配内存 TLAB
问题:堆空间都是共享的吗?
为什么有TLAB
堆区是线程共享的区域,任何线程都可以访问到堆区中的共享数据。
什么是TLAB
一旦对象在 TLAB 空间分配内存失败,JVM 会尝试着通过加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存(对象首先是通过 TLAB 开辟空间,如果不能放入,那么需要通过 Eden 来进行分配)。
总结
年轻代是对象的诞生、成长和消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、清理。
老年代放置长生命周期的对象,通常都是从 Survivor 区筛选拷贝过来的 java 对象。当然,也会有特殊的情况,我们知道普通的对象会被分配到 TLAB 上;如果对象较大,JVM 会试图直接分配在 Eden 区其它位置上;如果对象太大,完全无法在新生代找到足够大的连续空闲空间,JVM会直接分配到老年代。
当 GC 只发生在年轻代时,回收年轻代对象的行为被称为 MinorGC。当GC发生在老年代时,则被称为 MajorGC 或者 FullGC。
一般来说,MinorGC 的发生频率要比 MajorGC 高很多,即老年代中垃圾回收发生的频率大大低于年轻代。
最后
到此为止,我们就把 JVM 的堆区 聊完了,如果觉得不错,转发、在看、点赞安排起来吧。
你知道的越多,你的思维越开阔。我们下期再见。
祝大家五一快乐。