6 堆 Heap⭐️⭐️⭐️⭐️⭐️
6.1 核心概述
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
- 堆是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
堆内存细分
Java 7及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区
- Young Generation Space 新生区 Young/New
- 又被划分为Eden区和Survivor区
- Tenure generation space 养老区 Old/Tenure
- Permanent Space 永久区 Perm
Java 8及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间
- Young Generation Space 新生区 Young/New
- 又被划分为Eden区和Survivor区
- Tenure generation space 养老区 Old/Tenure
- Meta Space 元空间 Meta
约定:新生区(代)<=>年轻代 、 养老区<=>老年区(代)、 永久区<=>永久代
6.2 设置堆内存大小与OOM
堆空间大小设置
Java堆区用于存储Java对象实例,堆的大小在JVM启动时就已经设定好了,大家可以通过选项"-Xmx"和"-Xms"来进行设置。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在 Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下
- 初始内存大小:物理电脑内存大小 / 64
- 最大内存大小:物理电脑内存大小 / 4
public class HeapTest {
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");
}
}
查看设置的参数
- 方式1:jps / jstat -gc 进程id
- 方式2:-XX:+PrintFCDetails
OutOfMemory举例
public class OOMTest {
public static void main(String[]args){
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e){
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024*1024)));
}
}
}
打印结果
Exception in thread "main" java.lang.OutofMemoryError: Java heap space
at com.atguigu. java.Picture.<init>(OOMTest. java:25)
at com.atguigu.java.O0MTest.main(OOMTest.java:16)
6.3 年轻代与老年代
存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
- 默认
-XX:NewRatio=2
,表示新生代占1,老年代占2,新生代占整个堆的1/3
几乎所有的 Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。
6.4 图解对象分配过程
1. new的对象先放伊甸园区。此区有大小限制。
2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。
3. 然后将伊甸园中的剩余对象移动到幸存者0区。
4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
6. 啥时候能去养老区呢?可以设置次数。默认是15次。
○ 可以设置参数:进行设置-Xx:MaxTenuringThreshold= N
7. 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理
8. 若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。
-
伊甸园区的对象先往to区放(空的)
-
年龄计数器达到15晋升老年代
-
总结
- 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
- 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集
流程图
常用调优工具(在JVM下篇:性能监控与调优篇会详细介绍)
- JDK命令行
- Eclipse:Memory Analyzer Tool
- Jconsole
- VisualVM
- Jprofiler
- Java Flight Recorder
- GCViewer
- GC Easy
6.5 Minor GC、MajorGC、Full GC
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
-
- 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
-
- 老年代收集(Major GC / Old GC):只是老年代的圾收集。
-
-
- 目前,只有CMSGC会有单独收集老年代的行为。
-
-
-
- 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
-
-
- 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。
-
-
- 目前,只有G1 GC会有这种行为
-
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
6.6 堆空间分代思想
分代的唯一理由就是优化GC性能。
如果没有分代,GC的时候要找到哪些对象没用,就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
6.7 内存分配策略
针对不同年龄段的对象分配原则如下所示:
- 优先分配到Eden
- 大对象直接分配到老年代(尽量避免程序中出现过多的大对象)
- 长期存活的对象分配到老年代
- 动态对象年龄判断:如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到
MaxTenuringThreshold
中要求的年龄。 - 空间分配担保:
-XX:HandlePromotionFailure
6.8 为对象分配内存:TLAB
6.9 小结:堆空间的参数设置
// 详细的参数内容会在JVM下篇:性能监控与调优篇中进行详细介绍,这里先熟悉下
-XX:+PrintFlagsInitial //查看所有的参数的默认初始值
-XX:+PrintFlagsFinal //查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms //初始堆空间内存(默认为物理内存的1/64)
-Xmx //最大堆空间内存(默认为物理内存的1/4)
-Xmn //设置新生代的大小。(初始值及最大值)
-XX:NewRatio //配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio //设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold //设置新生代垃圾的最大年龄
-XX:+PrintGCDetails //输出详细的GC处理日志
//打印gc简要信息:①-Xx:+PrintGC ② - verbose:gc
-XX:HandlePromotionFalilure://是否设置空间分配担保
堆是分配对象的唯一选择么?
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。
但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。