JVM 内存模型

960 阅读7分钟

概述

本文主要讲述 JVM 内存模型, 解析各个运行时内存数据区域的作用和使用场景。

本文所提到的 JVM 模型都是基于 jdk-1.8 版本

JVM 内存模型

JVM 内存模型

运行时数据区域

程序计数器(Program Conunter Regisiter)

程序计数器是一个比较小的内存空间,可以看作是当前线程执行的字节码行号指示器。本质就是记录字节码执行顺序。 在《Java 虚拟机规范》中没有任何 OutOfMemoryError 情况的区域。

虚拟机栈(JVM Stack)

虚拟机栈,存放的是线程运行时内部的局部变量,也可以理解为线程栈。

每个方法被执行的时候, 虚拟机会创建一个栈帧(Stack Frame)用于存放局部变量表(local variable),操作数栈(operand stack),动态连接,方法出口等信息。

栈帧(Stack Frame)随着方法的调用而创建,随着方法的结束而销毁(不论是正常结束还抛出异常)。

字节码指令分析(描述 JVM Stack 操作过程)

public int add() {
  int a = 1;
  int b = 2;
  int c = b - a;
  return c;
}

0 iconst_1 //将 a 压入局部变量表栈顶
1 istore_1 //对 a 进行赋值 1  
2 iconst_2 //将 b 压入局部变量表栈顶
3 istore_2 //对 b 进行赋值 2
4 iload_2  //读取 b 到操作数栈
5 iload_1  //读取 a 到操作数栈
6 isub     //执行 b - a
7 istore_3 //将 int 类型的值存入局部变量表 3 
8 iload_3  //读取 c 到操作数栈
9 ireturn  //返回

局部变量表(local variable)

局部变量表存放了各种编译期 Java 虚拟机基本数据库类型(boolean、byte、char、short、int、float、long、dubble)和对象引用(reference 类型,即对象的起始位置指针或者对象句柄), 对象的真实数据通常存放在堆空间。

局部变量表中的存储空间通过变量槽(slot) 来表示,其中 64 位长度的 long 和 double 占 2 个变量槽。

操作数栈(operand stack)

每个栈帧都包含一个操作数栈的先进先出(FIFO)栈,栈帧中操作数栈的深度由编译期决定,并且通过方法的 code 属性保存以及提供给栈帧使用。

动态链接

每个栈帧都包含一个指向当前方法所在类型的运行时常量池的引用。以便对当前方法的代码实现动态链接

在 class 文件中,一个方法如果要调用其它方法, 或者访问局部成员变量,则需要将符号引用(synbolic reference)来表示,动态链接的作用就是将这些符号引用转换为对实际方法的直接引用

方法出口

方法正常完成,当前栈帧恢复调用者的责任,包括恢复调用者的局部变量表,操作数栈,以及正确的程序计数器递增。跳过刚才执行的方法调用指令等,低哦啊用着的代码被调用的方法正返回值压入调用者操作数栈后,会继续正常执行。

方法一场完成,某些指令导致了 JVM 虚拟机抛出异常,或者用户显示的通过 thorw 关键字跑出一场,同时在改该方法中没有捕获异常。如果方法异常调用完成,那不一定有方法返回值返回给调用者。

本地方法栈(Native Method Stack)

为本地方法所分配的内存空间,就是为 native 关键字修饰的方法提供服务的。

本地方法主要是 Java 来调用 C/C++ 函数库的调用方法。

方法区(Method Area)

主要存放数据有:常量,静态变量,类信息。

方法区存放的是静态变量的内存地址, 方法区里面有一个元空间, 在JDK1.8 之前叫永久代。

堆(Heap)

JVM 管理的最大的一块内存空间。与堆相关的一个重要概念是垃圾收集器。几乎所有的垃圾收集器都是采用分代收集算法,所以对内存空间也是基于这一点进行相应的划分:新生代和老年代,新生代分为Eden 空间、From Survivor 空间、To Survivor 空间。

堆空间的分布

对象创建的过程中首先会存在 Eden 区,然后经过 minor gc 过后进入 survivor ,进过 15 次 survivor 转移过后,进入老年代。

如果内存都不够用了就触发 full gc, 再次触发 GC 过后无法分配申请内存,JVM 就会抛出 OOM。

分析对象是否存在引用,是否被回收采用的是 GC ROOT 可达性分析。

