Java -- 虛擬機閱讀記錄

1、運行時數據區域

  • Java 堆
    在虛擬機啓動時創建,存儲對象實例和數組。是垃圾回收的主要區域,一般分爲 OLD、YOUNG(Eden,S0,S1)區域,進一步的劃分都是爲了更好的回收內存。邏輯上是連續的,但是可以處在物理上不連續的內存空間中。對象的組成包括對象頭、實例數據、對齊填充(不滿8的整數倍字節進行填充)。

  • Java對象佈局中有鎖相關的信息,也就是說synchronized關鍵字實際上鎖的是對象。對象頭中運行時數據的大小爲JVM的一個WORD大小,即32位JVM的爲32位,64位JVM爲64位,爲了存儲更多的信息,最後的lock兩位爲標誌位,分別表示無鎖、偏向鎖、輕量級鎖、重量級鎖、GC標記。其中lock和biased_lock一起表示是否有鎖。age字段爲4位,最大值爲16,也就是minor GC的時候,S0和S1循環累加的最大值,超過就會進入到老年代。
  • 無鎖情況:存儲25位的對象hashcode碼,調用了hashcode方法進行計算之後纔會存儲進去;偏向鎖情況:thread爲持有偏向鎖的線程ID,epoch爲偏向時間戳;輕量級鎖情況:指向棧中鎖記錄的指針;重量級鎖:指向管程Monitor的指針,也就是常用的synchronized鎖。
  • synchronized加鎖的方式:同步實例方法,鎖的是對象實例;同步類方法,鎖的是class實;同步代碼塊,鎖的是括號裏面的對象,鎖住的是所有以該對象爲鎖的代碼塊;相比JUC包中的代碼,區別在於synchronized控制的代碼在沒有多線程的情況下也無法解除,造成性能低下,所以後期synchronized進行了升級,也就是鎖膨脹。
  • 1.5之前使用操作系統進行加鎖,synchronized核心組件:
1、waitSet
調用wait方法後被阻塞的線程存放的位置

2、contentionList
競爭隊列,所有請求鎖的線程首先被放置到這裏

3、EntryList
contentionList中有資格成爲候選者的線程被放置到這裏

4、OnDeck
任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被設置爲OnDeck

5、Owner
當前已經獲取到鎖資源的線程

6、!Owner
當前釋放鎖的線程


//執行過程
JVM每次從隊列的尾部,取出一個線程作爲OnDeck,

過程:線程訪問代碼塊的時候,如果當前鎖處於重量級鎖,直接掛起線程;如果處於輕量級鎖,CAS嘗試修改鎖記錄的指針,失敗的話自旋幾次,如果自旋幾次都沒有成功,那麼當前鎖升級爲重量級鎖,當前線程掛起;如果處於偏向鎖,而且不是自己,說明多個線程加入了競爭,那麼開始撤銷偏向鎖,等待原持有偏向鎖的線程到達安全點,然後暫停原持有偏向鎖的線程,如果此時原線程沒有退出,則升級爲輕量級鎖,喚醒原有暫停的線程從安全點執行。

說明:偏向鎖主要是針對一個線程的,表示不需要進行同步操作,只需要簡單的判斷偏向鎖的標誌。此時如果有兩個線程來競爭,就會升級爲輕量級鎖了。

1、依賴
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>


2、類測試
package com.vim.modules.web.controller;

import org.openjdk.jol.info.ClassLayout;

public class Test {

    private boolean flag = false;

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new Test()).toPrintable());
    }
}

3、測試結果
com.vim.modules.web.controller.Test object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           9f 20 ed 27 (10011111 00100000 11101101 00100111) (669851807)
     12     1   boolean Test.flag                                 false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

4、分析結果
8個字節的對象頭+4個字節的類元空間指針+1個字節的實例數據+3個字節的8倍填充

 

