深入JVM内核(二)

JAVA 2015-12-04

在这里将继续对JVM进行探究,首先来看看

JVM启动流程

1.png 首先装载JVM所需的配置,通过环境变量寻找JDK中的java.exe实现查找,同时可以找到jvm.cfg(相当于配置文件)。接着根据配置找到JVM.dll(在jre的bin/server目录下)并调用系统API装载JVM.dll。最后就可以获得本地化接口并从main开始执行程序。

JVM基本结构

2.png 基本可以用上面的图表示,剩下的让我们接着往下看 解释一下PC寄存器的作用:java程序的每个线程拥有一个PC寄存器,它伴随着线程的创建而创建并且会指向下一条指令的地址,同时要注意在执行本地化方法时它的值为undefined。 1.方法区:用于保存转载类的信息,比如类型的常量池,字段,方法字节码等。它通常和永久区是关联的。 注:String等常量信息从JDK6后从方法区移到了堆。 2.堆:与所有线程共享;应用系统对象都保存在Java堆中;对分代GC来说,堆也是分代的。 注:GC即垃圾收集器,具体机制不展开。 3.栈:栈与堆不同,它是线程私有的并且栈由一系列帧组成,帧负责保存一个方法的局部变量、操作数栈、常量池指针,对于每一次方法调用创建一个帧,并压栈。 对于局部变量来说,例如public int Test(int i,long l,float f)这样一个函数,其栈从上到下的顺序是i,l,f。 那么Java的操作数栈是如何的?由于java没有寄存器,所以也是参数是通过操作数栈实现的。 举个例子:

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

我们对一个2byte数组赋值并运行1千万次,计算出时间,输出时间为5。 JVM此时的操作是

-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC

如果将正号改为负号,即改为栈下分配,结果将会不同:

…… [GC 3550K->478K(10240K), 0.0000977 secs] [GC 3550K->478K(10240K), 0.0001361 secs] [GC 3550K->478K(10240K), 0.0000963 secs] 564 时间上升到564! 从中可以看出JVM的一个优化机制:对于小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上,这样可以自动回收,减轻GC压力,而大对象或者逃逸对象无法栈上分配。

4.栈、堆、方法区交互 3.png

结合代码分析:

     public   class  AppMain     
         //运行时, jvm 把appmain的信息都放入方法区
        { 
          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{       
             //运行时, jvm 把appmain的信息都放入方法区
            private  name;     
             //new Sample实例后, name 引用放入栈区里,name 对象放入堆里 
            public  Sample(String name) 
            { 
               this .name = name; 
                }                           
            //print方法本身放入 方法区里。
            public   void  printName()    
            { 
                System.out.println(name);
              }
            }

内存模型

当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作,并且每一个操作都是原子的,即执行期间不会被中断。对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。如果需要在其他线程中立即可见,需要使用 volatile 关键字 示意图如下: 4.png

用一个例子验证一下:

 public class Sample {
        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 void static main(String args[])throws InterruptedException{
        VolatileStopThread t=new VolatileStopThread();
        t.start();
        Thread.sleep(1000);
        t.stopMe();
        Thread.sleep(1000);
        }
    }       
}

注:volatile 不能代替锁,一般认为volatile 比锁性能好,这并不是绝对的。选择使用volatile的条件是:语义是否满足应用。注意volatitle并不是线程安全的。

注:为了保证变量的可见性除了使用volatitle,还可以用synchronized((unlock之前,写变量值回主存))和final实现(一旦初始化完成,其他线程就可见) 要保证线程安全除了保证可见性还要保证有序性,那么什么是有序性呢? 有序性分为线程内有序性,和线程外,线程内有序线程外不一定有序,而指令重排则是相关干扰机制。 从简单的例子开始看:

线程内串行语义:

  • 写后读 a = 1;b = a; 写一个变量之后,再读这个位置
  • 写后写 a = 1;a = 2; 写一个变量之后,再写这个变量
  • 读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
  • 以上语句不可重排
  • 编译器不考虑多线程间的语义
  • 可重排: a=1;b=2

    也就是说如果JVM认为指令可重排,那么其赋值顺序可能并非所见到的先赋值a,再赋值b! 那么我们应该如何排除这一干扰呢,简单使用synchronized关键字以保证方法互斥。 我们来总结下指令重排的基本规则,着有助于我们避免问题的发生。

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

编译和解释运行的概念

最后,了解一下编译于解释运行方式的不同 1.解释运行即解释执行以解释方式运行字节码,通俗来说就是读一句执行一句 2.编译运行就是将字节码编译成机器码,并直接执行机器码,要注意的是它是运行时编译,所以编译后性能有较前者有较大的提升


本文由 Tony 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