5. JVM 堆
一、堆的核心概述
1.1 概念
Java 堆是虚拟机所管理的内存中最大的一块,它的唯一作用就是存放对象实例;因为它是垃圾回收器管理的内存区域,因此一些资料中也称它为 “GC 堆”。
1.2 特点
- Java 堆是线程共享的;
- 线程共享也不是绝对的,可以在共享的对空间中划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
- 方法结束后,堆中的对象并不会被马上溢出,仅仅在垃圾收集的时候才会被移除。这样做是因为防止频繁的 GC 影响性能
1.3 堆的分区
堆的分区是基于垃圾回收期的共同特性或者说是设计风格,并不是 JVM 具体实现的固定布局。现代垃圾回收器大部分都基于分代收集理论设计。
1.3.1 JDK7 及以前的分区
Young Generation Space:新生区
新生区又被分为 Eden 去和 Survivor 区
Tenure Generation Space:养老区
Permanent Space:永久区
1.3.2 JDK8 及以后的分区
Young Generation Space:新生区
新生区又被分为 Eden 去和 Survivor 区
Tenure Generation Space:养老区
Meta Space:元空间
二、设置堆内存大小与 OOM
2.1 设置堆大小的参数
堆的大小在 JVM 启动的时候就已经设定好了,默认的初始大小为电脑内存的 1/64,最大为电脑内存的 1/ 4。我们也可以通过 “-Xmx” 和 “-Xms” 来进行设置。
-Xmx:表示堆区的起始内存,等价于 -XX:InitialHeapSize
-Xms:表示堆区的最大内存,等价于 -XX:MaxHeapSize
在开发中一般将这两个设置成一样的,这样可以避免 GC 清理完堆区之后重新分隔计算堆区大小,从而提高性能。
三、对象的分配过程
3.1 分配过程
-
对象几乎都在伊甸园区创建,如果伊甸园区满了,就会触发 GC:Minor GC, 对新生区进行回收,将伊甸园区不再被引用的对象进行销毁,再加载新的对象到伊甸园区;
-
将伊甸园区中的剩余对象移动到 Survivior 0 区;
-
如果再次触发垃圾回收,上次幸存下来的放到 Survivior9 区的,如果没有被回收,就会放到 Survivior1 区;
-
重复第三步
-
当 Survivor 区中的对象年龄达到阈值,就会被放到养老区;
-
在养老区很少会触发 GC。但是当养老区内存不足时,也会触发GC:Major GC,进行养老区的清理;
-
如果养老区清理之后,以然无法保存对象,就会产生 OOM
上面用到了”几乎“,这是因为如果对象太大的话,也有可能会直接放在老年代;或者在 GC 的过程中,Survivor 出现了放不下对象的情况,则把该对象放到老年代。
四、Minor GC、Major GC、Full GC 的区别
JVM 在进行 GC 时,并不是每次都对整个堆空间和方法区一起回收的,大部分回收都是在新生代进行的。在不同的区域中有不同的 GC。
-
Minor GC:
-
负责进行新生代(Eden、Survivor)的垃圾收集,但是只有当 Eden 区满的时候才会触发 Minor GC,Survivor 满了不会触发;
-
因为大部分对象具备朝生夕死的特性,所以 Minor GC 会非常频繁
-
Minor GC 会引发 STW,暂停其他用户线程,但因为其速度快,相较于其他两个 GC,暂停时间最短
-
-
Major GC:
- Major GC 只负责老年代的垃圾收集
- 出现了 Major GC 伴随至少一次的 Minor GC,当老年代空间不足时,会先尝试触发 Minor GC,之后如果空间还不足,则会触发 Major GC
- Major GC 之后如果空间还不足,就报 OOM 了
- Major GC 比 Minor GC 慢 10 倍以上,STW 时间更长
-
Full GC:
- Full GC 会收集整个堆空间和方法区
- 当老年代空间不足或者方法区空间不足时会触发 Full GC
五、堆的分代思想和对象提升原则
5.1 堆的分代思想
不分代其实也完全可以正常工作,分代的唯一理由就是优化 GC 性能。如果没有分代,那么所有的对象就会被放到一块,当进行 GC 的时候必须扫描整个堆空间,而很多对象都是朝生夕死的,单独划分出一块区域来存储新对象,就会提高 GC 的性能。
5.2 对象提升的原则
-
如 3.1 所说,当 Survivor 区域中的对象年龄达到阈值,会将其提升到老年代;
-
Eden 区在 Minor GC 之后还放不下的大对象或者在 Minor GC 过程中 Survivor0/1 区放不下的对象,会被直接放到老年代,所以要避免创建过多的大对象;
-
动态对象年龄判断:如果 Survivor 区中相同年龄的所有对象大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代?
-
空间分配担保:在 Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间:
如果大于,则此次 Minor GC 是安全的;
如果小于,则虚拟机会检查老年代的最大可用连续空间是否大于历次晋升到老年代的对象的平均大小:
如果大于,尝试进行一次 Minor GC
如果小于,则改为进行一次 Full GC
六、堆空间参数的设置
官网:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
参数 | 说明 |
---|---|
-XX:+PrintFlagsInitial | 查看所有参数的默认初始值 |
-XX:+PrintFlagsFinal | 查看所有参数的最终值 |
-XX:+PrintGCDetails | 输出详细的GC处理日志 |
-Xms: | 初始堆空间内存(默认为物理内存的 1/64) |
-Xmx: | 最大堆空间内存 |
-Xmn | 设置新生代的大小 |
-XX:NewRatio | 设置新生代与老年代的占比 |
-XX:SurvivorRatio: | 设置新生代中 Eden 和 S0/S1 空间的比例 |
-XX:HandlePromotionFailure | 设置空间分配担保 |
七、逃逸分析
7.1 概述
随着逃逸分析逐渐成输,栈上分配、标量替换优化技术导致了一些微妙的变化,所有对象都分配到对上也不是那么绝对了。
逃逸分析就是分析对象动态作用域:如果一个对象被认定为只在方法内部被使用那么它就没有发生逃逸;相反,如果方法外部还引用了该对象,则认为发生逃逸。
7.2 基于逃逸分析的技术
-
栈上分配:栈上分配使得对象能够被分配到 JVM 栈上,这样就可以随着栈的弹出而释放内存,可以避免 GC,从而提高速度;
-
标量替换:
标量:无法被分解成更小的数据的数据
在 JIT 阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,就会把这个对象拆解成若干个成员变量来代替。
简单来说就是用成员变量来代替对象,这样做的好处就是可以大大减少堆内存的使用,从而提高速度。标量替换为栈上分配提供了很好的基础。
例:图一所示的代码经过标量替换之后就变成了图二所示的代码
7.3 逃逸分析的缺点
进行逃逸分析的这个过程也需要消耗性能,可能这个分析的过程消耗的性能会大于未经过逃逸分析而直接对上分配所消耗的性能;甚至有时候经过逃逸分析发现没有一个对象不逃逸,这就白白浪费了性能。