public Test{
    public static void main(String[] args){
        //test變量存儲在main棧幀的局部變量表中,指向的對象存儲在堆中
        Test test = new Test(); 
        //和test一樣,指向的是同一個類元信息    
        Test test2 = new Test();
    }   
}
  • 方法區,也叫元空間
    存儲已被虛擬機加載的類信息、常量、靜態變量,也常被稱爲永久代,也可以理解爲class文件在內存中的存放位置。是在類加載的時候,通過類的全限定名稱,去讀取類的二進制字節流(各種加載方式),將其轉化爲方法區的運行時數據結構存儲起來。
    1)Class文件常量池,屬於非運行常量池,在編譯階段就已經確定。主要有兩種:字面量和符號引用量,如class類全限定名、方法名。在執行的時候會解析這些符號,轉化爲符號對應的直接引用,也就是指令碼,而運行時常量池,是jvm虛擬機在完成類裝載操作後,將class文件中的常量池載入到內存中,並保存在方法區

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
          
System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
System.out.println(s4 == s5);  // false
System.out.println(s1 == s6);  // true

解析:
1、s1 == s2,是由於在編譯期間,Hello字面量會直接放入class文件的常量池中,從而實現複用,載入運行時常量池後,s1、s2指向的是同一個內存地址,所以相等。
2、s1 == s3,在編譯期間進行了優化合並,所以也成立。
3、s1 == s4,在運行期間才能確定
4、s1 == s9,由於s9是兩個變量拼接而成,但是編譯期間無法確定,不能做優化。
5、s4 == s5,兩者都在堆中
6、s1 == s6,s5在堆中,內容爲Hello ,intern方法會嘗試將Hello字符串添加到常量池中,並返回其在常量池中的地址,因爲常量池中已經有了Hello字符串,所以intern方法直接返回地址;而s1在編譯期就已經指向常量池了,因此s1和s6指向同一地址,相等
  • 本地方法棧
    爲虛擬機使用到的 Native 方法服務

  • 程序計數器 (PC register)
    根據這個計數器的值來選取下一條需要執行的字節碼的指令,由於在任何一個確定的時刻,一個處理器都只有一個線程在工作,所以爲了在線程切換後能夠恢復到確定的位置。

  • 虛擬機棧(Stacks)
    每一個線程對應一個虛擬機棧,生命週期和線程相同,描述的是Java方法執行的內存模型。每一個方法執行時,會創建一個棧幀,壓入到棧中,執行完成後會出棧。
    棧幀在執行時,會存在局部變量表、操作數棧、動態鏈接、方法出口等信息,其中局部變量表中存放了編譯器內可知的各種數據類型,包括對象引用的內存地址。局部變量表所需要的空間,在編譯期間就已經分配確定完畢,在執行方法的時候,已經將方法下面的一條指令地址存儲到方法出口中,以便執行返回,動態鏈接中放置的是對應的方法在類元信息中的地址。以下實例演示: javap -c Test.class 反編譯

public class Test {

	public static int test(){
		int a=3;
		int b=4;
		int c = a + b;
		return c;
	}

	public static void main(String[] args){
		test();
	}
}
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":
()V
       4: return

  public static int test();
    Code:
       0: iconst_3    //將常量2壓入棧中
       1: istore_0    //將棧中的常量2取出,放入到局部變量表0位置
       2: iconst_4    //將常量4壓入棧中
       3: istore_1    //將棧中的常量4取出,放入到局部變量表1位置
       4: iload_0     //將局部變量表中0位置取出,放入棧中
       5: iload_1     //將局部變量表中1位置取出,放入棧中
       6: iadd        //將棧頂兩int型數值相加並將結果壓入棧頂
       7: istore_2    //將棧中的結果取出,放入到局部變量表2位置
       8: iload_2     //將局部變量表中2位置取出,放入棧中
       9: ireturn     //返回結果

//備註,istore 放到局部變量表 和 iload 從局部變量表取出

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #2                  // Method test:()I
       3: pop
       4: return
}

2、對象創建過程

  • 創建的幾種方式

