天天看點

厚積薄發——Java1. JVM2. 并發

1. JVM

1.1 Java運作時資料區

1.1.1 線程私有區域

  1. 程式計數器:線程所執行的位元組碼的行号訓示器(唯一沒有oom異常的區域),記錄正在執行的虛拟機位元組碼指令的位址(如果正在執行的是本地方法則為空)。
  2. Java虛拟機棧:描述Java方法執行的記憶體模型。
  3. 本地方法棧:描述Native方法執行的記憶體模型。

1.1.2 線程共享區域

  1. 堆:存放所有的執行個體對象(-Xms設定堆最小值,-Xmx設定堆最大值)。
  2. 方法區(JDK8後的元空間):存儲已加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼以及運作時常量池。
  3. 直接記憶體: NIO類中引入了一種基于通道與緩存區 的 I/O 方式,它可以直接使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在 Java 堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作。

1.2 記憶體配置設定與回收

1.2.1 記憶體配置設定

  1. 對象優先在Eden區配置設定。
  2. 大對象直接在老年代配置設定。
  3. 長期存活對象進入老年代。

1.2.2 對象通路定位

  1. 句柄:Java 堆中會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊。
  2. 直接指針:Java 堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,而 reference中存儲的直接就是對象的位址。
這兩種對象通路方式各有優勢。使用句柄來通路的最大好處是reference中存儲的是穩定的句柄位址,在對象被移動時隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。使用直接指針通路方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

1.2.3 垃圾收集(GC)

1.2.3.1 哪些記憶體需要回收

Minor GC 的觸發條件:Eden區域沒有足夠的空間。

Full GC(Major GC)的觸發條件:老年代沒有足夠空間。

  1. 引用計數算法(無法處理循環引用)
  2. 可達性分析算法

    思想:從GC Roots開始分析引用鍊,不可達的對象可以回收。

    GC Roots:虛拟機棧中引用的對象、本地方法棧中引用的對象、方法區中類靜态屬性引用的對象、方法區中常量引用的對象。

Java中的引用類型

強引用(new):隻要強引用還存在,垃圾收集器就不會回收被引用的對象。

軟引用(SoftReference):在系統将要發生記憶體溢出異常之前,将會把這些對象列入回收範圍進行二次回收。

弱引用(WeakReference):隻能存活到下次垃圾收集發生之前。

虛引用(PhantomReference):一個對象是否有虛引用存在,不對其生存時間構成影響。

拯救對象:可以用finalize()方法拯救一次對象。

1.2.3.2 垃圾回收算法

Java堆分為新生代和老年代,預設比例為新生代:老年代 = 1:2,即新生代占總堆記憶體的1/3,老年代占2/3。
  1. 标記清除(先标記後清除):标記和清除步驟效率低,容易産生記憶體碎片。
  2. 複制算法(将記憶體分塊):每次隻能使用一部分記憶體造成浪費,适合新生代(Eden:Survivor = 8:1)。
  3. 标記整理(标記移動清理):适合老年代。
  4. 分代收集算法:新生代和老年代采用不同回收算法。

1.2.3.3 垃圾收集器

  1. Serial/Serial Old收集器
  2. Parallel/Parallel Old收集器
  3. CMS(Concurrent Mark Sweep):初始标記、并發标記、重新标記、并發清除
  4. G1:初始标記、并發标記、最終标記、篩選回收

1.3 性能監控與故障處理工具

1.3.1 jps(Jvm Process Status):虛拟機程序狀态工具

jps的功能類似于Linux的ps指令,可以列出正在運作的虛拟機程序, 并顯示虛拟機執行主類名稱,以及這些程序的本地虛拟機唯一ID(LVMID)。

jps [options] [hostid]

options:

-l 輸出主類全名,如果執行的是jar包則輸出jar路徑

-v 輸出虛拟機程序啟動時JVM參數

hostid:開啟了RMI服務的遠端虛拟機主機名

