歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Java虛擬機基礎知識

寫在前面

之前老大讓做一些外包面試,我的問題很簡單:

  1. 介紹一下工作中解決過比較有意思的問題。
  2. HashMap使用中需要注意的點。

第一個問題主要是想了解一下對方項目經驗的含金量,第二個問題則是測試下是否知道一些細節,比如HashMap是線程不安全的、用HashMap來做緩存的話可能導致內存洩露等,自我感覺問題設計的還可以:D~ 但是看了其他同事的題目就淚崩了:

  1. 設計模式XXX
  2. 垃圾回收XXX

擦,怎麼感覺這個問題我也不會。。。

虛擬機給人的感覺像是操作系統、編譯器:非常高大上。但是Java程序就跑在上面,遇到問題還得去排查,性能不行還得去優化,基礎的知識還是需要的!

內存管理

Java虛擬機在執行的過程中會把它所管理的內存劃分為若干個不同的數據區域,大致如下:

各部分的功能如下:

區域功能 程序計數器 可以看做當前線程執行字節碼的行號 虛擬機棧 存放局部變量、操作棧等 本地方法棧 與虛擬機棧類似,不過是服務於本地方法 堆 存放對象 方法區 存放類信息、常量、靜態變量、JIT編譯後的代碼等 運行時常量池 編譯時生成的各種字面量和符號使用 直接內存 通過NIO分配的對外內存

在內存管理部分比較大的一塊內容是GC(垃圾回收),所謂垃圾回收就是將垃圾占用的內存回收掉。那麼第一個問題:什麼是垃圾?

  1. 引用計數算法:被引用次數為0的對象。
  2. 根搜索算法:從GC Roots沿著引用找不到的對象。

這裡都提到了引用,在JDK 1.2之後Java就已經對引用的概念進行了擴充,那麼第二個問題:有哪些類型的引用?

  1. 強引用:Object o = new Object()這種都是強引用。
  2. 弱引用:還有用但非必須的,在OOM之前被回收。
  3. 軟引用:更弱的引用,在下次GC的時候被回收。
  4. 虛引用:最弱的,唯一的作用是在對象被回收的時候可以收到通知。

這裡只有強引用才能對對象的生命周期造成影響。在虛擬機發展的過程中進化出不少垃圾回收算法,比如:

  1. 標記-清除算法
  2. 復制算法
  3. 標記-整理算法
  4. 分代收集算法

在實際中用到的回收器都是這幾種算法的組合,比如從VisualVM中看到的內存是這樣的(需要明白各部分都是怎樣互相配合的):

整體上來看是分代收集算法,而S0、S1這兩部分可以看做是標記-整理算法。那麼第三個問題:常見的CMS垃圾回收器的執行流程是怎樣的?

  1. 初始標記:GC Roots直接關聯的對象。
  2. 並發標記:Root Tracing。
  3. 重新標記:修復由於程序運行導致標記產生變動。
  4. 並發清除

具體如下圖所示:

可以看到只有在初始標記和重新標記的時候才需要Stop The World,其他都是和用戶線程一起執行,不要以為這就完美了,並行執行的過程會消耗掉一些CPU資源。

代碼執行

把Java源碼丟給JVM肯定是不能執行的,需要先用javac編譯成class文件才行,那麼第一個問題:class文件的結構是怎樣的?

  • 常量池
  • 訪問標志
  • 類索引、父類索引和接口索引
  • 字段表
  • 方法表
  • 屬性表

虛擬機規范並沒有規定在什麼時候要加載類,但是規定了在遇到new、反射、父類、Main的時候需要初始化完成。整個類的生命周期如下:

在虛擬機中通過ClassLoader來進行類的加載,這地方需要明白:

  • 兩個類是否相同,除了類名外還需要判斷ClassLoader是否相同。
  • 雙親委派模式並不是一個強制約束。

在類加載完成之後就可以開始執行了,和線程運轉相關的東西都放在棧幀中,其結構如下:

屬性作用/含義 局部變量表 方法參數及方法內部定義的局部變量 操作數棧 用來被指令操作 動態連接 指向運行時常量池中該棧幀所屬方法的引用 方法返回地址 上層方法調用本方法的位置 附加信息 調試信息等

執行中具體調用哪個方法是個頭疼的問題,需要處理:

  • 靜態分派:相同名稱、不同參數類型的方法。
  • 動態分派:繼承中復寫的方法。

字節碼中的指令都是基於棧的操作,比如要完成1+1這樣的計算,對應的指令如下:

iconst_1 // 將常量1壓入棧
iconst_1
iadd // 把棧頂的兩個值相加並出棧,然後把結果放回棧
istore_0 // 將棧頂的值放到局部變量表第0個Solt

解釋執行的好處是下載後啟動速度快,但是確定也非常明顯:運行速度慢。JIT正是用來解決這個問題的,能夠將多次調用的方法、多次執行的循環體編譯成本地代碼。

優化是個很好玩的題目,記得在參加一次變成比賽的時候用gcc -O3編譯之後的代碼把printf()都沒輸出了。。在JIT中比較常見的優化手段有:

手段描述 公共子表達式消除 如果一個表達式已經計算過了,那麼後面不需要重復計算 數組范圍檢查消除 並不是必須一次不漏地檢查 方法內聯 把代碼復制到調用方法中 逃逸分析 判斷對象是否可能被方法外引用到

程序執行一定會涉及到內存操作,在Java中定義了八種操作來完成:

操作含義 lock 把一個變量標識為線程獨占狀態 unlock 釋放變量 read 將變量從主存讀取到工作內存 load 將read到的變量值放入工作內存中的副本 use 將工作內存中的變量傳遞給執行引擎 assign 引擎返回的值傳遞給工作內存中的副本 store 將工作內存中的變量傳遞給主存 write 把從工作內存得到的變量寫入主存對應的變量中

這裡有必要講一下volatile的作用,在使用到的時候能明白下面兩條即可:

  • 保證變量對所有線程是可見的。
  • 禁止指令重排優化。

如果Java中所有的操作都需要程序員來控制的話,會有大量的重復代碼,而且寫起來很累,那麼我們可以通過先行發生原則來判斷並行的兩個操作是否存在沖突:

  • 程序次序規則:單線程內按照程序書寫順序。
  • 管程鎖定規則:unlock必須在lock之前。
  • volatile變量規則:寫操作先行發生於讀操作。
  • 線程啟動規則:Thread.start()先於線程的其他任意方法。
  • 線程終止規則:線程中所有的操作都先於對此線程的終止檢測。
  • 線程中斷規則:interrupt()先於中斷檢測。
  • 對象終結規則:對象的初始化完成先於它的finalize()方法。
  • 傳遞規則:如果A先於B、B先於C,那麼A先於C。

Thread的底層實現還是比較麻煩的,但是最起碼應該知道Thread的狀態是如何進行轉換:

最後,常見的同步方式是synchronized或者aqs的各種實現,這裡就不講了,因為每個都足夠寫一大篇。

 

Copyright © Linux教程網 All Rights Reserved