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

目录

  1. JVM 启动流程
  2. JVM 基本结构
  3. 内存模型
  4. 虚拟机的运行方式

1. JVM 启动流程

如下图所示:

JVM启动流程

2. JVM 基本结构

两幅经典的模型图:

JVM基本结构

JVM-internal

其中:

  1. PC寄存器:每个线程都拥有一个PC寄存器,用于指向下一条指令的地址,因此,PC是线程私有的内存。当执行 native 方法时,PC的值为undefined。此内存区域是唯一一个在 Java 虚拟机规范中没有规定 OOM 的区域。
  2. Java 栈:是线程私有的,栈是由一系列帧(frame)组成。JVM 是 Stack-Based 的,栈帧中保存:方法的返回值(Return Value),局部变量表(Local variables),操作数栈(Operand Stack)和常量池指针(Constant Pool Refernce)。其中,局部变量表包含了参数和局部变量(槽)。两个经典的异常:StackOverflowError 和 OutOfMemoryError。
  3. 本地方法栈:JVM 执行 Native 方法所使用的栈,HotSpot 直接将 Java 栈和 Native 栈合二为一。
  4. Java 堆:用于保存应用程序对象实例,被所有线程所共享,是发生 GC 的主要区域。对于分代 GC 而言,堆也是分代的:新生代和老年代。Java 堆可能划分出多个线程私有的分配缓存区(TLAB, Thread Local Allocation Buffer)。
  5. 方法区:被各个线程共享。用来存储已被虚拟机加载的类信息、常量、静态变量、方法的字节码等数据。别名是 Non-Heap,在 HotSpot 中,方法区经常被称为永久代(PermG),因为HotSpot将分代延生到方法区,该区域也可尽心回收。值得注意的是,JDK1.7的HotSpot已经将Interned Strings(字符串常量池)移出永久代。
  6. 运行时常量池(Runtime Constant Pool):方法区的一部分。主要用来存放编译期生成的各种字面量和符号引用,当然,运行时也可以将新的常量放入池中,如:String.intern()方法。
  7. 直接内存(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       // 返回

局部变量表和操作数栈的变化过程:

local variables&stack

栈上分配

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. 内存模型

memory model

  1. 每一个线程有一个工作内存和主内存独立。
  2. 工作内存存放主存中变量的值的拷贝。
  3. 原子操作。
    read and load 从主存复制变量到当前工作内存
    use and assign 执行代码,改变共享变量值
    store and write 用工作内存数据刷新主存相关内容
  4. 使用 volatile 关键字能够保证变量更改在其他线程立即可见。

可见性

一个线程修改了变量,其他线程可以立即知道。

如何确保可见性:

  • volatile
  • synchronized (unlock之前,写变量值回主存)
  • final(一旦初始化完成,其他线程就可见)

有序性

在本线程内,操作都是有序的。在线程外观察,操作都是无序的。(指令重排 或 主内存同步延时)。

重排序

指令重排序。

4. 虚拟机的运行方式

虚拟机中存在两种运行方式:分为解释和编译。

字节码指令编译为本机机器指令过程,有解释器或者编译器完成.

a. 解释
解释是最简单的字节码编译形式. 解释器查找每条字节码指令对应的硬件编码,再由 CPU 执行相应的硬件指令。

这个过程可以准确执行字节码,没有机会对某个指令集合进行优化,难以发挥目标平台处理器的最佳性能。

b. 编译
编译执行应用程序时,编译器会将加载运行时会用到的全部代码. 因为编译器可以将字节码编译为本地代码,因此它可以获取到完整或部分运行时上下文信息,并依据收集到的信息决定到底应该如何编译字节码。

可以对指令集合进行优化,优化后的指令集合会被存储到 code cache 的数据结构中,当下次执行这部分字节码序列时,会执行这些经过优化后被存储到code cache的指令集合。在某些情况下,性能计数器会失效,并覆盖掉先前所做的优化,这时,编译器会执行一次新的优化过程。
使用code cache的好处是优化后的指令集可以立即执行.

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

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

运行时数据监控有助于编译器完成多种代码优化工作,进一步提升代码执行性能。

posted @ 2015-08-11 20:42  nil2inf  阅读(955)  评论(1编辑  收藏  举报