JVM 学习笔记 1. JVM 运行模型
目录
- JVM 启动流程
- JVM 基本结构
- 内存模型
- 虚拟机的运行方式
1. JVM 启动流程
如下图所示:
2. JVM 基本结构
两幅经典的模型图:
其中:
- PC寄存器:每个线程都拥有一个PC寄存器,用于指向下一条指令的地址,因此,PC是线程私有的内存。当执行 native 方法时,PC的值为undefined。此内存区域是唯一一个在 Java 虚拟机规范中没有规定 OOM 的区域。
- Java 栈:是线程私有的,栈是由一系列帧(frame)组成。JVM 是 Stack-Based 的,栈帧中保存:方法的返回值(Return Value),局部变量表(Local variables),操作数栈(Operand Stack)和常量池指针(Constant Pool Refernce)。其中,局部变量表包含了参数和局部变量(槽)。两个经典的异常:StackOverflowError 和 OutOfMemoryError。
- 本地方法栈:JVM 执行 Native 方法所使用的栈,HotSpot 直接将 Java 栈和 Native 栈合二为一。
- Java 堆:用于保存应用程序对象实例,被所有线程所共享,是发生 GC 的主要区域。对于分代 GC 而言,堆也是分代的:新生代和老年代。Java 堆可能划分出多个线程私有的分配缓存区(TLAB, Thread Local Allocation Buffer)。
- 方法区:被各个线程共享。用来存储已被虚拟机加载的类信息、常量、静态变量、方法的字节码等数据。别名是 Non-Heap,在 HotSpot 中,方法区经常被称为永久代(PermG),因为HotSpot将分代延生到方法区,该区域也可尽心回收。值得注意的是,JDK1.7的HotSpot已经将Interned Strings(字符串常量池)移出永久代。
- 运行时常量池(Runtime Constant Pool):方法区的一部分。主要用来存放编译期生成的各种字面量和符号引用,当然,运行时也可以将新的常量放入池中,如:String.intern()方法。
- 直接内存(Direct Memory):不是 JVM 运行时数据区的一部分,没有在虚拟机规范中定义该内存区域。NIO 利用 Native 函数库直接分配堆外内存,避免了Java堆和 堆外内存的来回复制,提高了性能。
栈的执行过程
JVM 没有寄存器(除PC),所有的参数传递都使用操作数栈。
public static int add(int a,int b){
int c = 0;
c = a + b;
return c;
}
编译之后,注意操作数栈如何实现参数传递。
0: iconst_0 // 0压栈
1: istore_2 // 弹出int,存放于局部变量2
2: iload_0 // 把局部变量0压栈
3: iload_1 // 局部变量1压栈
4: iadd // 弹出2个变量,求和,结果压栈
5: istore_2 // 弹出结果,放于局部变量2
6: iload_2 // 局部变量2压栈
7: ireturn // 返回
局部变量表和操作数栈的变化过程:
栈上分配
public class OnStackTest {
public static void alloc() {
byte[] b = new byte[2];
b[0] = 1;
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e - b);
}
}
默认运行,采用了栈上分配了. 测试结果:
➜ jvm-learning java com.nil2inf.memory.OnStackTest
52
➜ jvm-learning java -server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC com.nil2inf.memory.OnStackTest
40
➜ jvm-learning java -server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC com.nil2inf.memory.OnStackTest
[GC 2624K->432K(9856K), 0.0014784 secs]
[GC 3056K->416K(9856K), 0.0006314 secs]
[GC 3040K->432K(9856K), 0.0004287 secs]
[GC 3056K->416K(9728K), 0.0003950 secs]
... ...
[GC 3280K->400K(9984K), 0.0001506 secs]
[GC 3280K->400K(10048K), 0.0001993 secs]
[GC 3408K->400K(10048K), 0.0001092 secs]
... ...
[GC 3664K->400K(10176K), 0.0001128 secs]
582
栈上分配:
- 栈上分配、标量替换技术是JVM的一项优化技术,涉及到逃逸分析和标量替换。
- 通常只有没有逃逸的小对象,才可以栈上分配。反之,大对象或者逃逸对象无法栈上分配。
- 栈上分配的目的是减清 GC 的压力。
3. 内存模型
- 每一个线程有一个工作内存和主内存独立。
- 工作内存存放主存中变量的值的拷贝。
- 原子操作。
read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容 - 使用 volatile 关键字能够保证变量更改在其他线程立即可见。
可见性
一个线程修改了变量,其他线程可以立即知道。
如何确保可见性:
- volatile
- synchronized (unlock之前,写变量值回主存)
- final(一旦初始化完成,其他线程就可见)
有序性
在本线程内,操作都是有序的。在线程外观察,操作都是无序的。(指令重排 或 主内存同步延时)。
重排序
指令重排序。
4. 虚拟机的运行方式
虚拟机中存在两种运行方式:分为解释和编译。
字节码指令编译为本机机器指令过程,有解释器或者编译器完成.
a. 解释
解释是最简单的字节码编译形式. 解释器查找每条字节码指令对应的硬件编码,再由 CPU 执行相应的硬件指令。
这个过程可以准确执行字节码,没有机会对某个指令集合进行优化,难以发挥目标平台处理器的最佳性能。
b. 编译
编译执行应用程序时,编译器会将加载运行时会用到的全部代码. 因为编译器可以将字节码编译为本地代码,因此它可以获取到完整或部分运行时上下文信息,并依据收集到的信息决定到底应该如何编译字节码。
可以对指令集合进行优化,优化后的指令集合会被存储到 code cache 的数据结构中,当下次执行这部分字节码序列时,会执行这些经过优化后被存储到code cache的指令集合。在某些情况下,性能计数器会失效,并覆盖掉先前所做的优化,这时,编译器会执行一次新的优化过程。
使用code cache的好处是优化后的指令集可以立即执行.
c. 优化
随着动态编译器一起出现的是性能计数器。
例如,编译器会插入性能计数器,以统计每个字节码块(对应与某个被调用的方法)的调用次数。 -- 代码的热度.
运行时数据监控有助于编译器完成多种代码优化工作,进一步提升代码执行性能。