JVM内存模型

809 阅读9分钟

java运行时数据区域

java虚拟机所管理的内存包含以下几个运行时数据区域,如图:

  • 程序计数器
  • java虚拟机栈
  • 本地方法栈
  • java堆
  • 方法区
    运行时数据区

1. 程序计数器

程序计数器是一块比较小的内存空间,它是当前线程所执行的字节码的行号指示器。

1.1 程序计数器的作用

由于java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,这类内存区域为“线程私有”的内存。

1.2 程序计数器的特点

  • 是一块较小的内存空间。

  • 线程私有,每条线程都有自己的程序计数器。

  • 生命周期:随着线程的创建而创建,随着线程的结束而销毁。

  • 是唯一一个不会出现OutOfMemoryError的内存区域。


2 java虚拟机栈

2.1 java虚拟机栈定义

java虚拟机栈也是线程私有的,他的生命周期与线程想同。虚拟机栈描述的是java方法执行的线程内存模型。

每个方法被执行的时候,java虚拟机都会同步创建一个战争,用于存储在该方法运行过程中的一些信息:

  • 局部变量表
    • java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double);
    • 对象引用 ;
    • returnAddress ;
  • 操作数栈
  • 动态连接

    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

  • 方法出口

    当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且 这个异常没有在方法体内得到处理。

  • .....

每个方法被调用知道完毕的过程都对应着一个栈帧在虚拟机中从入栈到出栈的过程。

2.2 java虚拟机栈的特点

  • 局部变量表所需的内存空间在编译期间完成分配,进入一个方法时,这个方法需要在栈帧中分配的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  • Java虚拟机栈会出现两种异常:StackOverflowErrorOutOfMemoryError
    • 如果线程请求的站深度大于虚拟机所允许的深度,将抛出StackOverflowError;
    • 如果java虚拟机站容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。

3. 本地方法栈

本地方法栈与虚拟机所发挥的作用很相似,其区别在于虚拟机栈为虚拟机执行java方法服务,而本地方法栈则是为虚拟机使用到本地方法服务。

3.1 本地方法栈的特点

  • Java虚拟机栈会出现两种异常:StackOverflowErrorOutOfMemoryError
    • 如果线程请求的站深度大于虚拟机所允许的深度,将抛出StackOverflowError;
    • 如果java虚拟机站容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。

4. java堆

4.1 java堆定义

java堆是虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,java中“几乎”所有的对象实例都在这里分配内存。这里使用“几乎”是因为java语言的发展,及时编译的技术发展,逃逸分析技术的日渐强大,栈上分配、标量替换等优化手段,使java对象实例都分配在堆上变得不那么绝对。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法(G1之后开始变得不一样,引入了region,但是依旧采用了分代思想),Java堆中还可以细分为:新生代和老年代。再细致一点的有Eden空间、From Survivor空间、ToSurvivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

4.2 堆区的调整

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以在运行时动态地调整。

调整参数

通过设置如下参数,可以设定堆区的初始值和最大值,比如:

-Xms256M -Xmx 1024M

其中 -X这个字母代表它是JVM运行时参数,ms是memory start的简称,中文意思就是内存初始值,mx 是 memory max的简称,意思就是最大内存。

PS:在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,会形成不必要的系统压力 所以在线上生产环境中 JVM的Xms和 Xmx会设置成同样大小,避免在GC 后调整堆大小时带来的额外压力。

4.3 OOM异常

堆的大小既可以固定也可以扩展,但对于主流的虚拟机,堆的大小是可扩展的,因此当线程请求分配内存,但堆已满,且内存已无法再扩展时,就抛出 OutOfMemoryError 异常。

/**
 * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOMTest {

    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        List<Integer[]> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Integer[] ints = new Integer[2 * _1MB];
            list.add(ints);
        }
    }
}

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid32372.hprof ...
Heap dump file created [7774077 bytes in 0.009 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at jvm.HeapOOMTest.main(HeapOOMTest.java:18)

5. 方法区

方法区是java堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

在 HotSpot JVM 中,永久代(永久代实现方法区)中用于存放类和方法的元数据以及常量池,比如Class和Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。
永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出, java.lang.OutOfMemoryError: PermGen,为此我们不得不对虚拟机做调优。
后来HotSpot放弃永久代(PermGen),jdk1.7版本中,HotSpot已经把原本放在永久代的字符串常量池、静态变量等移出,到了jdk1.8,完全废弃了永久代,方法区移至元空间(Metaspace)。比如类元信息、字段、静态属性、方法、常量等都移动到元空间区。 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

5.1 对应的JVM调参:

参数 作用
-XX:MetaspaceSize 分配给Metaspace(以字节计)的初始大小。
如果不设置的话,默认是20.79M,
这个初始大小是触发首次 Metaspace Full GC 的阈值,例如 -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize 分配给Metaspace 的最大值,超过此值就会触发Full GC,
此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
但是线上环境建议设置,例如-XX:MaxMetaspaceSize=256M
-XX:MinMetaspaceFreeRatio 最小空闲比,当 Metaspace 发生 GC 后,会计算 Metaspace 的空闲比,
如果空闲比(空闲空间/当前 Metaspace 大小)小于此值,
就会触发 Metaspace 扩容。默认值是 40 ,也就是 40%,
例如 -XX:MinMetaspaceFreeRatio=40
-XX:MaxMetaspaceFreeRatio 最大空闲比,当 Metaspace 发生 GC 后,会计算 Metaspace 的空闲比,
如果空闲比(空闲空间/当前 Metaspace 大小)大于此值,
就会触发 Metaspace 释放空间。默认值是 70 ,也就是 70%,
例如 -XX:MaxMetaspaceFreeRatio=70

建议将 MetaspaceSize 和 MaxMetaspaceSize 设置为同样大小,避免频繁扩容。

5.2 OOM异常

如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。

5.3 运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期间生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

方法区中存放:类信息、常量、静态变量、即时编译器编译后的代码。常量就存放在运行时常量池中。 当类被 Java 虚拟机加载后, .class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如 String 类的 intern() 方法就能在运行期间向常量池中添加字符串常量。


6. 直接内存

直接内存并不是虚拟机运行时数据区的一部分。 在 NIO 中引入了一种基于通道和缓冲的 IO 方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在堆中的DirectByteBuffer对象直接操作该内存,而无须先将外部内存中的数据复制到堆中再进行操作,从而提高了数据操作的效率。
直接内存的大小不受 Java 虚拟机控制,但既然是内存,当内存不足时就会抛出 OutOfMemoryError 异常。

6.1 直接内存与堆内存比较

  • 直接内存申请空间耗费更高的性能;
  • 直接内存读取 IO 的性能要优于普通的堆内存。
  • 直接内存作用链: 本地 IO -> 直接内存 -> 本地 IO
  • 堆内存作用链:本地 IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地 IO

服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

参考