文章目录
  1. 1. JVM运行机制
    1. 1.1. 运行环境
    2. 1.2. JVM启动流程
    3. 1.3. JVM基本结构
      1. 1.3.1. PC寄存器
      2. 1.3.2. 方法区
      3. 1.3.3. Java堆
      4. 1.3.4. Java栈
    4. 1.4. JVM内存模型
      1. 1.4.1. volatile关键字介绍
      2. 1.4.2. 内存模型相关的概念
    5. 1.5. 编译运行和解释运行的区别
    6. 1.6. jdk6下载地址

JVM运行机制

该文主要介绍:JVM启动流程JVM基本结构JVM的内存模型编译运行和解释运行的区别 通过这几部份来了解和学习JVM。

运行环境

1
2
java version:1.6.0_45
os:Linux centos6

JVM启动流程

JVM的启动流程如下:

jvm启动流程

通过java 命令执行某个class文件,通过jvm.cfg文件装载对应的配置信息,通过配置查找JVM.dll文件初始化JVM,找到main 方法运行。

JVM基本结构

JVM的主要结构如下:

JVM基本结构

我们主要关心的是:方法区、Java堆、Java栈和PC寄存器。下面将依次介绍

PC寄存器

  • 每个线程拥有一个PC寄存器
  • 在线程创建时 创建
  • 指向下一条指令的地址
  • 执行本地方法时,PC寄存器地址的值为undefined

方法区

  • 保存类的元信息
    • 类型的常量池
    • 字段、方法信息
    • 方法字节码
  • 通常和永久区(Perm)关联在一起
  • 注意点
    • JDK6中,String常量信息置于方法中;JDK7中,已经移动到堆中

Java堆

  • 和程序开发密切相关
  • 应用系统对象保存在Java堆中
  • 所有线程共享Java堆
  • 对分代GC来说,堆也是分代的
  • GC的主要工作空间

Java栈

  • 线程私有
  • 栈由一系列帧组成
  • 帧保存一个方法的局部变量、操作数栈、常量池指针
  • 每一次方法调用创建一个栈、并压栈

局部变量表

1
2
3
4
5
6
7
8
9
public class StackDemo{
public static int runStatic(int i,long l,float f,Object o,byte b){
return 0;
}
public int runInstance(char c,short s,boolean b){
return 0;
}
}

静态方法对应的局部变量表如下

0 int int i
1 long long l
3 float float f
4 reference Object o
5 int byte b

这里long 类型占用两个字节,Object 类型的变量只是一个引用

实例方法对应的局部变量表如下

0 reference this
1 int char c
2 int short s
3 int boolean b

基本和静态方法一致,只是局部变量表的第一个参数表示的是当前对象的引用

函数调用组成帧栈

通过下段代码演示

1
2
3
public static int runStatic(int i,long l,float f,Object o,byte b){
return runStatic(i,l,f,o,b);
}

上面这段代码是一个递归调用,而且是没有退出条件,最后结果就是栈溢出,下面的图片显示了调用三次的栈帧示例

栈帧示例

操作数栈

Java没有寄存器,所有参数传递使用操作数栈实现

1
2
3
4
5
public static int add(int a,int b){
int c = 0;
c = a + b;
return c;
}

翻译后

1
2
3
4
5
6
7
8
0: iconst_0 // int常量0压栈
1: istore_2 // 弹出int,存放在本地变量2,这里变量2表示c
2: iload_0 // 把局部变量0压栈,局部变量0表示a
3: iload_1 // 把局部变量1压栈,局部变量1表示b
4: iadd // 弹出2两个变量,就是结果压栈
5: istore_2 // 弹出结果,放入局部变量2中
6: iload_2 // 局部变量2压栈
7: ireturn // 返回

操作数栈例子

栈上分配

下面通过一段代码来演示栈上分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}
}
1
java -server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC OnStackTest

执行返回结果为 42

1
java -server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC OnStackTest

执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
[GC 2624K->192K(9856K), 0.0006780 secs]
[GC 2816K->160K(9856K), 0.0006150 secs]
[GC 2784K->160K(9856K), 0.0005490 secs]
[GC 2784K->160K(9856K), 0.0005980 secs]
[GC 2784K->160K(9856K), 0.0003480 secs]
[GC 2784K->160K(10048K), 0.0003390 secs]
[GC 3168K->124K(9920K), 0.0010710 secs]
[GC 3132K->124K(9984K), 0.0001650 secs]
[GC 3004K->124K(9984K), 0.0001340 secs]
[GC 3004K->124K(9984K), 0.0001420 secs]
[GC 3004K->124K(9792K), 0.0001190 secs]
...
1035