1.3.2 jstat(Jvm Statistic):虛拟機統計資訊監視工具

jstat用于監視虛拟機的各種運作狀态資訊,包括類裝載、垃圾收集以及運作期編譯狀況等。

jstat [ option vmid [ interval[s|ms] [count] ] ]

interval與count代表查詢間隔和次數

vmid是虛拟機程序ID

option:

-gc:監視Java堆狀況,包括容量、已用空間以及GC資訊等

-gcutil:與gc選項基本相同,但關注點主要是已使用空間占總空間的百分比

1.3.3 jinfo(Configuration Info):Java配置資訊工具

jinfo的作用是實時檢視和調整虛拟機各項參數。

jinfo [option] pid

1.4 類加載機制

1.4.1 類的生命周期

加載、連接配接(驗證、準備、解析)、初始化、使用、解除安裝

  1. 加載:通過一個類的全限定名來擷取定義此類的二進制位元組流,将這個位元組流所代表的的靜态存儲結構轉化為方法區的運作時資料結構,在記憶體中生成一個代表這個類的

    java.lang.Class

    對象作為方法區這個類的各種資料的通路入口。
  2. 驗證:確定Class檔案位元組流中包含的資訊符合目前虛拟機的要求,包括檔案格式驗證、中繼資料驗證、位元組碼驗證以及符号引用驗證。
  3. 準備:為類變量配置設定記憶體并設定類變量初始值,這裡的類變量指static變量。
  4. 解析:将常量池内的符号引用替換為直接引用。
  5. 初始化:準備階段變量已經賦過一次系統要求的初始值,初始化階段則根據程式員的要求進行變量初始化,即執行類構造器的

    <clinit>()

    方法。
  6. 使用
  7. 解除安裝

1.4.2 類加載器

類加載器在層次上由上至下(由父至子)分别為:

  1. 啟動類加載器(Bootstrap):加載

    %JAVA_HOME%\lib

    目錄下的類庫。
  2. 擴充類加載器(Extension):加載

    %JAVA_HOME%\lib\ext

    目錄下的類庫。
  3. 應用程式類加載器(Applicaton):加載使用者類路徑上指定的類庫。
  4. 自定義類加載器(User):繼承ClassLoader,并覆寫findClass()方法。

雙親委派模型:若一個類加載器收到類加載請求,它首先不會嘗試自己加載這個類,而是把這個請求委托給它的父加載器去完成,每一層次類加載器都是如此,隻有當父類加載無法加載時,子加載器才會嘗試自己加載。

破壞雙親委派機制:自定義類加載器,重寫loadClass()方法

2. 并發

2.1 程序與線程

2.1.1 概念了解

程序是程式的一次執行過程,是系統運作程式的基本機關,是以程序是動态的。系統運作一個程式即是一個程序從建立,運作到消亡的過程。

線程與程序相似,但線程是一個比程序更小的執行機關。一個程序在其執行的過程中可以産生多個線程。與程序不同的是同類的多個線程共享程序的堆和方法區資源,但每個線程有自己的程式計數器、虛拟機棧和本地方法棧,是以系統在産生一個線程,或是在各個線程之間作切換工作時,負擔要比程序小得多,也正因為如此,線程也被稱為輕量級程序。

2.1.2 線程生命周期

線程建立之後處于NEW(建立)狀态,調用start()方法後線程處于READY(可運作)狀态。可運作狀态的線程獲得CPU時間片後就處于RUNNING(運作)狀态。當線程執行wait()方法之後,線程進入WAITING(等待)狀态,進入等待狀态的線程需要依靠其他線程的通知才能夠傳回到READY狀态。sleep(long millis)或wait(long millis)方法會讓線程變為TIMED_WAITING狀态,TIME_WAITING(逾時等待) 在等待狀态的基礎上增加了逾時限制,當逾時時間到達後Java線程将會傳回到REDAY狀态。當線程調用同步方法時,在沒有擷取到鎖的情況下,線程将會進入到BLOCKED(阻塞)狀态。線程在執行完的run()方法之後将會進入到TERMINATED(終止)狀态。

