JVM运行时数据区篇(堆空间进阶掌握)
Posted ProChick
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM运行时数据区篇(堆空间进阶掌握)相关的知识,希望对你有一定的参考价值。
1.堆中对象分配的一般过程
为新对象分配内存是件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配的问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
-
首先把新生成的对象放在伊甸园区
-
当伊甸园的空间满时,就会触发JVM的垃圾回收机制,此时将使用
Minor GC/ Yong GC
垃圾回收器对伊甸园区进行垃圾回收 -
将伊甸园区中的不再被其他对象所引用的对象进行销毁,然后将伊甸园中的剩余还需使用的对象移动到其中一个空的幸存者区(
0区或者1区
),并设置一个age
计数器,数值初始化为1
-
如果再次触发垃圾回收机制,也会对存有对象的幸存者区进行回收。如果对象不再使用,则丢弃;如果还需使用,则将其转移到另外一个空的幸存者区,并将其
age
计数器数值加一
-
每当伊甸园的空间满时,都会重复以上过程
-
当某个幸存者区对象的
age
计数器数值达到15(默认值,可以通过参数:-XX:MaxTenuringThreshold进行设置
),如果它还需要被使用,那么就会把它转移到养老区
-
当养老区内存不足时,再次触发垃圾回收机制,此时将使用
Major GC
垃圾回收器对养老区进行垃圾回收 -
当养老区执行完垃圾回收之后发现空间依然不足,那么就会产生OOM异常
2.堆中对象分配代码示例
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) {
ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.堆中垃圾回收器常见种类
JVM在进行GC垃圾回收的时候,并非每次都是针对上面三个内存区域(
新生代、老年代、方法区
)一起回收的,大部分时候都是对新生代区域进行回收。 针对 HotSpot VM 的实现,按照回收区域将垃圾回收器大体分为两种类型:一种是部分收集(Partial GC
),一种是整堆收集(Full GC
)
-
部分收集(Partial GC)
只对部分堆空间进行的垃圾收集
- 新生代收集(Minor GC/Young GC):对新生代区域的垃圾收集
- 老年代收集(Major GC/Old GC):对老年代区域的垃圾收集,目前只有
CMS GC
会单独收集老年代垃圾。很多时候Major GC
会和Full GC
混淆使用,需要具体分辨是对老年代的回收还是对整个堆空间的回收 - 混合收集(Mixed GC):对整个新生代以及部分老年代的垃圾收集,目前只有
G1 GC
会进行混合收集
-
整堆收集(Full GC)
对整个堆空间和方法区进行的垃圾收集
4.堆中垃圾回收器触发机制
-
Minor GC
- 当年轻代空间不足时,就会触发 Minor GC,这里的年轻代空间不足指的是 Eden 区域空间不足,而Survivor 区域空间不足则不会引发 Minor GC,因为它是被动触发的
- 因为Java对象大多都具备朝生夕灭的特性,所以 Monor GC 会被触发的非常频繁,一般回收速度也比较快
- 此外 Minor GC 会引发 STW(Stop the World),会导致用户线程暂时停止,只有等垃圾回收结束后,用户线程才恢复运行
-
Major GC
- 当老年代空间不足时,就会触发 Minor GC 或者 Full GC
- 出现了 Major GC,经常会伴随至少一次的 Minor GC发生(
不是绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程
) 。也就是当老年代空间不足时,会先尝试触发 Minor GC,如果之后空间还不足,则触发 Major GC - Major GC 速度一般会比 Minor GC 慢10倍以上,进而 STW 的时间也会更长
- 如果 Major GC 后老年代空间仍然不足,就会抛出OOM异常
-
Full GC
Full GC 是开发或调优中尽量要避免的,这样暂停用户线程的时间会短一些
- 在程序中手动调用
System.gc()
方法时,就会触发 Full GC,但是这也只是建议去执行Full GC,真正是否执行还要进行JVM自身的判断 - 老年代空间不足时,就会触发 Full GC
- 方法区空间不足时,就会触发 Full GC
- 由于Eden区、Survivor S0区、Survivor S1区都存不下某个对象, 则把该对象转存到老年代,此时当老年代的可用内存大小也存储不了该对象时, 就会触发 Full GC
- 在程序中手动调用
5.堆中垃圾回收代码示例
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "abc";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
System.out.println("遍历次数为:" + i);
}
}
}
6.堆中对象分代的思想
为什么要把Java堆空间进行年轻代、老年代的划分?不分代就不能正常工作了么
- 其实不分代完全可以,分代的唯一理由就是优化 GC 的性能。因为经研究发现,不同对象的生命周期不同,70%-99%的对象都是临时对象,我们只去频繁的对这些数据进行处理就可以了
- 如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。此时 GC 要进行垃圾回收的时候,就要找到哪些对象没用,这样就会对所有的堆空间进行扫描,很浪费时间
- 如果分代的话,把新创建的对象放到某一地方,当 GC 进行垃圾回收的时候先把这些临时对象的区域进行回收,这样就会腾出很大的空间出来。
7.堆中对象分配的策略
- 新生成的对象优先分配到年轻代的 Eden 区
- 长期存活的对象分配到老年代
- 大的对象直接分配到老年代,尽量避免程序中出现过多的大对象
- 动态对象年龄(
age计数器的值
)判断,如果年轻代的 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接分配到老年代 - 进行空间分配担保,可以通过参数
-XX: HandlePromotionFailure
设置
大对象直接分配到老年代的程序示例
/**
* 参数配置:-Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* 空间大小情况:年轻代(Eden区16m、Survivor0区2m、Survivor1区2m)、老年代(40m)
*/
public class YoungOldAreaTest {
public static void main(String[] args) {
// 申请一个20m大小的对象
byte[] buffer = new byte[1024 * 1024 * 20];
}
}
以上是关于JVM运行时数据区篇(堆空间进阶掌握)的主要内容,如果未能解决你的问题,请参考以下文章