深入理解JVM-JVM运行机制分析

章节目录:

1.JVM启动流程

2.JVM 类加载过程

3.JVM运行时数据区域

4.Java内存模型 

JVM启动流程 

  1) 创建JVM装载环境和配置 

  2) 装载JVM.dll 

  3) 初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例

  4) 调用JNIEnv实例装载并处理class类。  

blob.png

JVM 类加载过程:

   类从加载到虚拟机到卸载,它的整个生命周期包括:加载(Loading),验证(Validation),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading)。其中,验证、准备和解析部分被称为连接(Linking)。

blob.png

JVM运行时数据区域 

blob.png

  1、程序计数器(PC寄存器):每一条java线程都有一个独立的程序计数器,我们把线程相互独立隔离的区域叫线程私有的,它的作用可以看作是当前线程所执行的字节码的行号指示器,它是一块较小的空间区域,如果执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码的指令地址,如果是native的方法,这个计数器的值为空(undefined)。

  2、java虚拟机栈:java虚拟机栈与程序计数器一样,也是一条线程私有的,java虚拟机栈描述的是java方法执行的内存模型,每个方法被执行的时候 都会同时创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链路,方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  3、局部变量表:局部变量表存放了编译器可知的8种java基本类型数据、对象引用(注意不是对象实例本身)、方法返回地址returnAddress;

  由下案例可以看出局部变量表的参数的布局情况:

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;
  }
}

blob.png

   局部变量表空间单位是槽(Slot),每个槽位最大能容纳32位的数据类型,其中64位长度的double和long类型会占用两个slot,其余的数据类型只占用一个slot。局部变量表所需内存空间在编译期间完成分配,当进入一个方法时,该方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

  操作数栈: 和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用,所有参数传递使用操作数栈。

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   //返回

图示:

blob.png


 4、本地方法栈:本地方法栈和虚拟机栈作用非常相似,不同的是java虚拟机栈是为执行的是java方法服务的,而本地方法栈是为native的方法执行服务的。

 5、java堆:java堆(heap)是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在堆上分配内存。在堆上的内存是分代管理的,分为新生代和老年代,新生代又细分为:Eden,From Survivor,To Survivor,它们空间大小比例为8:1:1。

 6、方法区:方法区与java堆一样,是各个线程共享的内存区域,它用用于存储已被虚拟机加载的类信息,常量,静态变量、即时编译器编译后的代码等数据。虽然java虚拟机规范把方法区描述为堆得一个逻辑部分,但是它却有一个别名叫Non-Heap(非堆),目的应该是与java堆区分开来,也称“永久代”(Permanent Generation)。hotspot虚拟机永久代已经完全在JDK 8移除,用Native Memory来的实现,命名为metaSpace..

 7、运行时常量池:运行时常量池是方法区的一部分。用于存放编译期生成的各种字面变量、符号引用、直接引用等。这些内容将在类加载后存放到方法区的运行时常量池中,另外在运行期间也可以将新的常量存放到常量池中,如String的intern()方法。

 8、Java直接内存:直接内存并不是java虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是在java开发中还是会使用到。  JDK1.4中新引入的NIO(new I/O),引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,可以使用操作系统本地方法库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为堆外直接内存的引用进行操作,避免了java堆内存和本地直接内存间的数据拷贝,可以显著提高性能。虽然直接内存并不直接收到java虚拟机内存影响,但是如果java虚拟机各个内存区域总和大于物理内存限制,从而导致直接内存不足,动态扩展时也会抛出OutOfMemoryError异常。

JAVA内存模型:

   内存区域就是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图描述这写交互:

blob.png

共享变量:

  对于多个线程共享的变量,存储在内存中,每个线程都有自己独立的工作内存,线程只能访问自己的工作内存,不可以访问其他线程的工作内存,工作内存中保存了主内存共享变量的副本,只能通过操作工作内存的副本实现对主内存的变量操作,操作完毕后在同步回主内存中。

线程变量的工作流程:

blob.png

  read and load 从主存复制变量到当前工作内存
  use and assign  执行代码,改变共享变量值 
  store and write 用工作内存数据刷新主存相关内容

  其中use and assign 可以多次出现

    但是这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样,对于普通变量,一个线程中更新的值,不能马上反应在其他变量中,如果需要在其他线程中立即可见,需要使用 volatile 关键字,但不能保证原子性。JVM对于64位的数据类型long/double的读写操作划分为两次的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型load、store、read、write这四个操作的原子性,这就是所谓的long和double的非原子性.

  原子性、可见性与有序性

  原子性:基本数据类型的访问读写是具备原子性的,synchronized块之间的操作也具备原子性。

   可见性:指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。synchronized和final 包括 volatile 可以保证可见性。Final 修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么在其他线程中就能看见final字段的值。

   有序性:volatile本身包含了禁止指令重排序的语义,还有synchronized关键字。

  内存可见性:

   一个线程修改了变量,其他线程可以立即知道,通过参数volatile

   如: 

public class VolatileStopThread extends Thread{
 
      private  boolean stop = false;
 
      public void stopMe() {
           stop = true;
      }
 
      public void run() {
           int i = 0;
           while (!stop) {
                 i++;
           }
           System.out.println("Stop thread"+i);
      }
 
      public static void main(String args[]) throws InterruptedException {
           VolatileStopThread t = new VolatileStopThread();
           t.start();
           Thread.sleep(1000);
           t.stopMe();
           Thread.sleep(1000);
      }
}

  假设如果没有volatile,那么程序将无法停止运行,因为VolatileStopThread这个线程只在工作内存区域查看这个值。

  所以对于上面的情景,要求一个线程对open的改变,其他的线程能够立即可见,Java为此提供了volatile关键字,在声明stop变量的时候加入 volatile关键字就可以保证stop的内存可见性,即stop的改变对所有的线程都是立即可见的。volatile保证可见性的原理是在每次访问变 量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。 对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的

 有序性:

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

  指令重排序

  很多介绍JVM并发的书或文章都会谈到JVM为了优化性能,采用了指令重排序,但是对于什么是指令重排序,为什么重排序会优化性能却很少有提及,其实道理很简单,假设有这么两个共享变量a和b:

private int a;
private int b;

在线程A中有两条语句对这两个共享变量进行赋值操作:

a = 1;
b = 2;

  假设当线程A对a进行复制操作的时候发现这个变量在主内存已经被其它的线程加了访问锁,那么此时线程A怎么办?等待释放锁?不,等待太浪费时间了,它会去尝试进行b的赋值操作,b这时候没被人占用,因此就会先为b赋值,再去为a赋值,那么执行的顺序就变成了:

b = 2;
a = 1;

对于在同一个线程内,这样的改变是不会对逻辑产生影响的,但是在多线程的情况下指令重排序会带来问题,看下面这个情景:

package com.internet.test;
 
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

blob.png

  要解决重排序问题还是通过volatile关键字, volatile关键字能确保变量在线程中的操作不会被重排序而是按照代码中规定的顺序进行访问。synchronized/final也是可以保证有序的 

指令重排的基本原则 :

 程序顺序原则:一个线程内保证语义的串行性

 volatile规则:volatile变量的写,先发生于读

 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前

 传递性:A先于B,B先于C 那么A必然先于C

 线程的start方法先于它的每一个动作

 线程的所有操作先于线程的终结(Thread.join())

 线程的中断(interrupt())先于被中断线程的代码

 对象的构造函数执行结束先于finalize()方法 

 JVM性能调优参考资料: 《深入理解Java虚拟机》-周志明 建议买一本好好看看。

 http://www.cnblogs.com/wade-luffy/p/5753057.html

发表评论