堆
1.定义
堆是Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代
;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
2.堆的结构
堆内存被通常分为下面三部分:
1.新生代(Young Generation)
- 作用:存放新创建的对象。
- 特点:
- 大多数对象在年轻代创建,并且很快变得不可达(生命周期短)。
- 年轻代的垃圾回收称为 Minor GC,回收频率较高。
- 分区:
- Eden 区:新创建的对象首先分配在 Eden 区。
- Survivor 区:分为两个区域(From 和 To),用于存放经过 Minor GC 后仍然存活的对象。
2.老生代(Old Generation)
- 作用:存放生命周期较长的对象。
- 特点:
- 对象在年轻代经过多次 Minor GC 后仍然存活,会被晋升到老年代。
- 老年代的垃圾回收称为 Major GC 或 Full GC,回收频率较低,但耗时较长。
- 触发条件:
- 对象年龄达到一定阈值(通过 -XX:MaxTenuringThreshold 设置)。
- Survivor 区空间不足。
3.永久代(Permanent Generation)元空间(MetaSpace)
- 作用:存放类的元数据(如类信息、方法信息、常量池等)。
- 特点:
- 在 JDK 8 之前,这部分称为 永久代(PermGen),位于堆中。
- JDK 8 及以后:被 元空间(MetaSpace) 取代,元空间使用本地内存而非堆内存。
- 相关参数:
- -XX:MetaspaceSize:初始元空间大小。
- -XX:MaxMetaspaceSize:最大元空间大小。
下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。
- 大部分情况,对象都会首先在 Eden 区域分配。
- 在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。
- 当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
- 对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
- 不过,设置的值应该在 0-15,否则会爆出以下错误:
MaxTenuringThreshold of 20 is invalid; must be between 0 and 15
为什么JVM新生代对象年龄只能是 0-15?
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
其中,对象头包括两部分:标记字段(Mark Word)和类型指针(Klass Word)。关于对象内存布局的详细介绍,后文会介绍到,这里就不重复提了。
这个年龄信息就是在标记字段中存放的(标记字段还存放了对象自身的其他信息比如哈希码、锁状态信息等等)。
而JVM内部使用4位(bit)来表示对象的年龄,因此可以表示的最大数值为2^4−1=15。这就是为什么对象年龄的上限被固定为15的原因。
3.堆内存溢出
堆最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:
1.java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
2.java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值,详见:Default Java 8 max heap size
当没有使用对象时进行回收对象,当生成大量的对象同时使用时则不能回收,当对象过多时就会导致堆内存耗尽溢出。
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println("循环了:"+i+"次");
}
}
报错:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at cn.itcast.jvm.t1.heap.Demo1_5.main(Demo1_5.java:19)
循环了:24次
修改堆内存大小:-Xmx8m
报错
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at cn.itcast.jvm.t1.heap.Demo1_5.main(Demo1_5.java:19)
循环了:17次
4.堆内存诊断
- jps 工具 + jmap 工具
jsp:查看当前系统中有哪些 java 进程
jmap:查看堆内存占用情况 jmap -heap 进程id - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
代码示例
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
System.out.println("2...");
Thread.sleep(20000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
启动项目,在1…打断点
先使用jps
使用jmap -heap 43020
观察其中Eden 区、From 和 To区、Old Generation区
在2…打断点,使用jmap -heap 43020
在3…打断点,使用jmap -heap 43020