【修炼内功】[JVM] 浅谈虚拟机内存模型

栏目: Java · 发布时间: 6年前

内容简介:不论做技术还是做业务,对于Java开发人员来讲,理解JVM各种原理的重要性不必再多言对于C/C++而言,可以轻易地操作任意地址的内存,而对于已申请内存数据的生命周期,又要担负起维护的责任。不知各位在初学C语言时,是否经历过由于内存泄漏导致系统内存不足,又或者因为误操作系统关键内存导致强制关机……

【修炼内功】[JVM] 浅谈虚拟机内存模型

不论做技术还是做业务,对于 Java 开发人员来讲,理解JVM各种原理的重要性不必再多言

对于C/C++而言,可以轻易地操作任意地址的内存,而对于已申请内存数据的生命周期,又要担负起维护的责任。不知各位在初学 C语言 时,是否经历过由于内存泄漏导致系统内存不足,又或者因为误操作系统关键内存导致强制关机……

对于Java使用者来说,内存由虚拟机直接管理, 不容易 出现内存泄漏或内存溢出等问题,将开发人员解放出来,使得更多的精力可以用于具体实现上。也正是因此,一旦出现内存泄漏或溢出问题,如果不了解JVM的内存管理原理,那么将会对问题的排查带来极大的困难。

JVM在执行Java程序的过程中,会将所管理的内存划分为不同的区域,这些区域各自都有自己的用途、可见性及生命周期,根据《Java虚拟机规范》的规定,JVM所管理的内存包含如下几个区域

【修炼内功】[JVM] 浅谈虚拟机内存模型

0x00 程序计数器

程序计数器是一个很小的内存区域,不在RAM上,而是直接划分在CPU上,用于JVM在解释执行字节码时,存储当前线程执行的字节码行号,每条线程都拥有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储

字节码解释器工作时,就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常等基础功能都需要依赖计数器来完成

如果线程正在执行的是一个Java方法,则程序计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是native方法,则计数器的值为空。此内存区是唯一一个在虚拟机规范中没有规定任何OutOfMemoryError的区域

0x01 堆

Java堆,是日常工作中最常接触的、也是虚拟机所管理的最大的一块内存区域,其被所有线程共享,在虚拟机启动时创建,此区域唯一的目的就是 存放对象实例

所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展及逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在对上也逐渐变得不是那么"绝对"了

从内存回收角度,Java堆分为新生代和老年代,新生代又分为E(den)空间和S(urvivor)0空间、S(urvivor)1空间

从内存分配角度,Java堆可能分为多个线程私有的分配缓冲区

如果存在实例未完成堆内存分配,且堆无法再扩展时(通过-Xmx及-Xms控制),将会抛出OutOfMemoryError异常

对于堆上各区域的分配、回收等细节,将在《[JVM] 虚拟机垃圾收集器》系列文章中详述

Java堆溢出

只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免GC回收,那么在对象数量达到堆的最大容量限制后就会产生内存溢出异常

/**
 * VM Args: -Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
 *
 * @author manerfan
 */
public class HeapOOM {
    static class OOMObject {
        private int i;
        private long l;
        private double d;
    }