package com.vim.modules.web.controller;

import com.vim.modules.web.model.User;

public class Test {

    public static void main(String[] args) throws Exception{
        //使用new關鍵字
        User user1 = new User();

        //使用Class類的newInstance方法
        User user2 = (User)Class.forName("com.vim.modules.web.model.User").newInstance();
        User user3 = User.class.newInstance();

        //使用Constructor類的newInstance方法
        User user4 = User.class.getConstructor().newInstance();
    }
}
  • 初始化的觸發條件
    new 實例化對象、讀取或設置一個類的靜態字段、調用類的靜態方法、反射,這幾種情況,如果類沒有進行初始化,會出發初始化的操作;當父類沒有初始化時,先初始化父類;

  • 非觸發條件情況
    讀取類的靜態字段,該字段被final修飾;通過數組定義來引用類;

3、垃圾回收機制

  • 垃圾回收的區域主要是在堆,分爲老年代、新生代(Eden、S0、S1),S0和S1屬於Survivor區域,新創建的對象都會放置到Eden區域,但是該區域最終會滿,會觸發minor GC,會將該區域無效的對象進行垃圾收集。會將Eden部分存活的對象放置到S0區域,S0滿了,會將存活的放置到S1中,S0和S1之間循環的放置會不斷的加年齡,到達了一定的次數,就會被移動到老年代。當老年代放置滿了,就會觸發full GC,通過jvisualvm命令可以看到動態的分配過程。

  • 對象無用的條件是沒有指針指向這些對象。可達性分析算法:通過一系列“GC root”的對象作爲起點,從這些節點開始向下搜索,節點走過的路徑稱爲引用鏈,當一個對象到GC root沒有任何引用鏈的話,則證明此對象是不可用的。還有一種方式是引用計數法,爲每一個對象創建一個引用計數,引用時加1,釋放引用時減1,到達0時可以被回收,但是會存在循環引用的問題。
  • 垃圾回收算法:標記清除,標記無用對象,然後進行清除回收,效率不高,無法解決碎片問題;標記整理,標記無用對象,讓所有的存活對象往一邊移動,然後清除邊界外的內存;複製算法,劃分兩個相同大小的區域,當一塊用完的時候,將獲得對象複製到另一塊上,然後把已使用的空間一次性的清理掉。

標記清除

標記整理

  • 回收算法

4、雙親委派模型

  • 如果一個類加載器收到了類加載的請求,它自己並不會去嘗試加載這個類,而是交由自己的父類加載器去加載,一直遞歸到頂層。只有父類無法加載到該類時,自己纔會去嘗試加載這個類。
package com.vim.modules.web.controller;

import java.net.URL;
import java.net.URLClassLoader;

public class Test {

    public static void main(String[] args) throws Exception{
        //啓動類加載器,sun.boot.class.path
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();

        //擴展類加載器,java.ext.dirs
        urls = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();

        //應用類加載器
        urls = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
        for(URL url : urls){
            System.out.println(url);
        }
    }
}
  • 類加載階段分爲加載、連接、初始化過程,而加載階段,需要通過類的全限定名去獲取類的二進制字節流。對於任何一個類,都需要由它的類加載器和這個類本身一同確立在JVM中的唯一性。每一個類加載器都有一個獨立的類名稱空間。
  • 雙親委派的好處是,使類有了層次劃分,父類已經加載的類,子類就不需要加載了,同時java核心庫中定義的class不會被任意替換,保證了安全性。
  • 自定義類加載器
package com.vim.modules.web.controller;

public class CustomClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) {
        //加載class字節流
        return null;
    }
}
  • 破壞雙親委派模式,原因是有時候需要使用當前類加載器的子類加載器去加載,但是雙親委派模式單向的,可以使用Thread類的setContextClassLoader方法去設置上下文類加載器,沒有設置的話默認是系統類加載器。

 

 

 

發佈了100 篇原創文章 · 獲贊 20 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章