本文将对JVM内存数据模型进行介绍,并给出一个简单的Java应用程序,描述其内存分配过程。在编写代码中,只有对类、各个变量和Java对象做到心中有数,才能“下笔”(敲代码)如有神。
1. JVM内存数据模型
如下图所示,
根据JVM规范,在运行时刻JVM内存数据分为如下6种,
- PC Register 程序计数器: 一个JVM中支持多个线程的执行,每个线程拥有各自独立的程序计数器,程序计数器指向线程执行的当前方法地址。
- JVM Stacks 栈区:每个线程拥有各自独立的JVM栈,一个栈存储着frames列表,每个frame对应着一个方法调用,其保存着方法调用所使用的本地变量和Java对象的引用,方法返回的值和异常。Frame按照后入先出的原则,执行并返回调用结果。这个数据区会发生如下两种内存溢出错误,
- StackOverflowError 栈超过允许的调用深度
- OutofMemoryError 栈超过允许的可用内存大小
- Heap 堆区,这个区的数据被所有线程所共享,是类对象创建时分配内存的地方。这个区的内存被JVM管理,实现对象的自动回收,也就是GC。这个数据区发生如下的内存溢出错误,
- OutofMemoryError创建的对象超过可分配的内存大小
- Method Area方法区:这个区的数据被所有线程所共享,里面加载着类的定义,包括常量池,变量和方法数据等。这个数据区发生如下的内存溢出错误,
- OutofMemoryError加载的类超过可分配的内存大小
- Run-Time Contant Pool 常量池,一个类文件中所定义的常量,一般会存储在方法区中。
- 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将会执行如下动作,
- JVM根据启动参数,初始化各个内存区。
- JVM加载Demo类到方法区,加载各个变量和方法定义,加载常量定义,其中字符串常量从堆区分配。
- 启动一个main线程,执行main主程序,线程的执行进度记录在程序计数器中。同时,在栈区初始化当前线程的方法调用栈。
- 进入main()方法调用,创建frame1,初始化如下变量
- String args 输入参数,引用指向堆区所创建的args对象
- Demo demo 引用,指向堆区所创建的demo对象
- int i = 0 分配一个整型i,初始化值为0
- String s 引用,指向堆区所创建的s对象
- 进入print()方法调用,创建frame2
- String s 输入参数,引用指向堆区已创建的s对象
- 返回s,指向堆区的s对象。
- print()方法调用结束,栈回到frame1。
- main()方法调用结束,方法调用栈清空。
- main线程执行结束。
上述在内存的分配可以描述为下图,
3. 堆区和元空间
JVM Heap是最大的内存分配区域,所有的Java对象都从这里获得内存存储空间,这里也是JVM自动内存回收(GC)的地方。
要想了解GC的工作机制,首先需要了解堆区中Java对象按代进行存储的机制。整个堆区分为如下几个区域,
- Eden 伊甸园区:这里是创建对象时最先分配内存的地方,名副其实的创世区
- Survivor 存活区:在发生Young GC时,会将Eden区中大量不再使用的对象删除,留下来的放入Survivor区。注意的是,Survivor区一般有两个,每次YGC时,将会S0和S1交换着来保存存活下来的对象。也就是说,S0和S1总有一个是处于清空状态。
- Tenured年老代:在GC多次过后,有些对象存活时间比较长,将会移入到年老代。
至于对象的存活与否,如何回收,这个将涉及到对象引用计数的概念,以及各个GC算法实现,这里不再扩展。
下图描述了堆区的示意图,
图中还有一个元空间(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选项 |