1. 程式人生 > >分散式Java應用基礎與實踐-筆記

分散式Java應用基礎與實踐-筆記

分散式Java應用基礎與實踐

  • 類載入機制:JVM將類載入過程分為三個步驟:裝載、連結和初始化。
  • https://img.mubu.com/document_image/67864e07-5f28-49d7-aa29-4e29dc8b459c-67246.jpg
    • 1、裝載Load
      • 裝載過程負責找到二進位制位元組碼並載入至JVM中,JVM通過類的全限定名及類載入器完成類的載入,同樣,也採用以上兩個元素來標識一個被載入了的類:類的全限定名+ClassLoader例項ID。
    • 2、連結Link
      • 連結過程負責對二進位制位元組碼的格式進行校驗、初始化裝載類中的靜態變數及解析類中呼叫的介面、類。二進位制位元組碼的格式校驗遵循JVM規範,如果格式不符合,則丟擲VerifyError;校驗過程中如果碰到要引用到其他的介面和類,也會進行載入;如果載入過程失敗,則會丟擲NoClassDefFoundError。在完成校驗後,JVM初始化類中的靜態變數,並將其值賦為預設值。最後對類中的所有屬性、方法進行驗證,以確保其要呼叫的屬性、方法存在,以及具備相應的許可權(例如public、private域許可權等)。如果這個階段失敗,可能會造成NoSuchMethodEror、NoSuchFieldError。
    • 3、初始化Initialize
      • 初始化過程即執行類中的靜態初始化程式碼、構造器程式碼及靜態屬性的初始化,在以下四種情況下初始化過程會被出發執行:
        • a. 呼叫了new;
        • b. 反射呼叫了類中的方法;
        • c. 子類呼叫了初始化;
        • d. JVM啟動過程中指定的初始化類。
      • 在執行初始化過程之前,首先必須完成連結過程中的校驗和準備階段,解析階段則不強制。
  • 為什麼我們要自定義類載入器?
    • 舉一個例子來說吧,主 流的Java Web伺服器,比如Tomcat,都實現了自定義的類載入器。因為一個功能健全的Web伺服器,要解決如下幾個問題:
    • 1、部署在同一個伺服器上的兩個Web應用程式所使用的Java類庫可以實現相互隔離。這是最基本的要求,兩個不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求一個類庫在一個伺服器中只有一份,伺服器應當保證兩個應用程式的類庫可以互相使用。
    • 2、部署在同一個伺服器上的兩個Web應用程式所使用的Java類庫可以相互共享。這個需求也很常見,比如相同的Spring類庫10個應用程式在用不可能分別存放在各個應用程式的隔離目錄中。
    • 3、支援熱替換,我們知道JSP檔案最終要編譯成.class檔案才能由虛擬機器執行,但JSP檔案由於其純文字儲存特性,執行時修改的概率遠遠大於第三方類庫或自身.class檔案,而且JSP這種網頁應用也把修改後無須重啟作為一個很大的優勢看待。
  • Java開發人員呼叫Class.forName來獲取一個對應名稱的Class物件時,JVM會從方法棧上尋找第一個ClassLoader,通常也就是執行Class.forName所在類的ClassLoader,並使用此ClassLoader來載入此名稱的類。JVM為了保護載入、執行的類的安全,它不允許ClassLoader直接解除安裝載入了的類,只有JVM才能解除安裝,在Sun JDK中,只有當ClassLoader物件沒有引用時,此ClassLoader物件載入的類才會被解除安裝。
  • 類載入方面的常見異常
    • 1、ClassNotFoundException
      • 產生這個異常的原因是在當前的ClassLoader中載入類時未找到類檔案,對位於System ClassLoader的類很容易判斷,只要載入的類不再Classpath中,而對位於User-Defined ClassLoader的類則麻煩些,要具體檢視這個ClassLoader載入類的過程,才能判斷此ClassLoader要從什麼位置載入到此類。
    • 2、NoClassDefFoundError
      • 造成此異常的主要原因是載入的類中引用到的另外的類不存在,例如要載入A,而A中呼叫了B,B不存在或當前ClassLoader沒法載入B,就會丟擲這個異常。因此,對於這個異常,須先檢視是載入那個類時報出的,然後再確認該類中引用的類是否存在於當前ClassLoader能載入到的位置。
    • 3、LinkageError
      • 該異常在自定義ClassLoader的情況下更容易出現,主要原因是此類已經在ClassLoader載入過了,重複地載入會造成該異常,因此要注意避免在併發的情況下出現這樣的問題。
      • 由於JVM的這個保護機制,使得在JVM中沒辦法直接更新一個已經load的Class,只能建立一個新的ClassLoader來載入更新的Class,然後將新的請求轉入該ClassLoader來獲取類,這也是JVM中不要實現動態更新的原因之一,而其他更多的原因時物件狀態的複製、依賴的設定等。
    • 4、ClassCaseException
      • 該異常有多種原因,在JDK 5支援泛型後,合理使用泛型可相對減少此異常的觸發。這些原因中比較難查的時兩個A物件由不同的ClassLoader載入的情況,這時如果將其中某個A物件造型成另外一個A物件,也會報出ClassCaseException。
  • 類執行機制
    • 在完成將class檔案資訊載入到JVM併產生Class物件後,就可執行Class物件的靜態方法或例項化物件進行呼叫了。在原始碼編譯階段將原始碼編譯為JVM位元組碼,JVM位元組碼時一種中間程式碼的方式,要由JVM在執行期對其進行解釋並執行,這種方式稱為位元組碼解釋執行方式。
    • Sun JDK基於棧的體系結構來執行位元組碼,基於棧方式的好處為程式碼緊湊,體積小。
    • 執行緒在建立後,都會產生程式計數器(PC)和棧(Stack);PC存放了下一條要執行的指令在方法內的偏移量;棧中存放了棧幀(StackFrame),每個方法每次呼叫都會產生棧幀。棧幀主要分為區域性變數區和運算元棧兩部分,區域性變數區用於存放方法中的區域性變數和引數,運算元棧中用於存放方法執行過程中產生的中間結果,棧幀中還會有一些雜用空間,例如只想方法已解析的常量池的引用、其他一些VM內部實現需要的資料等。
  • 物件引用關係
    • 強引用
    • A a = new A();就是一個強引用,強引用的物件只有在主動釋放引用後才會被GC。
    • 軟引用
    • 軟引用採用SoftReference來實現,採用軟引用來建立引用的物件,在JVM記憶體不足時會被回收,因此很適合用於實現快取。另外,當GC認為掃描到的SoftReference不經常使用時,也會進行回收。
    • 弱引用
    • 弱引用採用WeakReference來實現,採用弱引用建立引用的物件沒有強引用後,GC時即會被自動釋放。ThreadLocal的實現。
    • 虛引用
    • 虛引用採用PhantomReference來實現,採用虛引用可追蹤到物件是否已從記憶體中被刪除。
  • CPU消耗分析
    • a. 上下文切換
      • 每個CPU(或多核CPU中的每核CPU)在同一時間只能執行一個執行緒,Linux採用的是搶佔式呼叫。即為每個執行緒分配一定的執行時間,當到達執行時間、執行緒中有IO阻塞或高優先順序程序要執行時,Linux將切換執行的執行緒,在切換時要儲存目前執行緒的執行狀態,並恢復要執行的執行緒的狀態,這個過程就稱為上下文切換。對於Java應用,典型的是在進行檔案IO操作、網路IO操作、鎖等待或執行緒Sleep時,當前執行緒會進入阻塞或休眠狀態,從而觸發上下文切換,上下文切換過多會造成核心佔據較多的CPU使用,使得應用的響應速度下降。
    • b. 執行佇列
      • 每個CPU核都維護了一個可執行的執行緒佇列,例如一個4核的CPU,Java應用中啟動了8個執行緒,且這8個執行緒都處於可執行狀態,那麼在分配平均的情況下每個CPU中的執行佇列裡就會有兩個執行緒。通常而言,系統的load主要由CPU的執行佇列來決定,假設以上狀況維持了一分鐘,那麼這一分鐘內系統的load就是2,但由於load是個複雜的值,因此也不是絕對的,執行佇列值越大,就意味著執行緒會要消耗越長時間才能執行完。
    • c. 利用率
      • 在Linux中,可通過top命令檢視CPU的消耗情況。
      • 對於Java應用而言,CPU消耗嚴重主要體現在us、sy兩個值上。
      • us
        • 當us值過高時,表示執行的應用消耗了大部分CPU,在這種情況下,對於Java應用而言,最重要的是找到具體消耗CPU的執行緒所執行的程式碼。
      • sy
        • 當sy值高時,表示Linux花費了更多的時間在進行執行緒切換,Java應用造成這種現象的主要原因時啟動的執行緒比較多,且這些執行緒多數都處於不斷的阻塞(例如鎖等待、IO等待狀態)和執行狀態的變化過程中,這就導致了作業系統要不斷地切換執行的執行緒,產生大量的上下文切換。
  • CPU消耗高的解決方法
    • 1CPU us高的解決方法
      • CPU us高的原因主要是執行執行緒無任何掛起動作,且一致執行,導致CPU沒有機會去排程執行其他的執行緒,造成執行緒餓死的現象。對於這種情況,常見的一種優化方法時對這種執行緒的動作增加Thread.sleep,以釋放CPU的執行權,降低CPU的消耗。
      • 在實際的Java應用中會有很多類似的場景,例如多執行緒的任務執行管理器,它通常要通過掃描任何集合列表來執行任務。對於類似的場景,都可通過增加一定的sleep時間來避免消耗過多的CPU。
      • 除了上面的場景外,還有一種經典的場景時狀態的掃描,例如某執行緒要等其他執行緒改變了值後才可繼續執行。對於這種場景,最佳的方式是改為wait/notify機制。
      • 對於其他類似迴圈次數太多、正則、計算等造成的CPU us過高的情況,則要結合業務需求來進行調優。
      • 對於GC頻繁造成的CPU us高的現象,則要通過JVM調優或程式調優,降低GC的執行次數。
    • 2CPU sy高的解決方法
      • CPU sy高的原因主要是執行緒的執行狀態要經常切換,對於這種情況,最簡單的優化方法是減少執行緒數。減少執行緒數是能讓sy值下降的,所以不是執行緒數越多吞吐量就越高,執行緒數需要設定為合理的值。
      • 造成CPU sy高的原因除了啟動的執行緒過多以外,還有一個重要的原因是執行緒之間的鎖競爭激烈,造成了執行緒狀態經常要切換,因此儘可能降低執行緒之間的鎖競爭也是常見的優化方法。鎖競爭降低後,執行緒的狀態切換次數也就會下降,sy值會相應下降。但值得注意的是,如果執行緒數過多,調優後可能造成us值過高,所以合理地設定執行緒數非常關鍵。鎖競爭更有可能造成系統資源消耗不多,但系統性能不足的現象。
  • 協程
    • 對於分散式Java應用而言,還有一種典型的現象是應用中由較多的網路IO操作或確實需要一些鎖競爭機制(例如資料庫連線池),但為了能夠支援高的併發量,在Java應用中只能藉助啟動更多的執行緒來支撐。在這樣的情況下當併發量增長到一定程度後,可能會造成CPU sy高的現象,對於這種現象,可採用協程來支撐更高的併發量,避免併發量上漲後造成CPU sy消耗嚴重、系統load迅速上漲和系統性能下降。
    • 在目前的Sun JDK中,建立並啟動一個Thread物件就意味著運行了一個原生執行緒,當這個執行緒中由任何阻塞動作(例如同步檔案IO、同步網路IO、鎖等待、Thread.sleep等)時,這個執行緒就會被掛起,但仍然佔據著執行緒的資源。當執行緒中的阻塞動作完成時,由作業系統來恢復執行緒的上下文,並呼叫執行,這是一種標準的遵循目前作業系統的實現方式,這種方式對於Java應用而言,當併發量上漲後,有可能出現的現象是啟動的大量執行緒都處於浪費狀態。例如一個執行緒在等待資料庫執行結果的返回,如這個資料庫執行操作需要花費2秒,那麼就意味著這個執行緒資源被白白佔用了2秒,一方面導致了其他的請求只能是放在緩衝佇列中等待執行,效能下降;另一方面是造成系統中執行緒切換頻繁,CPU執行佇列過長,協程要改變的就是不浪費相對昂貴的原生執行緒資源。
  • 檔案IO消耗嚴重的解決方法
    • 1、非同步寫檔案
      • 將寫檔案的同步動作改為非同步動作,避免應用由於寫檔案慢而效能下降太多,例如寫日誌,可以使用log4j提供的AsyncAppender。
    • 2、批量讀寫
      • 頻繁的讀寫操作對IO消耗會很嚴重,批量操作將大幅度提升IO操作的效能。
    • 3、限流
      • 頻繁讀寫的另外一個調優方式是限流,從而將檔案IO消耗控制在一個能接受的範圍。
  • 網路IO消耗嚴重的解決方法
    • 造成網路IO消耗嚴重的主要原因是同時需要傳送或接收的包太多。對於這類情況,常用的調優方法為進行限流,限流通常是限制傳送packet的頻率,從而在網路IO消耗可接受的情況下來發送packet。
  • 對於記憶體消耗嚴重的情況
    • 1、釋放不必要的記憶體
      • 記憶體消耗嚴重的情況中最典型的一種現象是程式碼中持有了不需要的物件引用,造成這些物件無法被GC,從而佔據了JVM堆記憶體。這種情況最典型的一個例子是在複用執行緒的情況下使用ThreadLocal,由於執行緒複用,ThreadLocal中存放的物件如未做主動釋放的話則不會被GC。
    • 2、使用物件快取池
    • 3、採用合理的快取失效演算法(FIFO、LRU、LFU等)
    • 4、合理使用SoftReference和WeakReference
  • 對於資源消耗不多,但是程式執行慢的情況
    • 1、鎖競爭激烈
      • a. 使用並法包中的類
      • b. 使用Treiber演算法非阻塞棧演算法
      • c. 使用Michael-Scott非阻塞佇列演算法
      • d. 儘可能少用鎖
      • e. 拆分鎖
      • f. 去除讀寫操作的互斥鎖
    • 2、未充分使用硬體資源
      • a. 未充分使用CPU(在能並行處理的場景中未使用足夠的執行緒)
      • b. 未充分使用記憶體
        • 未充分使用記憶體的場景非常多,如資料的快取、耗時資源的快取(例如資料庫連線的建立、網路連線的建立)、頁面片段的快取等。但也要避免記憶體資源的過度使用,在記憶體資源可接受、GC頻率及系統結構(例如叢集環境可能會帶來快取的同步等)可接受的情況下,應充分使用記憶體來快取資料,提升系統的效能。