您的位置:首页 > 其它

JVM 学习笔记 1. JVM 运行模型

2015-08-11 20:42 197 查看

目录

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. 优化

随着动态编译器一起出现的是性能计数器。

例如,编译器会插入性能计数器,以统计每个字节码块(对应与某个被调用的方法)的调用次数。 -- 代码的热度.

运行时数据监控有助于编译器完成多种代码优化工作,进一步提升代码执行性能。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: