深入JVM內核(原理、診斷與優化)——JVM運行機制

目錄:

1. JVM啓動流程
2. JVM基本結構
3. 內存模型
4. 編譯和解釋運行的概念

1.JVM啓動流程

在這裏插入圖片描述

2.JVM基本結構

在這裏插入圖片描述

2.1 PC寄存器
  • 每個線程擁有一個PC寄存器
  • 在線程創建時 創建
  • 指向下一條指令的地址
  • 執行本地方法時,PC的值爲undefined
2.2 方法區
  • 保存裝載的類信息
    • 類型的常量池
    • 字段,方法信息
    • 方法字節碼
  • 通常和永久區(Perm)關聯在一起

JDK6時,String等常量信息置於方法
JDK7時,已經移動到了堆

2.3 Java堆
  • 和程序開發密切相關
  • 應用系統對象都保存在Java堆中
  • 所有線程共享Java堆
  • 對分代GC來說,堆也是分代的
  • GC的主要工作區間

在這裏插入圖片描述

2.4 Java棧
  • 線程私有
  • 棧由一系列幀組成(因此Java棧也叫做幀棧)
  • 幀保存一個方法的局部變量、操作數棧、常量池指針
  • 每一次方法調用創建一個幀,並壓棧
  • 局部變量表 包含參數和局部變量
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;
	}

}

在這裏插入圖片描述

  • 函數調用組成幀棧
public static int runStatic(int i,long l,float  f,Object o ,byte b){
	return runStatic(i,l,f,o,b);
}

在這裏插入圖片描述

  • 操作數棧
    • 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);
    }
}

在這裏插入圖片描述

 - 小對象(一般幾十個bytes),在沒有逃逸的情況下,可以直接分配在棧上
 - 直接分配在棧上,可以自動回收,減輕GC壓力
 - 大對象或者逃逸對象無法棧上分配
  • 棧、堆、方法區交互
    在這裏插入圖片描述
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);
  } 
}

3.內存模型

每一個線程有一個工作內存和主存獨立
工作內存存放主存中變量的值的拷貝
在這裏插入圖片描述

當數據從主內存複製到工作存儲時,必須出現兩個動作:第一,由主內存執行的讀(read)操作;第二,由工作內存執行的相應的load操作;當數據從工作內存拷貝到主內存時,也出現兩個操作:第一個,由工作內存執行的存儲(store)操作;第二,由主內存執行的相應的寫(write)操作

每一個操作都是原子的,即執行期間不會被中斷

對於普通變量,一個線程中更新的值,不能馬上反應在其他變量中
如果需要在其他線程中立即可見,需要使用 volatile 關鍵字

在這裏插入圖片描述

  • volatile
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 -server 運行 無法停止
  • volatile 不能代替鎖
  • 一般認爲volatile 比鎖性能好(不絕對)
  • 選擇使用volatile的條件是:語義是否滿足應用
  • 可見性

    • 一個線程修改了變量,其他線程可以立即知道
  • 保證可見性的方法

    • volatile
    • synchronized (unlock之前,寫變量值回主存)
    • final(一旦初始化完成,其他線程就可見)
  • 有序性

    • 在本線程內,操作都是有序的
    • 在線程外觀察,操作都是無序的。(指令重排 或 主內存同步延時)
  • 指令重排

    • 線程內串行語義
    • 寫後讀 a = 1;b = a; 寫一個變量之後,再讀這個位置。
    • 寫後寫 a = 1;a = 2; 寫一個變量之後,再寫這個變量。
    • 讀後寫 a = b;b = 1; 讀一個變量之後,再寫這個變量。
    • 以上語句不可重排
    • 編譯器不考慮多線程間的語義
    • 可重排: a=1;b=2;
  • 指令重排 – 破壞線程間的有序性

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
flag=true
a=1

線程B
flag=true(此時a=0)

線程A首先執行writer()方法
線程B線程接着執行reader()方法
線程B在int i=a+1 是不一定能看到a已經被賦值爲1
因爲在writer中,兩句話順序可能打亂

  • 指令重排 – 保證有序性的方法
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;      
        ……
    }
}
}

線程A
flag=true
a=1

線程B
flag=true(此時a=1)

同步後,即使做了writer重排,因爲互斥的緣故,reader 線程看writer線程也是順序執行的。

  • 指令重排的基本原則
  • 程序順序原則:一個線程內保證語義的串行性
  • volatile規則:volatile變量的寫,先發生於讀
  • 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
  • 傳遞性:A先於B,B先於C 那麼A必然先於C
  • 線程的start方法先於它的每一個動作
  • 線程的所有操作先於線程的終結(Thread.join())
  • 線程的中斷(interrupt())先於被中斷線程的代碼
  • 對象的構造函數執行結束先於finalize()方法

4.解釋運行和編譯運行(JIT)

  • 解釋運行

    • 解釋執行以解釋方式運行字節碼
    • 解釋執行的意思是:讀一句執行一句
  • 編譯運行(JIT)

    • 將字節碼編譯成機器碼
    • 直接執行機器碼
    • 運行時編譯
    • 編譯後性能有數量級的提升
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章