第一个命令表示是栈上分配,第二个命令表示在堆上分配

  • 小对象(一般几十比特),在没有逃逸的情况下,可以直接分配在栈上;ps:逃逸对象指,当变量(或者对象)在方法中分配后,其指针被返回或者被全局引用(这样就会被其他过程或者线程所引用),这种现象称作指针(或者引用)的逃逸(Escape)
  • 直接分配在栈上,可以自动回收,减少GC压力
  • 大对象或者逃逸对象无法栈上分配

栈、堆、方法区交互

通过下图和代码块来描述栈、堆、方法区的交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public   class  AppMain     
//运行时, jvmappmain的信息都放入方法区
public   static   void  main(String[] args) 
//main 方法本身放入方法区。 
{
Sample test1 = new  Sample( " 测试1 " );  
//test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面 
Sample test2 = new  Sample( " 测试2 " );
test1.printName(); 
test2.printName(); 
}
public   class  Sample       
//运行时, jvmappmain的信息都放入方法区 
private  name;     
//new Sample实例后, name 引用放入栈区里,  name 对象放入堆里 
public  Sample(String name) 
this .name = name; 
//print方法本身放入 方法区里。
public   void  printName()    
System.out.println(name); 
}

栈堆方法区交互

执行main方法的Sample对象的引用是放在main方法的栈中,对象本身是在堆中创建,在创建对象时需要读取方法区的类的元数据信息。

JVM内存模型

内存模型

  • 每一个线程有一个工作内存和主内存独立
  • 工作内存存放主内存中变量的值得拷贝

当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作

每一个操作都是原子的,即执行期间不会被中断。也就是read操作是原子的,但是read和load操作之间是可以暂停的

对于普通变量,一个线程中更新的值,不能马上反应在其他变量中

如果需要在其他线程中立即可见,需要使用 volatile 关键字

具体如下图:

JVM内存模型

volatile关键字介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class VolatileStopThread extends Thread{
private volatile boolean stop = false;
public void stopMe(){
stop=true;
}
public void run(){
int i=0;
while(!stop){
i++;
}
System.out.println("Stop thread");
}
public static void main(String args[]) throws InterruptedException{
VolatileStopThread t=new VolatileStopThread();
t.start();
Thread.sleep(1000);
t.stopMe();
Thread.sleep(1000);
}
}

volatile 不能代替锁

一般认为volatile 比锁性能好(不绝对)

选择使用volatile的条件是:满足某个线程的依赖另外线程的对共享变量的修改

内存模型相关的概念

  • 可见性
    • 一个线程修改了共享变量,其他线程可以立即知道
  • 保证可见性的方法
    • volatile 关键字
    • synchronized 关键字
    • final 关键字 (一旦初始化完成,其他线程可见)
  • 有序性
    • 在本线程内,操作都是有序的
    • 在线程外观察,操作都是无序的。(导致原因:指令重排或主内存同步延迟)
  • 指令重排
    • 线程内串行语义
      • 写后读 a=1;b=a; 写一个变量后,再读这个变量
      • 写后写 a=1;a=2; 写一个变量后,再写这个变量
      • 读后写 a=b;b=1;读完一个变量后,在写这个变量
      • 以上语句不可重排
      • 编译器不考虑多线程的语义
      • 可重排语句:a=1;b=2;

指令重排 —— 破坏线程间的有序性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class OrderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1;
flag = true;
}
public void reader() {
if (flag) {
int i = a +1;
……
}
}
}

执行流程:

线程A首先执行writer()方法

线程B线程接着执行reader()方法

线程B在int i=a+1 是不一定能看到a已经被赋值为1

因为在writer中,两句话顺序可能打乱

指令重排 —— 保证有序性的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class OrderExample {
int a = 0;
boolean flag = false;
public synchronized void writer() {
a = 1;
flag = true;
}
public synchronized void reader() {
if (flag) {
int i = a +1;
……
}
}
}

同步后,即使做了writer重排,因为互斥的缘故,reader 线程看writer线程也是顺序执行的。

指令重排的基本原则

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile变量的写,先发生于读
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A先于B,B先于C 那么A必然先于C
  • 线程的start方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行结束先于finalize()方法

编译运行和解释运行的区别

  • 解释运行
    • 解释执行以解释方式运行字节码
    • 解释执行的意思是:读一句执行一句
  • 编译运行(JIT)
    • 将字节码编译成机器码
    • 直接执行机器码
    • 运行时编译
    • 编译后性能有数量级的提升

jdk6下载地址

点击下载

文章目录
  1. 1. JVM运行机制
    1. 1.1. 运行环境
    2. 1.2. JVM启动流程
    3. 1.3. JVM基本结构
      1. 1.3.1. PC寄存器
      2. 1.3.2. 方法区
      3. 1.3.3. Java堆
      4. 1.3.4. Java栈
    4. 1.4. JVM内存模型
      1. 1.4.1. volatile关键字介绍
      2. 1.4.2. 内存模型相关的概念
    5. 1.5. 编译运行和解释运行的区别
    6. 1.6. jdk6下载地址