1. 程式人生 > 程式設計 >JAVA基礎知識複習-JVM篇

JAVA基礎知識複習-JVM篇

簡介

JVM(Java Virtual Machine)是執行Java位元組碼的虛擬機器器,由一套位元組碼指令集、一組程式暫存器、一個虛擬機器器棧、一個虛擬機器器堆、一個方法區和一個垃圾回收器構成。

過程

外部編譯器將.java(原始檔)編譯成.class(位元組碼檔案),類載入子系統載入.class檔案,資料存入執行時資料區,即時編譯器(實現跨平臺)將.class檔案程式設計成機器碼,機器碼通過本地介面庫呼叫作業系統本地方法庫執行程式

記憶體模型

類比物理機,在JVM中,執行緒引擎對標CPU,工作記憶體對標快取記憶體,主存對主存。當多個處理器同時對Java主存進行操作的時候就會發生不一致現象。單應用下,這種不一致可以由Java並併發包提供的方法解決。例如AtomicLong,底層是通過CAS(compareAndSwap)來實現執行緒的同步,是在一個死迴圈內不斷的嘗試修改目標的值,直到修改成功。分散式下則可以通過redis等元件來完成。

記憶體結構

執行緒共享區與JVM共存亡,執行緒私有區域與執行緒共存亡,堆外記憶體可以避免Java堆和Native堆之間來回複製的效能開銷,在併發程式設計中被廣泛應用。

元件概要

  • 程式計數器:執行緒私有,記錄實時位元組碼指令地址,執行Native則為空,唯一沒有Out Of Memory的區域
  • 棧:執行緒私有,每個⽅法在執⾏時都會床建立⼀個棧幀(Stack Frame)⽤於儲存區域性變量表 、 運算元棧 、 動態連結 、 ⽅法出⼝等資訊。每⼀個⽅法從調⽤直⾄執⾏結束,就對應著⼀個棧幀從虛擬機器器棧中⼊棧到出棧的過程
  • 本地方法區:執行緒私有,類似棧,區別是執行Native方法
  • 堆:執行緒共享,存放建立的物件和產生的資料
  • 方法區:執行緒共享,儲存已被虛擬機器器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,JDK1.8之後成為元資料區,原方法區的常量池和靜態變數存在堆中,元資料區則在堆外記憶體!!!!!

常量池

#string pool中存的是引⽤值⽽不是具體的例項物件,具體的例項物件是在堆中開闢的⼀塊空間存放的
#在堆中會有⼀個”abc”例項,全域性StringTable中存放著”abc”的⼀個引⽤值
String str1 = "abc";
String str2 = "abc";
#如果常量池沒有“abc”則⽣成兩個例項,⼀個是”abc”的例項物件,並且StringTable中儲存⼀個”abc”的引⽤值,還有⼀個是new出來的⼀個”abc”的例項物件
String str3 = new String("abc"); String str4 = str2.intern(); System.out.println(str1==str2);//true System.out.println(str1==str3);//false System.out.println(str1==str4);//true 複製程式碼

垃圾回收

物件已死判定⽅法

  • 引用計數法:物件新增一個引用,引用計數+1,反之-1,為0則認為可以回收。存在迴圈引用問題
  • 可達性分析:以一系列GC Roots的點作為起點向下搜尋,當一個物件到任何一個GC Roots都沒有引用鏈則認為可以回收(兩次標記)。

垃圾回收演演算法

  • 複製演演算法:分為兩個區域,只用一個區域,每次將存活的放入另一個區域。記憶體浪費問題
  • 標記清除演演算法:效率低,記憶體碎片問題
  • 標記整理演演算法:標記清除的基礎上,將物件放在記憶體的一端
  • 分代收集演演算法
    • 新生代:複製演演算法/...
    • 老年代: 標記清除/標記整理/...

進入老年代的途徑

  • 複製演演算法S區放不下
  • 物件過大,XX:PretenureSizeThreshold設定,一般為2KB~128KB
  • 存活時間過久,XX:MaxTenuringThreshold設定,預設15(15次GC仍然存活)

