JVM内存数据模型

图片来自pixabay.com的Gipfelsturm69-2191891会员

本文将对JVM内存数据模型进行介绍,并给出一个简单的Java应用程序,描述其内存分配过程。在编写代码中,只有对类、各个变量和Java对象做到心中有数,才能“下笔”(敲代码)如有神。

1. JVM内存数据模型

如下图所示,

JVM内存数据模型

根据JVM规范,在运行时刻JVM内存数据分为如下6种,

  1. PC Register 程序计数器: 一个JVM中支持多个线程的执行,每个线程拥有各自独立的程序计数器,程序计数器指向线程执行的当前方法地址。
  2. JVM Stacks 栈区:每个线程拥有各自独立的JVM栈,一个栈存储着frames列表,每个frame对应着一个方法调用,其保存着方法调用所使用的本地变量和Java对象的引用,方法返回的值和异常。Frame按照后入先出的原则,执行并返回调用结果。这个数据区会发生如下两种内存溢出错误,
    • StackOverflowError 栈超过允许的调用深度
    • OutofMemoryError 栈超过允许的可用内存大小
  3. Heap 堆区,这个区的数据被所有线程所共享,是类对象创建时分配内存的地方。这个区的内存被JVM管理,实现对象的自动回收,也就是GC。这个数据区发生如下的内存溢出错误,
    • OutofMemoryError创建的对象超过可分配的内存大小
  4. Method Area方法区:这个区的数据被所有线程所共享,里面加载着类的定义,包括常量池,变量和方法数据等。这个数据区发生如下的内存溢出错误,
    • OutofMemoryError加载的类超过可分配的内存大小
  5. Run-Time Contant Pool 常量池,一个类文件中所定义的常量,一般会存储在方法区中。
  6. Native Method Stacks原生方法栈,Java内核代码中含有很多对操作系统原生方法的调用,这里存储着对原生方法调用的信息。其只对Java内核代码有意义,对于Java程序员来说,可以忽略这个区。

2. 一个简单应用程序的JVM内存数据

下面以一个简单的Java程序,描述下JVM的内存分配过程。

public class Demo {

    private static String CONSTANT = "hello,world";

    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.print(CONSTANT);

        int i = 0;
        String s = String.valueOf(i);
        demo.print(s);
    }

    private String print(String s) {
        System.out.println(s);
        return s;
    }

}

上述程序定义了一个Demo的类,里面包含了一个main主程序,和一个print()方法调用。

整个程序在运行过程中,JVM将会执行如下动作,

  1. JVM根据启动参数,初始化各个内存区。
  2. JVM加载Demo类到方法区,加载各个变量和方法定义,加载常量定义,其中字符串常量从堆区分配。
  3. 启动一个main线程,执行main主程序,线程的执行进度记录在程序计数器中。同时,在栈区初始化当前线程的方法调用栈。
  4. 进入main()方法调用,创建frame1,初始化如下变量
    • String args 输入参数,引用指向堆区所创建的args对象
    • Demo demo 引用,指向堆区所创建的demo对象
    • int i = 0 分配一个整型i,初始化值为0
    • String s 引用,指向堆区所创建的s对象
  5. 进入print()方法调用,创建frame2
    • String s 输入参数,引用指向堆区已创建的s对象
    • 返回s,指向堆区的s对象。
  6. print()方法调用结束,栈回到frame1。
  7. main()方法调用结束,方法调用栈清空。
  8. main线程执行结束。

上述在内存的分配可以描述为下图,

一个简单的Java程序内存分配.PNG

3. 堆区和元空间

JVM Heap是最大的内存分配区域,所有的Java对象都从这里获得内存存储空间,这里也是JVM自动内存回收(GC)的地方。

