JVM内存结构笔记(中)

文章目录


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,否则会爆出以下错误:
js 复制代码
MaxTenuringThreshold of 20 is invalid; must be between 0 and 15

为什么JVM新生代对象年龄只能是 0-15?

在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:

  1. 对象头(Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(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

当没有使用对象时进行回收对象,当生成大量的对象同时使用时则不能回收,当对象过多时就会导致堆内存耗尽溢出。

java 复制代码
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+"次");
    }
}

报错:

js 复制代码
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

报错

js 复制代码
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.堆内存诊断

  1. jps 工具 + jmap 工具
    jsp:查看当前系统中有哪些 java 进程
    jmap:查看堆内存占用情况 jmap -heap 进程id
  2. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测

代码示例

java 复制代码
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


相关推荐
yyueshen6 分钟前
volatile 在 JVM 层面的实现机制
java·jvm
mercyT13 分钟前
Kotlin学习笔记之类与对象
笔记·学习·kotlin
啥都想学的又啥都不会的研究生30 分钟前
Redis设计与实现-服务器中的数据库
运维·服务器·数据库·redis·笔记·缓存·性能优化
HBryce241 小时前
垃圾收集算法与收集器
jvm
瑶光守护者2 小时前
并行计算编程模型的发展方向与RISC-V的机遇
人工智能·笔记·学习·架构·risc-v
半夏知半秋2 小时前
linux下的网络抓包(tcpdump)介绍
linux·运维·服务器·网络·笔记·学习·tcpdump
yyueshen3 小时前
JVM中是如何定位一个对象的
java·jvm
异常驯兽师3 小时前
《Java三剑客:JDK、JRE、JVM的“塑料友情”》
java·开发语言·jvm
诗句藏于尽头5 小时前
mac部署GPT-SoVITS,生成粤语踩坑点及使用记录
笔记·gpt·macos