垃圾收集器

  • 新生代
    • Serial:單執行緒,複製演演算法,Client預設
    • ParNew:Serial多執行緒版(預設CPU等值執行緒數),Server預設
    • Parallel Scavenge:多執行緒,複製演演算法,吞吐量(GC時間/總時間)
  • 老年代
    • CMS:標記清除,初始標記和重新標記需要STW,其他過程不需要,為了實現最短垃圾回收停頓時間
    • Serial Old:單執行緒,標記整理,Client預設
    • Parallel Old:多執行緒,標記整理
    • G1:標記整理,記憶體分割槽,通過優先順序列表回收垃圾最多的區域,保證吞吐量的前提下實現最短垃圾回收停頓時間。

引用型別

  • 強引⽤
    • 類似於 Object obj = new Object(); 建立的,只要強引⽤在就不回收
  • 軟引⽤
    • SoftReference 類實現軟引⽤。在系統要發⽣記憶體溢位異常之前,將會把這些物件列進回收範圍之中進⾏⼆次回收
  • 弱引⽤
    • WeakReference 類實現弱引⽤。物件只能⽣存到下⼀次垃圾收集之前。在垃圾收集器⼯作時,⽆論記憶體是否⾜夠都會回收掉只被弱引⽤關聯的物件
  • 虛引⽤
    • PhantomReference 類實現虛引⽤。⽆法通過虛引⽤獲取⼀個物件的例項,為⼀個物件設定虛引⽤關聯的唯⼀⽬的就是能在這個物件被收集器回收時收到⼀個系統通知

網路程式設計模型

阻塞I/O模型

使用者發出I/O請求->等待記憶體資料就緒(阻塞)->I/O->返回I/O結果

非阻塞I/O模型(基於輪詢)

使用者發出I/O請求->輪詢(一個selector對應一個socket)判斷記憶體資料就緒(非阻塞)->I/O->返回I/O結果

多路複用I/O模型(基於輪詢)

使用者發出I/O請求->輪詢(一個selector對應多個socket)判斷記憶體資料就緒(非阻塞)->I/O->返回I/O結果,在連線數量眾多且訊息體不大的情況下有很大的優勢

訊號驅動I/O模型(基於通知)

使用者發出I/O請求->註冊訊號函式->記憶體資料就緒後通知使用者執行緒->I/O->返回I/O結果

非同步I/O模型(基於通知)

使用者發出I/O請求->註冊訊號函式->記憶體資料就緒後且I/O完成,通知使用者執行緒->返回I/O結果

Java I/O

  • File/OutputStream/InputStream/Writer/Reader
  • Serializable

Java NIO

  • Channel:類似流,區別是Channel是雙向的
  • Buffer:Channel在檔案、網路上對資料的讀取/寫入都需要經過Buffer
  • Selector:用於檢測多個註冊的Channel上是否有I/O發生,有則進行I/O.一個Selector可對應多個Socket.
// todo 程式碼演示NIO
複製程式碼

類載入機制

類載入階段

靜態變數

# 準備階段值0,初始化階段賦值1
public static int i =1;
# 準備階段賦值1
public static final int i =1;
複製程式碼

雙親委派機制

雙親委派機制保證了類的唯一性和安全性

監控

命令

  • -XX:+PrintFlagsFinal:顯示JVM引數
java -XX:+PrintFlagsFinal -version > flags.txt
複製程式碼