2.1.3 線程死鎖

什麼是死鎖:多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,是以程式不可能正常終止。

産生死鎖的四個必要條件:

  1. 互斥條件:該資源任意一個時刻隻由一個線程占用。
  2. 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強行剝奪,隻有自己使用完畢後才釋放資源。
  4. 循環等待條件:若幹程序之間形成一種頭尾相接的循環等待資源關系。

如何避免死鎖:破壞請求與保持條件(一次性申請所有的資源)、破壞不剝奪條件(占用部分資源的線程進一步申請其他資源時,如果申請不到主動釋放它占有的資源)、破壞循環等待條件(按某一順序申請資源,釋放資源則反序釋放)。

2.1.4 常用方法

wait()與sleep()差別: sleep方法沒有釋放鎖,而wait方法釋放了鎖 。

start()與run():調用start()方法,會啟動一個線程并使其進入就緒狀态,當配置設定到時間片後就可以自動執行run()方法的内容,這是真正的多線程工作。 而直接執行run()方法,會把run方法當成一個main線程下的普通方法去執行,并不會在某個線程中執行它,是以這并不是多線程。

2.2 線程安全

synchronized關鍵字解決的是多個線程之間通路資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻隻能有一個線程執行。

synchronized實作單例模式:

public class Singleton {
    //volatile禁止指令重排序
    private volatile static Singleton instance;
    private Singleton() {}
    public static Singleton getSingleton() {
       //先判斷對象是否已經執行個體過,沒有執行個體化過才進入加鎖代碼
        if (instance == null) {
            //類對象加鎖
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
           

synchronized同步語句塊實作原理:

synchronized同步語句塊使用的是monitorenter和monitorexit指令,執行monitorenter指令時,線程試圖擷取monitor(monitor對象存在于每個Java對象的對象頭中,是以Java中任意對象可以作為鎖) 鎖的持有權。當計數器為0,則可以成功擷取,擷取後将鎖的計數器設為1。執行monitorexit指令時,将鎖計數器設為0,表明鎖被釋放。如果擷取對象鎖失敗,那麼目前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。

鎖優化(偏向鎖、輕量級鎖、自旋鎖、适應性自旋鎖、鎖消除、鎖粗化):

  1. 偏向鎖:偏向鎖會偏向于第一個獲得它的線程,如果在接下來的執行中,該鎖沒有被其他線程擷取,那麼持有偏向鎖的線程就不需要進行同步。偏向鎖失敗後,會更新為輕量級鎖。
  2. 輕量級鎖:輕量級鎖不是為了代替重量級鎖,它的本意是在沒有多線程競争的前提下,減少傳統的重量級鎖使用作業系統互斥量産生的性能消耗,因為使用輕量級鎖時,不需要申請互斥量。另外,輕量級鎖的加鎖和解鎖都用到了CAS操作。
  3. 自旋鎖和自适應自旋:輕量級鎖失敗後,虛拟機為了避免線程在作業系統層面挂起,還會進行一項稱為自旋鎖的優化手段。為了讓一個線程等待,我們隻需要讓線程執行一個忙循環(自旋),這項技術就叫做自旋。自适應的自旋鎖帶來的改進就是:自旋的時間不在固定了,而是和前一次同一個鎖上的自旋時間以及鎖的擁有者的狀态來決定。
  4. 鎖消除:虛拟機在運作時,如果檢測到那些共享資料不可能存在競争,那麼就執行鎖消除。
  5. 鎖粗化:我們在編寫代碼的時候,總是推薦将同步塊的作用範圍限制得盡量小,但是如果一系列的連續操作都對同一個對象反複加鎖和解鎖,那麼會帶來很多不必要的性能消耗。

繼續閱讀