havareadimportantjavaJava深入学习

深入理解 Java 虚拟机(二)Java内存区域与内存溢出异常

2017-02-02  本文已影响1316人  SawyerZh
Java内存区域与内存溢出异常

1.概述


2.运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁时间。根据 《Java 虚拟机规范》的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域。

运行时数据区

2.1 - 程序计数器(Program Counter Register)

2.2 - Java 虚拟机栈(Java Virtual Machine Stacks)

2.2.1 - Java 虚拟机栈
2.2.2 - 局部变量表

我们经常将 Java 内存分为堆内存(Heap)和栈内存(Stack),这种分法中所指的栈就是 Java 虚拟机栈,或者说是虚拟机栈中 局部变量表 部分。

2.2.3 - 对象引用
2.2.4 - 异常
异常类型 发生条件
StackOverflowError 线程请求的栈深度大于虚拟机所允许的深度时抛出该异常。
OutOfMemoryError 无法申请到足够的内存时抛出该异常。

2.3 - 本地方法栈(Native Method Stack)

在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(e.g. Sun HotSpot VM)直接将本地方法栈与虚拟机栈合二为一。

2.4 - Java 堆(Java Heap)

异常类型 发生条件
StackOverflowError
OutOfMemoryError 在堆中没有内存来完成实例分配,且堆无法再扩展时,抛出该异常。

Reminde 🤗
随着 JIT 编译器的发展与逃逸分析技术成熟,栈上分配标量替换 等优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也变得不那么绝对了。

2.5 - 方法区(Method Area)

异常类型 发生条件
StackOverflowError
OutOfMemoryError 当方法区无法满足内存分配需求时,抛出该异常。

相对而言,垃圾收集行为在这个区域是比较少出现的,这个区域的内存回收目标主要是针对 常量池的回收类型的卸载

2.6 - 运行时常量池(Runtime Constant Pool)

异常类型 发生条件
StackOverflowError
OutOfMemoryError 因为是方法区的一部分,所以受到方法区内存的限制,当常量池无法再申请到内存时抛出该异常。

Java 虚拟机对 Class 文件的每一部分(包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据类型必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现虚拟机可以按照自己的需求来实现这个内存区域。一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在该区域中。

2.7 - 直接内存(Direct Memory)

异常类型 发生条件
StackOverflowError
OutOfMemoryError 受到物理内存限制,动态扩展时无法申请到内存时抛出该异常。

显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机内存(包括 RAM 以及 SWAP 区或者分页大小)大小以及处理器寻址空间的限制,当各个内存区域总和大于物理内存限制(包括物理和操作系统级的限制)时会出现异常。


3.HotSpot 虚拟机对象探秘

这一部分内容将以 HotSpot 虚拟机和常用的内存区域 Java 堆为例,阐述对象分配、布局和访问的全过程。

3.1 - 对象的创建

3.1.1 - 类加载
3.1.2 - 分配内存

选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用 Serial、ParNew 等带 Compact 过程的收集器时,系统采用的分配算法是指针碰撞,而使用 CMS 这种基于 Mark-Sweep 算法的收集器时,通常采用空闲列表。

3.1.3 - 同步控制
3.1.4 - 初始化
3.1.5 - 对象头(Object Header)
3.1.6 - init

3.2 - 对象的内存布局

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

3.2.1 - 对象头(Header)

HotSpot 虚拟机的对象头包括两部分信息,存储自身的运行时数据的(Mark Word)类型指针。

第一部分:Mark Word
存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空(不需要记录信息) 11 GC 标记
偏向线程 ID、偏向时间戳、对象分代年龄 01 可偏向
第二部分:类型指针

Reminder 👉
并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身。

3.2.2 - 实例数据(Instance Data)
3.2.3 - 对齐填充(Padding)

3.3 - 对象的访问定位

Sun HotSpot 使用的是第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。


4.实战:OutOfMemoryError 异常

在 Java 虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemory(OOM)异常的可能。

VM Args 设置

4.1 - Java 堆溢出

-verbose:gc
-Xms20M // 最小 GC 启动
-Xmx20M // 最大 GC 启动
-XX:+PrintGCDetails // 打印设置
-XX:SurvivorRatio=8 // 存活对象比率
private static class OOMObject {
}
public static void main(String[] args) {
    List<OOMObject> list = new ArrayList<>();
    while (true) {
        list.add(new OOMObject());
    }
} 
java.lang.OutOfMemoryError: Java heap space

4.2 - 虚拟机栈和本地方法栈溢出

异常类型 发生条件
StackOverflowError 线程请求的栈深度大于虚拟机所允许的深度时抛出该异常。
OutOfMemoryError 无法申请到足够的内存时抛出该异常。

这里把异常分为两种情况,看似更加严谨,但却存在一些相互重叠的地方:方栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事的两种描述而已。

4.2.1 - StackOverflowError
VM Args: -Xss128k   // 栈内存容量
// 记录堆栈深度
private int stackLength = 1;
public void stackLeak() {
    stackLength++;
    stackLeak();
}
public static void main(String[] args) throws Throwable  {
    JavaVMStackSOF oom = new JavaVMStackSOF();
    try {
        oom.stackLeak();
    } catch (Throwable e) {
        System.out.println("Stack length: " + oom.stackLength);
        throw e;
    }
} 
Stack length: 718
Exception in thread "main" java.lang.StackOverflowError
4.2.2 - OutOfMemoryError
VM Args: -Xss2M // 栈内存容量
private void neverStop() {
    while (true) {
    }
}
// 循环开启线程
public void stackLeakByThread() {
    while (true) {
        new Thread(this::neverStop).start();
    }
}
public static void main(String[] args) {
    JavaVMStackOOM oom = new JavaVMStackOOM();
    oom.stackLeakByThread();
}
Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread

4.3 - 方法区和运行时常量池溢出

4.3.1 - OutOfMemoryError
-XX:PermSize=10M    // 方法区最小值
-XX:MaxPermSize=10M // 方法区最大值
public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    long i = 0;
    while (true) {
        list.add(String.valueOf(i++).intern());
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: PermGen spacee
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)
4.3.2 - String 常量池测试

使用 JDK1.7 运行这段程序就不会得到相同的结果,while 循环将一直进行下去。关于这个字符串常量池的实现问题,还可以引申出一个更有意思的影响。

String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
4.3.3 - 测试设计思路
4.3.4 - 总结

4.4 - 本机直接内存溢出

private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
    Field unsafeField = Unsafe.class.getDeclaredFields()[0];
    unsafeField.setAccessible(true);
    Unsafe unsafe = (Unsafe) unsafeField.get(null);
    while (true) {
        unsafe.allocateMemory(_1MB);
    }
} 
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)

悄悄话 🌈


彩蛋 🐣


如果你觉得我的分享对你有帮助的话,请在下面👇随手点个喜欢 💖,你的肯定才是我最大的动力,感谢。

上一篇下一篇

猜你喜欢

热点阅读