    public static void main(String[] args) {
        List<OOMObject> list = new LinkedList<>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

指定堆大小固定为5MB且不能扩展,运行结果

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid71020.hprof ...
Heap dump file created [9186606 bytes in 0.069 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at HeapOOM.main(HeapOOM.java:19)

当Java堆内存溢出时,异常堆栈信息"java.lang.OutOfMemoryError"会跟着进一步提示"Java heap space"

对Dump出来的堆转储快照进行分析(如Eclipse Memory Analyzer),可以确认内存中的对象是否是必要的,可以清楚到底是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

观察堆使用情况,如下图

【修炼内功】[JVM] 浅谈虚拟机内存模型

0x02 虚拟机栈

虚拟机栈也是线程私有的,它的生命周期与线程相同,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,方法执行时栈帧入栈,方法结束时栈帧出栈

局部变量表存放编译器可知的各种基本数据类型、对象引用及returnAddress类型,局部变量表所需的内存空间在编译期间确定,运行期间不会再改变,具体的分析会在《[JVM] 虚拟机栈及字节码基础》中介绍

虚拟机栈规定了两种异常:如果线程请求的栈深度大于虚拟机允许的最大栈深度,则会抛出StackOverflow异常;如果虚拟机可以动态扩展栈深度,在扩展时无法申请足够内存,则会抛出OutOfMemoryError异常

Java栈溢出

StackOverflow

可以使用递归,无限增加栈的深度

/**
 * StackSOF
 *
 * @author Maner.Fan
 */
public class StackSOF {
    private int stackLen = 1;

    public void stackLeak() {
        stackLen++;
        stackLeak();
    }

    public static void main(String[] args) {
        StackSOF stackSOF = new StackSOF();
        try {
            stackSOF.stackLeak();
        } catch (Throwable e) {
            System.out.println("statck length: " + stackSOF.stackLen);
            throw e;
        }
    }
}

运行结果

statck length: 18455
Exception in thread "main" java.lang.StackOverflowError
    at StackSOF.stackLeak(StackSOF.java:13)
    at StackSOF.stackLeak(StackSOF.java:13)
    at StackSOF.stackLeak(StackSOF.java:13)
    at ...

OutOfMemoryError

对于栈空间的OutOfMemoryError,不论是减少最大堆容量、还是减少最大栈容量、还是增加局部变量大小、还是无限创建线程,都没有模拟出栈空间的OutOfMemoryError,倒是在堆空间比较小的时候会产生 java.lang.OutOfMemoryError: Java heap space 堆异常

环境

java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.212-b10, mixed mode)

macOS Mojave 10.14.4
2.2GHz Intel Core i7
16GB 1600 MHZ DDR3

思路

/**
 * VM Args: -Xms20M -Xmx20M -Xss512K
 *
 * @author Maner.Fan
 */
public class StackOOM {
    private void dontStop() {
        long l0 = 0L;
        long l1 = 1L;
        long l2 = 2L;
        long l3 = 3L;
        long l4 = 4L;
        long l5 = 5L;
        long l6 = 6L;
        long l7 = 7L;
        long l8 = 8L;
        long l9 = 9L;
        long l10 = 10L;
        long l11 = 11L;
        long l12 = 12L;
        long l13 = 13L;
        long l14 = 14L;
        long l15 = 15L;
        long l16 = 16L;
        long l17 = 17L;
        long l18 = 18L;
        long l19 = 19L;
        while(true) {}
    }

    public void stackLeak() {
        while (true) {
            new Thread(() -> dontStop()).start();
        }
    }

    public static void main(String[] args) {
        StackOOM stackOOM = new StackOOM();
        stackOOM.stackLeak();
    }
}

0x03 本地方法栈

本地方法栈与虚拟机栈的运行运行机制一致,用于存储每个Native方法的执行状态,唯一区别在于虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,很多虚拟机直接将本地方法栈与虚拟机栈合二为一

同虚拟机栈一样,本地方法栈也会抛出StackOverflow及OutOfMemoryError异常

0x04 方法区/元空间

Method Area

在Java7及其之前,虚拟机中存在一块内存区域叫方法区(Method Area),同样为线程共享,其主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,有时候会将该区域称之为永久代(Permanent Generation),但本质上两者并不等价

相对而言,GC行为在这个区域是比较少出现的,但并非数据进入了方法区就意味着"永久"存在,该区域的GC目标主要是针对常量池的回收及类型的卸载,但这个区域的回收成绩比较难以令人满意,尤其是对类型的卸载

当方法区无法满足内存分配需求时,将抛出OutofmemoryError异常

在Java7中,常量池已经从方法区移到了堆中,到了Java8及之后的版本,方法区已经被永久移除,取而代之的是元空间(Metaspace)

为什么要移除Method Area

This is part of the JRockit and Hotspot convergence effort. JRockit customers do.

一方面,移除方法区是为了和JRockit进行融合;另一方面,方法区大小受到 -XX: PermSize-XX: MaxPermSize 两个参数的限制,而这两个参数又受到JVM设定的内存大小限制,这就导致在使用过程中可能出现方法区内存溢出的问题

Metaspace

Metaspace并不在虚拟机内存中,而是使用本地内存,因此Metaspace具体大小理论上取决于系统的可用内存,同样也可以通过参数进行配置( -XX:MetaspaceSize -XX:MaxMetaspaceSize )

当然,Metaspace也是有OutOfMemoryError风险的,但是由于Metaspace使用本机内存,因此只要不要代码里面犯太低级的错误,OOM的概率基本是不存在的

Java元空间溢出

由于Java8之后,方法区被永久移除,这里我们不再测试方法区(永久代)的内存溢出

最简单的模拟Metaspace内存溢出,我们只需要无限生成类信息即可,类占据的空间总是会超过Metaspace指定的空间大小的,这里借助Cglib来模拟类的不断加载

/**
 * VM Args: -XX:MetaspaceSize=8M -XX:MaxMetaspaceSize=16M
 *
 * @author Maner.Fan
 */
public class MetaspaceOOM {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("MetaspaceOOM.java");
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(
                (MethodInterceptor)(obj, method, args1, methodProxy) -> methodProxy.invokeSuper(obj, args1)
            );
            enhancer.create();
        }
    }

