资讯专栏INFORMATION COLUMN

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

sanyang / 2536人阅读

摘要:也正是因此,一旦出现内存泄漏或溢出问题,如果不了解的内存管理原理,那么将会对问题的排查带来极大的困难。

本文已收录【修炼内功】跃迁之路

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

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

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

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

0x00 程序计数器

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

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

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

0x01 堆

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

《深入理解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 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)

观察堆使用情况,如下图

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"

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

0x05 直接内存

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

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

0x06 对象的访问定位

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

句柄访问

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

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

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

直接指针访问

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

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

参考:
深入理解Java虚拟机

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74545.html

相关文章

  • 修炼内功】[JVM] 虚拟栈及字节码基础

    摘要:本文已收录修炼内功跃迁之路在浅谈虚拟机内存模型一文中有简单介绍过,虚拟机栈是线程私有的,每个方法在执行的同时都会创建一个栈帧,方法执行时栈帧入栈,方法结束时栈帧出栈,虚拟机中栈帧的入栈顺序就是方法的调用顺序写了很多文字,但都不尽如意,十分惭 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbtSi5?w=1654&h=96...

    VEIGHTZ 评论0 收藏0
  • 修炼内功】[JVM] 深入理解JVM之ClassLoader

    摘要:本文已收录修炼内功跃迁之路在诞生之初便提出,各提供商发布很多不同平台的虚拟机,这些虚拟机都可以载入并执行同平台无关的字节码。设计者在第一版虚拟机规范中便承诺,时至今日,商业机构和开源机构已在之外发展出一大批可以在上运行的语言,如等。 本文已收录【修炼内功】跃迁之路 Java在诞生之初便提出 Write Once, Run Anywhere,各提供商发布很多不同平台的虚拟机,这些虚拟机...

    荆兆峰 评论0 收藏0
  • 修炼内功】[JVM] 类文件结构

    摘要:本文已收录修炼内功跃迁之路学习语言的时候,需要在不同的目标操作系统上或者使用交叉编译环境,使用正确的指令集编译成对应操作系统可运行的执行文件,才可以在相应的系统上运行,如果使用操作系统差异性的库或者接口,还需要针对不同的系统做不同的处理宏的 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbtpPd?w=2065&h=11...

    Eminjannn 评论0 收藏0
  • 修炼内功】[JVM] 虚拟视角的方法调用

    摘要:本文已收录修炼内功跃迁之路我们写的方法在被编译为文件后是如何被虚拟机执行的对于重写或者重载的方法,是在编译阶段就确定具体方法的么如果不是,虚拟机在运行时又是如何确定具体方法的方法调用不等于方法执行,一切方法调用在文件中都只是常量池中的符号引 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbuesq?w=2114&h=12...

    shevy 评论0 收藏0
  • 修炼内功】[Java8] Lambda究竟是不是匿名类的语法糖

    摘要:本文已收录修炼内功跃迁之路初次接触的时候感觉表达式很神奇表达式带来的编程新思路,但又总感觉它就是匿名类或者内部类的语法糖而已,只是语法上更为简洁罢了,如同以下的代码匿名类内部类编译后会产生三个文件虽然从使用效果来看,与匿名类或者内部类有相 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbui4o?w=800&h=600)...

    ?xiaoxiao, 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<