要想了解GC的工作机制,首先需要了解堆区中Java对象按代进行存储的机制。整个堆区分为如下几个区域,

  1. Eden 伊甸园区:这里是创建对象时最先分配内存的地方,名副其实的创世区
  2. Survivor 存活区:在发生Young GC时,会将Eden区中大量不再使用的对象删除,留下来的放入Survivor区。注意的是,Survivor区一般有两个,每次YGC时,将会S0和S1交换着来保存存活下来的对象。也就是说,S0和S1总有一个是处于清空状态。
  3. Tenured年老代:在GC多次过后,有些对象存活时间比较长,将会移入到年老代。

至于对象的存活与否,如何回收,这个将涉及到对象引用计数的概念,以及各个GC算法实现,这里不再扩展。

下图描述了堆区的示意图,

JVM堆区

图中还有一个元空间(MetaSpace),在JDK7之前其是一个永久代(PermGen)的内存空间,里面存放类定义等数据。在JDK8之后,永久代被元空间取代,两者的区别之一在于空间地址,永久代位于JVM Heap Memory中,而元空间移到了native memory中,这里的native memory是相对于JVM里面的heap memory而言,是位于JVM所运行的内存空间。

一个查看Java进程的堆区内存使用情况,命令如下(请使用JDK8的jmap工具),

$ jmap -heap 14120
Attaching to process ID 14120, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.111-b14

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4208984064 (4014.0MB)
   NewSize                  = 88080384 (84.0MB)
   MaxNewSize               = 1402994688 (1338.0MB)
   OldSize                  = 176160768 (168.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 31981568 (30.5MB)
   used     = 21396720 (20.405502319335938MB)
   free     = 10584848 (10.094497680664062MB)
   66.90328629290471% used
From Space:
   capacity = 1048576 (1.0MB)
   used     = 524288 (0.5MB)
   free     = 524288 (0.5MB)
   50.0% used
To Space:
   capacity = 1048576 (1.0MB)
   used     = 0 (0.0MB)
   free     = 1048576 (1.0MB)
   0.0% used
PS Old Generation
   capacity = 176160768 (168.0MB)
   used     = 68010784 (64.86013793945312MB)
   free     = 108149984 (103.13986206054688MB)
   38.60722496396019% used

6179 interned Strings occupying 524264 bytes.

4. Java主要启动参数

在了解了JVM内存数据模型之后,下面就可以看看Java 的各种启动参数配置,来了解如何配置JVM的内存空间。

可以通过java -X命令获取java的启动参数列表,或者查看文档。

参数 描述 默认值
-server 服务器模式
-Xms 堆初始化容量
-Xmx 堆最大可分配容量 建议根据可用物理内存设置
-Xmn 年轻代堆初始化容量(且为最大容量) 建议不配置,根据NewRatio动态调整
-Xss 栈大小 320KB-1MB
-XX:MetaspaceSize 元空间初始化容量
-XX:MaxMetaspaceSize 元空间最大可分配容量
-XX:NewSize 同-Xmn
-XX:NewRatio 年老代和年轻代的容量比例 2
-XX:SurvivorRatio Eden和单个Survivor的容量比例 8
-XX:+UseAdaptiveSizePolicy 允许JVM动态调整年老代和年轻代的容量比例 enabled
-XX:+PrintGC 每次GC时输出相关信息 disabled
-XX:+PrintGCDateStamps GC日志中输出日期时间
-Xloggc:./gc.log GC日志文件位置
-XX:+HeapDumpOnOutOfMemoryError 在OOM时输出堆区内存情况 disabled
-XX:HeapDumpPath=path 输出堆区内存到指定文件
-XX:+UseSerialGC 串行GC disabled
-XX:+UseParallelGC 并行GC JDK8中服务器模式下默认GC选项
-XX:+UseG1GC G1 GC JDK9中服务器模式下默认GC选项

5. 参考资料

  1. 官方文档:The Java Virtual Machine Specification
  2. Java Heap Space vs Stack – Memory Allocation in Java
  3. IBM Developer Works - Understanding how the JVM uses native memory on Windows and Linux
  4. 官方文档:JDK tool - java
  5. JDK9:JEP 248: Make G1 the Default Garbage Collector