前言
Java虚拟机使用软件模拟Java字节码的指令集,它只是一个可以运行Java代码的假想计算机,JVM实际上运行在操作系统之上,它与硬件没有直接的交互。用户编写的Java代码通过编译器生成class文件,之后通过JVM的类加载组件加载到内存中并且存放在方法区中,用户需要创建类的实例需要根据方法区里的数据创建Class对象,生成对应的类对象,执行的代码由JVM的执行引擎转换成本地代码执行。
JVM内存结构
JVM运行时的内存被分成了5个主要部分,Java方法栈、程序计数器、堆区、方法区和本地方法栈,现在来详细讨论这些内存区域的作用。
- Java方法栈:JVM运行程序方法的时候会为每个方法创建一个栈帧,栈帧中包含当前方法的局部变量表,传递参数、返回地址和额外信息,每当一个方法运行完成就会从它对应的栈帧去除返回地址同时将该栈帧从方法栈中弹出。
- 程序计数器:存放的是程序运行到的下一条指令的位置,大小是固定的,如果当前正在执行的是本地方法,那么程序计数器中的值是未定义的。
- 堆区:主要负责存放各种用户定义的对象,是内存中最大的一块区域,也是垃圾回收主要运行的内存区域。
- 方法区:负责存放用户定义的类信息,包含类中的静态变量,定义的常量值和类中方法的字节码数据。
- 本地方法栈:负责本地方法调用的时候存放本地方法栈帧,JVM没有强制规定本地方法栈的实现,不同JVM实现可以不相同。
JVM配置参数
JVM内的各种内存结构大小都是可以通过参数调整的,这里先简单介绍配置堆大小的-Xms初始堆大小,-Xmx最大的堆大小,-Xmn年轻带的堆大小,其他的Suivivor比例等都可以配置,这里不再赘述。
堆配置
private static void testHeap() {
byte[][] bytes = new byte[1024][];
for (int i = 0; i < 1000; i++) {
bytes[i] = new byte[1024 * 1024];
}
}
java -Xms5m -Xmx5m Main
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Main.main(Main.java:5)
上面的例子不断地分配1M内存空间,不过堆的大小被限制在5M,用不了多久就导致堆内存用尽抛出了OutOfMemoryError。实际开发的过程中如果程序分配的对象占用空间很大可以将堆的大小尽量设置大一点,不过最终还是要受到硬件内存大小的限制。
方法区配置
java -Xms2048m -Xmx2048m -XX:PermSize5m -XX:MaxPermSize5m Main
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize5m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize5m; support was removed in 8.0
上面的代码中不断的通过自定义的ClassLoader向程序中加载Class对象,这样会不断增大方法区的大小,不过8.0中已经取消了这个方法区大小设置,也就是说用户可以加载任意多个类,不过依然受物理内存大小的限制。
栈配置
private static int count = 0;
private static void testStack() {
count++;
System.out.println(count);
testStack();
}
java -Xss64k Main
The stack size specified is too small, Specify at least 108k
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
javac Main.java
java -Xss128k Main
996
997
998
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.ext.DoubleByte$Encoder.encodeArrayLoop(DoubleByte.java:578)
at sun.nio.cs.ext.DoubleByte$Encoder.encodeLoop(DoubleByte.java:617)
at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
上面通过配置-Xss设置线程Java方法栈的大小,为了能够达到方法栈的上线这里使用递归调用,可以看到栈大小越大它能够容纳的栈帧越多,如果栈帧超出了配置的大小会抛出StackOverflowError。
GC信息查看
private static void testGC() {
for (int i = 0; i < 3; i++) {
byte[] bytes = new byte[1024 * 1024 * 5];
System.gc();
}
}
java -XX:+PrintGC Main
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
[GC (System.gc()) 7721K->5848K(249344K), 0.0029938 secs]
[Full GC (System.gc()) 5848K->5761K(249344K), 0.0045162 secs]
[GC (System.gc()) 12181K->10881K(249344K), 0.0026517 secs]
[Full GC (System.gc()) 10881K->5760K(249344K), 0.0049357 secs]
[GC (System.gc()) 12181K->10880K(249344K), 0.0011909 secs]
[Full GC (System.gc()) 10880K->5760K(249344K), 0.0023418 secs]
java -XX:+PrintGCDetails Main
[Full GC (System.gc()) [PSYoungGen: 5120K->0K(75776K)] [ParOldGen: 5761K->5760K(173568K)] 10881K->5760K(249344K), [Metaspace: 2651K->2651K(1056768K)], 0.0057589 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
[GC (System.gc()) [PSYoungGen: 6420K->5120K(75776K)] 12181K->10880K(249344K), 0.0021681 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 5120K->0K(75776K)] [ParOldGen: 5760K->5760K(173568K)] 10880K->5760K(249344K), [Metaspace: 2651K->2651K(1056768K)], 0.0047050 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 75776K, used 1300K [0x000000076b980000, 0x0000000770e00000, 0x00000007c0000000)
eden space 65024K, 2% used [0x000000076b980000,0x000000076bac5360,0x000000076f900000)
from space 10752K, 0% used [0x000000076f900000,0x000000076f900000,0x0000000770380000)
to space 10752K, 0% used [0x0000000770380000,0x0000000770380000,0x0000000770e00000)
ParOldGen total 173568K, used 5760K [0x00000006c2c00000, 0x00000006cd580000, 0x000000076b980000)
object space 173568K, 3% used [0x00000006c2c00000,0x00000006c31a0280,0x00000006cd580000)
Metaspace used 2657K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
testGC方法会先不断的申请内存然后由于引用只保存的本次申请的内存,之前内存就变成了垃圾,需要执行GC操作,这里通过调用System.gc()触发了垃圾回收。打印的回收信息头部如果是GC开头代表是一次Minor GC,也就是指针对某个部分做垃圾回收,Full GC则代表对所有需要回收垃圾的内存区域做垃圾回收。后面方括号内部的“7721K->5848K(249344K)”指的是该区域(GC前已使用的容量->GC后已使用的容量(该内存区总容量))。下面更详细的垃圾回收信息包含了回收的区域名和该区域执行回收的数据,最后“0.0057589 secs”表示该内存区域GC所占用的时间,单位是秒。
JVM退出
Java虚拟机与程序的生命周期,在如下几种情况下,Java虚拟机将结束生命周期:
- 执行System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到异常或错误而异常终止
- 由于操作系统出现错误导致Java虚拟机进程终止
JVM退出的时候有一个监视钩子addShutdownHook,它能够添加任意多个关闭钩子,不过这些钩子是并发执行的它们的顺序是不一定的,如果JVM抛出异常导致退出那么这些钩子还是会被执行的,不过某些特殊情况钩子不一定会被执行。
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Hello JVM shutdown");
}));
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Bye-bye!!");
}));
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException("");
}
Exception in thread "main" java.lang.RuntimeException:
at Main.main(Main.java:17)
Hello JVM shutdown
Bye-bye!!