    static class OOMObject {}
}

运行结果

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
    at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
    at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
    at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
    at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
    at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
    at MetaspaceOOM.main(MetaspaceOOM.java:19)

当Java元空间内存溢出时,异常堆栈信息"java.lang.OutOfMemoryError"会跟着进一步提示"Metaspace"

观察元空间使用情况,如下图

【修炼内功】[JVM] 浅谈虚拟机内存模型

0x05 直接内存

直接内存并不是虚拟机运行时数据区的一部分,最典型的示例便是NIO,其引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,使用Native函数库直接分配堆外内存,通过一个存储在队中的DirectByteBuffer对象作为这块内存的引用进行操作

直接内存的分配不会受到Java堆大小的限制,但会受到本机总内存大小及寻址空间的限制,一旦本机内存不足以分配堆外内存时,同样会抛出OutOfMemoryError异常

0x06 对象的访问定位

对象的创建是为了使用,Java程序执行时需要通过栈上的reference数据来找到堆上的具体对象数据进行操作,目前主流的访问方式有两种:句柄访问、直接指针访问

句柄访问

Java堆中将分配一块内存作为句柄池,栈中的reference存储对象实例句柄的地址

句柄包含两个指针,一个指针记录对象实例的内存地址,另一个记录对象类型数据的地址

使用句柄的方式访问对象数据,需要进行两次指针定位,但其优点在于,在GC过程中对象被移动时,只需要修改句柄中对象实例数据指针即可

【修炼内功】[JVM] 浅谈虚拟机内存模型

直接指针访问

栈中reference直接存储堆中对象实例数据的内存地址,而对象类型数据的地址存放在对象实例数据中

使用直接指针访问的好处在于访问速度快,其只需要一次指针定位,但在GC过程中对象被移动时,需要将所有指向该对象实例的reference值修改为移动后的内存地址

【修炼内功】[JVM] 浅谈虚拟机内存模型


以上所述就是小编给大家介绍的《【修炼内功】[JVM] 浅谈虚拟机内存模型》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms and Theory of Computation Handbook

Algorithms and Theory of Computation Handbook

Mikhail J. Atallah (Editor) / CRC-Press / 1998-09-30 / USD 94.95

Book Description This comprehensive compendium of algorithms and data structures covers many theoretical issues from a practical perspective. Chapters include information on finite precision issues......一起来看看 《Algorithms and Theory of Computation Handbook》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具