直接内存(Direact Memory)

直接内存,不是 JVM 来管理,是通过操作系统来管理的, 与 Java NIO 密切相关。 Java 通过DirectByteBuffer来操作直接内存。

JVM 内存参数设置

内存参数配置

内存参数配置

Spring-Boot 程序的 JVM 内存参数设置格式(Tomcat 启动直接在 bin 目录下的 Catalina.sh 文件设置)

java -Xms2048m -Xmx2048m  -Xmn1024 -Xss512k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -jar  xxx-xxx.jar

关于元空间JVM 有两个:-XX:MetaspaceSize=N 和 -XX:MaxMetaspaceSize=N,对于 64 位 JVM 来说, 元空间默认是 21MB,默认的元空间的最大值是无限

-XX:MaxMetaspaceSize: 设置元空间最大值,默认是 -1, 即不限制,或者说是受限制于本地内存大小。

-XX:MetaspaceSize:指定元空间的初始大小,以字节为单位,默认是 21M,达到该值过后就会触发 full gc 进行类型卸载,同时收集器会对该值进行调整;如果释放了大量的空间就适当降低该值;如果释放了很少的空间,那么就在不超过 -XX:MaxMetaspaceSize (如果设置)的情况下,适当提高该值。

由于调整元空间大小需要 full gc , 这是一个非常昂贵的操作,如果在启动过程中发生大量 full gc, 通常都是由于永久代或者元空间发生了大小调整,基于这种情况,一般建议在 JVM 参数将 MaxMetaspaceSize 和 MetaspaceSize 设置成一样的值,并设置得比初始值要大,对于 8G 的物理内存来说我们通常都会将这两个值设置为 256M。

堆空间内存溢出

import java.util.ArrayList;
import java.util.List;

public class HeapOverFlowTest {

    byte[] a = new byte[1024 * 1024 * 2]; // 2mb

    public static void main(String[] args) {
        List<HeapOverFlowTest> list = new ArrayList<>();
        while(true) {
            list.add(new HeapOverFlowTest());
        }
    }
}
// 输出结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at cn.edu.cqvie.jvm.HeapOverFlowTest.<init>(HeapOverFlowTest.java:8)
	at cn.edu.cqvie.jvm.HeapOverFlowTest.main(HeapOverFlowTest.java:13)

虚拟机栈内存溢出

public class StackOverFlowTest {

    // JVM 设置
    // -Xss128k, -Xss默认1M
    static int count = 0;

    static void redo() {
        count++;
        redo();
    }

    public static void main(String[] args) {
        try {
            redo();
            System.out.println(count);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

// 输出结果: 栈溢出
java.lang.StackOverflowError
	at cn.edu.cqvie.jvm.StackOverFlowTest.redo(StackOverFlowTest.java:11)
	at cn.edu.cqvie.jvm.StackOverFlowTest.redo(StackOverFlowTest.java:11)
	at cn.edu.cqvie.jvm.StackOverFlowTest.redo(StackOverFlowTest.java:11)
....

总结:

-Xss 设置越小 count 值越小,说明一个线程栈里能够分配的栈帧就越小,但是对于 JVM 整体来说能够开启的线程数就会更多。

方法区内存溢出

  1. 需要注意的是 1.8 内模型中,将运行时常量池数据放入堆中,所以我们限制方法区的大小对运行时常量池的限制毫无意义。最终也只会抛出 java.lang.OutOfMemoryError: Java heap space 异常。
  2. 下面通过GCLib 模拟方法区溢出模拟的一个例子。
/**
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 */
public class MyTest4 {

    public static void main(String[] args) {
        for (; ; ) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MyTest4.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) ->
                    proxy.invoke(obj, args1));

            System.out.println("hello world");
            enhancer.create();
        }
    }
}

//输出结果
Caused by: java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
  ......

JVM 监控工具

VisualVM

VisualVM 提供在 Java 虚拟机 (Java Virutal Machine, JVM) 上运行的 Java 应用程序的详细信息。在 VisualVM 的图形用户界面中,可以方便、快捷地查看多个 Java 应用程序的相关信息。

JVM 内存监控

参考资料

  1. 《深入理解 Java 虚拟机》 第三版 周志明
  2. 《Java 虚拟机规范(Java SE 8 版)》 爱飞翔 周志明 等译
  3. visualvm 主页文档
  4. Oracle 官网 Java 虚拟机规范