我們可以通過搜尋上文MaxTenuringThreshold,預設值是15即經過15次GC仍然存活則進入老年代

  • jps:檢視Java程式

  • jinfo:檢視引數值

  • jstat:檢視JVM統計資訊(類裝載,垃圾收集,JIT編譯)【GC】

    • jstat -class(類裝載)
    • jstat -gc(垃圾收集)
      • S0C S1C S0U S1U (S0,S1的總量和使用量)
      • EC EU (Eden的總量和使用量)
      • OC OU (Old的總量和使用量)
      • MC MU (Metaspace的總量和使用量)
      • CCSC CCSU (壓縮類空間的總量和使用量)
      • YGC YGCT (YoungGC的次數和時間)
      • FGC FGCT (FullGC的次數和時間)
      • GCT (GC總時間)
  • jmap:生成堆dump檔案以供分析【堆】

  • jstack【執行緒】

    • 死鎖程式碼
        ...
        new Thread(
                new Runnable() {
                  @Override
                  public void run() {
                    try {
                      log.info("開始執行執行緒1");
                      int r = 0;
                      synchronized (r1) {
                        r = r1;
                        Thread.sleep(5000);
                        synchronized (r2) {
                          r = r2;
                        }
                      }
                      log.info("執行緒1結束");
                    } catch (Exception e) {
                      e.printStackTrace();
                    }
                  }
                })
            .start();
        new Thread(
                new Runnable() {
                  @Override
                  public void run() {
                    try {
                      log.info("開始執行執行緒2");
                      int r = 0;
                      synchronized (r2) {
                        r = r2;
                        synchronized (r1) {
                          r = r1;
                        }
                        log.info("執行緒2結束");
                      }
                    } catch (Exception e) {
                      e.printStackTrace();
                    }
                  }
                })
            .start();
        ...
    複製程式碼

    • 狀態
      • NEW:The thread has not yet started
      • RUNNABLE:The thread is executing in the JVM
      • BLOCKED:The thread is blocked waiting for a monitor lock
      • WAITING:The thread is waiting indefinitely for another thread to perform a particular action
      • TIMED_WAITING:The thread is waiting for another thread to perform an action for up to a specified waiting time
      • TERMINATED:The thread has exited

springboot actuator

springboot admin

jconsole

jvisualvm

GCEasy

下文模擬記憶體洩漏

FastThread

上文死鎖例子分析

HeapHero

下文模擬記憶體洩漏

JProfiler

MAT

記憶體溢位和記憶體洩漏

記憶體溢位

堆記憶體不夠用了,發生OOM

  • 原因
    • JVM記憶體過小
    • 程式不嚴密,產生了過多的垃圾
      • 記憶體中載入的資料量過於龐大,如一次從資料庫取出過多資料
      • 集合類中有對物件的引用,使用完後未清空,使得JVM不能回收
      • 程式碼中存在死迴圈或迴圈產生過多重複的物件實體
      • 啟動引數記憶體值設定的過小
  • 解決
    • 增加JVM的記憶體大小
    • 優化程式
  • 報錯
    • tomcat:java.lang.OutOfMemoryError: PermGen space
    • tomcat:java.lang.OutOfMemoryError: Java heap space
  • JVM記憶體引數
    • -Xms設定堆的最小值
    • -Xmx設定堆的最大值
    • -Xmn:設定新生代大小
    • -XX:NewRatio:設定新生代和老年代的比值。3,表示年輕代與老年代比值為1:3
    • -XX:SurvivorRatio:新生代中Eden區與兩個Survivor區的比值。3,表示Eden:Survivor=3:2,一個Survivor區佔整個新生代的1/5
    • -XX:MaxTenuringThreshold:設定轉入老年代的存活次數。0(預設15),則直接跳過新生代進入老年代
    • -XX:PermSize、-XX:MaxPermSize:分別設定永久代最小大小與最大大小(Java8以前)
    • -XX:MetaspaceSize、-XX:MaxMetaspaceSize:分別設定元空間最小大小與最大大小(Java8以後)
# 舉例
整個堆大小=年輕代大小+年老代大小+持久代大小
持久代大小=64m
固定新生代記憶體也就固定了老年代,官方推薦新生代佔3/8
堆最大值/最小值保持一致,避免每次垃圾回收完成後JVM重新分配記憶體
java -Xmx8g -Xms8g -Xmn3g
複製程式碼

記憶體洩漏

理應被回收的物件而沒有被回收,如用完不釋放,發生OOM

模擬事故

首先分析多次full gc後老年代記憶體沒有減少,進一步分析堆日誌,顯然map發現記憶體洩漏,無法被回收

...
  static Map<String,Student> map = new HashMap<>();
  // 先加100000,此處假定理應釋放
  @Bean
  public void put() throws Exception {
    for (int i = 0; i < 100000; i++) {
      map.put(String.valueOf(i),Student.builder().name("模擬記憶體洩露").age(1).build());
    }
  }
  // 慢慢加,System.gc()觸發gc
  @Bean
  public void test()throws Exception {
    for (int k=0;k<10;k++){
      Thread.sleep(2000);
      for (int i = 0; i < 10000; i++) {
        map.put(String.valueOf(i),Student.builder().name("模擬記憶體洩露").age(1).build());
      }
      log.info("map個數:{}",map.size());
      System.gc();
    }
  }
...
複製程式碼