1. 程式人生 > >Java虛擬機器筆記

Java虛擬機器筆記

簡要說明

本部落格整理來源

  1. 龍果學院jvm視訊學習初步認識jvm
  2. 通過書籍“深入瞭解JVM”來加深對jvm的理解
  3. 通過官網和部落格來完善零碎的其它jvm知識(引用的部落格來源大部分都有貼出地址) 本部落格未完目前只完成了前面的本部分知識視訊知識,後續待整理。

jdk、jre、jvm的關係

jdk:Java Development Kit(java開發工具集) jre:Java Runtime Environment(java執行環境) jdk:Java Virtual Machine(java虛擬機器)

jvm初體驗-記憶體溢位問題的分析和解決

記憶體溢位測試示例在這裡插入圖片描述

查詢解決問題方式

  1. 設定引數堆記憶體快照引數 命令:-XX:+HeapDumpOnOutOfMemoryError -Xms20m -Xmx20m
    說明: -XX:+HeapDumpOnOutOfMemoryError:使用了標誌-XX:+HeapDumpOnOutOfMemoryError,JVM會在遇到OutOfMemoryError時拍攝一個“堆轉儲快照”,並將其儲存在一個檔案中。 -Xms20m:最小可用記憶體 -Xmx20m:最大可用記憶體 在這裡插入圖片描述
  2. 執行 執行結果如下: 在這裡插入圖片描述 執行完後,會在專案的目錄產生一個快照檔案,如圖: 在這裡插入圖片描述
  3. 用eclipse memory工具開啟快照檔案 方式有兩種:
    1. eclipse memory外掛開啟 在這裡插入圖片描述 外掛安裝好後,開啟快照檔案,其它操作參考eclipse memory工具,eclipse memory外掛使用截圖: 在這裡插入圖片描述

用國內的站點下載快點 在這裡插入圖片描述

解壓完成後,開啟軟體介面如下: 在這裡插入圖片描述

開啟快照檔案 在這裡插入圖片描述

開啟堆記憶體樹資訊 在這裡插入圖片描述

到此基本就可以確定問題所在

JVM監控工具

  1. jdk自帶的監控工具在jdk安裝目錄的bin包下的jconsole.exe可執行程式,如圖: 在這裡插入圖片描述

  2. 雙擊選擇監控程序 在這裡插入圖片描述 這個不需要在意 在這裡插入圖片描述

  3. 開啟後介面如圖: 在這裡插入圖片描述

  4. 記憶體監控測試

    1. 測試程式碼 在這裡插入圖片描述
    2. 執行後,在jconsole程式工具欄上點選連結—>新建連結,選擇要監控的測試程式,如圖: 在這裡插入圖片描述
    3. 點選記憶體籤,即可觀察程式整個過程中各個記憶體的變化情況。

瞭解歷史

java發展歷史

java8新特性

  1. 介面的預設方法和靜態方法 Java8使用兩個新概念擴充套件了介面的定義:預設方法和靜態方法。預設方法使得開發者可以在不破壞二進位制相容性的前提下,往現存介面中新增新的方法,即不強制那些實現了該介面的類也同時實現這個新加的方法。
  2. Lambda表示式和函數語言程式設計 測試程式碼:
public class LanmbdaTest extends JFrame{
	private JButton jb;
	public LanmbdaTest() {
		this.setBounds(200, 200, 400, 200);
		this.setTitle("Lanmbda測試");
		jb = new JButton("click");
		/*jb.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				System.out.println("clicked");
			}
		});*/
		jb.addActionListener(event -> System.out.println("Hello"));//這一行程式碼與註釋程式碼作用等同
		this.add(jb);
		this.setVisible(true);
		this.setDefaultCloseOperation(EXIT_ON_CLOSE);
	}
	public static void main(String[] args) {
		new LanmbdaTest();
	}	
}
  1. Date API
  2. 重複註解
  3. 更好的型別推斷 Java8編譯器在型別推斷方面有很大的提升,在很多場景下編譯器可以推匯出某個引數的資料型別,從而使得程式碼更為簡潔。
  4. Nashorn JavaScript引擎 使用Metaspace(JEP 122)代替持久代(PermGen space)。在JVM引數方面,使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize代替原來的-XX:PermSize和-XX:MaxPermSize。

java虛擬機器

Java虛擬機器(JVM)一種用於計算機裝置的規範,可用不同的方式(軟體或硬體)加以實現。

也就是說jvm是一種規範,而實現這個規範的就是jvm的具體實現,而實現jvm規範的常見的有以下幾種

  1. Sun Classic VM

    1. 世界上的第一款商用jvm,因此具有特殊的意義。現已被淘汰
    2. 只能用純直譯器的形式來執行java程式碼
  2. Exact VM

    1. Exact Memory Management 準確式記憶體管理
    2. 編譯器和直譯器混合工作以及兩級即時編譯器
    3. 只在Solaris平臺釋出
    4. 英雄氣短
  3. HotSpot VM

    1. HotSpot的歷史
    2. 優勢
    3. 稱霸武林
  4. KVM (Kilobyte)

    1. 簡單、輕量、高度可移植
    2. 在手機平臺執行
  5. JRockit

    1. BEA
    2. 世界上最快的Java虛擬機器
    3. 專注伺服器端應用
    4. 優勢
      1. 垃圾收集器
      2. MissionControl服務套件

BEA JRockit Mission Control,用來診斷洩露並指出根本原因。該工具的開銷非常小,因此可以使用它來尋找生產環境中的系統的記憶體洩露。

BEA JRockit Mission Control,(以下簡稱JRMC)於2005年12月面世,並從JRockit R26.0.0版本開始捆綁了這個工具套件,目前最新的版本是2.0.1。它是一組以極低的開銷來監控、管理和分析生產環境中的應用程式的工具。它包括三個獨立的應用程式:記憶體洩漏監聽器(Memory Leak Detector)、JVM執行時分析器(Runtime Analyzer)和管理控制檯(Management Console)。

  1. J9 IBM IBM Technology for Java virtual Machine 簡稱IT4J
  2. Azul VM
  3. Liquid VM
  4. Dalvik Vm Dex dalvik Executalbe
  5. Microsoft JVM 微軟推出的jvm
  6. TaobaoVM 阿里為滿足自己的業務,深度定製的Java虛擬機器

**高效能JVM ** Azul VM 和Liquid VM

JVM記憶體

簡介

記憶體區劃分 執行時資料區 執行緒獨佔區:每個執行緒獨自的記憶體區 執行緒共享區:所有執行緒共享的記憶體區

程式計數器

  • 程式計數器是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器。
  • 程式計數器處於執行緒獨佔區
  • 如果程式執行的是Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址。如果正在執行的是native方法,這個計數器的值為undefined
  • 此區域是唯一一個在java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域

Java虛擬機器棧

  • 虛擬機器棧描述的是Java方法執行的動態記憶體模型
  • 棧幀
    • 每個方法執行,都會建立一個棧幀,伴隨著方法從建立到執行完成。用於儲存區域性變量表,運算元棧、動態連結、方法出口等。
  • 區域性變量表
    • 存放編譯期可知的各種基本資料型別,引用型別,returnAddress型別
    • 區域性變量表的記憶體空間在編譯期完成分配,當進入一個方法時,這個方法需要在幀分配多少記憶體是固定的,在方法執行期間是不會改變區域性變數的大小
  • 大小
    • StackOverflowError: 在單執行緒操作中,無論是棧深度無限增加,還是棧幀(每個方法呼叫執行時都會在棧中建立一個棧幀,用來儲存區域性變數,運算元棧,動態連結串列,方法出口等資訊)佔的空間太大,都出現的是StackOverflowError
    • OutOfMemory:不斷建立新的執行緒的實踐中會出現OutofMemoryError的錯誤

本地方法棧

基本上都與Java虛擬機器棧相同,不同點在於:

  • 虛擬機器棧為虛擬機器執行Java方法服務
  • 本地方法棧為虛擬機器執行native方法服務 HotSpotVM 中本地方法棧和虛擬機器棧合併為一個方法棧。

堆記憶體

  • 存放物件例項
  • 垃圾收集器管理的主要區域
  • 新生代、老年代、Eden空間
  • OutOfMemory
  • Xmx -Xms(修改堆記憶體最大和最新記憶體空間引數)

方法區

  • 儲存虛擬機器載入的類資訊、常量、靜態常量,即時編譯器編譯後的程式碼等資料
    • 類的版本、欄位、方法、介面等。
  • 方法區和永久代
  • 垃圾回收在方法區的行為
  • 異常的定義
    • OufOfMemoryError

直接記憶體和執行時常量池

  • 直接記憶體
  • 常量池
    • 在JDK6.0及之前版本,字串常量池是放在Perm Gen區(也就是方法區)中;
    • 在JDK7.0版本,字串常量池被移到了堆中了。至於為什麼移到堆內,大概是由於方法區的記憶體空間太小了。
    • 在HotSpot VM裡實現的string pool功能的是一個StringTable類,它是一個Hash表,預設值大小長度是1009;這個StringTable在每個HotSpot VM的例項只有一份,被所有的類共享。字串常量由一個一個字元組成,放在了StringTable上。
    • 在JDK6.0中,StringTable的長度是固定的,長度就是1009,因此如果放入String Pool中的String非常多,就會造成hash衝突,導致連結串列過長,當呼叫String#intern()時會需要到連結串列上一個一個找,從而導致效能大幅度下降;

測試程式碼:

public class Test4 {
	public static void main(String[] args) {
		String s1 = "abc";
		String s2 = "abc";
		System.out.println(s1 == s2);
		String s3 = new String("abc");
		System.out.println(s1 == s3);
		
		System.out.println(s1 == s3.intern());
	}
}

執行結果

true
false
true

String.intern()是一個Native方法,它的作用是:如果字元常量池中已經包含一個等於此String物件的字串,則返回常量池中字串的引用,否則,將新的字串放入常量池,並返回新字串的引用’

上面的s1和s2字串都是引用的字串常量,而字串常量是放到常量池中的,也就是s1和s2在虛擬機器棧中指向的是常量池裡的"abc"字串,而常量池中對於同一個字串,只會維護一份,因此s1和s2引用的是常量池中的同一個地址,而通過new建立的字串是存放在堆記憶體中的(不在常量池內),因此s1≠s3,而執行String.intern()後,s1和s3都是引用的常量池中的字串地址,所以相等。

物件在記憶體中的佈局

  • 物件的建立

    • 建立流程 在這裡插入圖片描述
    • 給物件分配記憶體
      • 指標碰撞
      • 空閒列表
    • 執行緒安全性問題
      • 執行緒同步
      • 本地執行緒分配緩衝 TLAB
    • 初始化物件
  • 物件的結構

    • Header (物件頭)
      • 自身執行時的資料(Mark Word)
        • 雜湊值、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳
      • 型別指標 確定物件是哪一個類的例項 在這裡插入圖片描述
    • InstanceData
    • Padding:填充佔位的作用

深入理解物件的訪問定位

  • 使用控制代碼: 定義: Java堆中將會劃分出一塊記憶體來作為控制代碼池,refenerce中儲存的就是物件的控制代碼的地址,而控制代碼中包 含了物件例項資料與型別資料各自的具體地址資訊 優點 : 最大的好處就是reference中儲存的是穩定的控制代碼的地址,在物件被移動(垃圾回收時移動物件是很常見的行為)時只會改變控制代碼中的例項資料的地址,而reference本身不需要修改 在這裡插入圖片描述
  • 直接指標: 定義 : reference中儲存直接物件的地址,但是必須考慮放置訪問型別資料的相關資訊 優點 : 訪問速度快,節省了一次指標定位的時間開銷 在這裡插入圖片描述

注:HotSpot使用第2種方法,但是使用控制代碼的方法也很常見

記憶體分配

  • 記憶體分配策略

    • 優先分配到eden
    • 大物件直接分配到老年代
      • -XX:PretenureSizeThreshold:用來指定物件大小超過多少被判定為大物件
    • 長期存活的物件分配到老年代
      • -XX:MaxTenuringThreshold:用來指定存活超過多少年齡(物件GC存活次數)被分配到老年代中
    • 空間分配擔保
      • -XX:+HandlePromotionFailure:可以用該引數關閉分配擔保(+:開啟、-:關閉)
    • 動態物件年齡判斷
  • 逃逸分析與棧上分配

    • 逃逸分析:分析物件的作用域
    • 棧上分配:如果物件的作用域僅存在於當前方法中,那兒麼該物件就會被分配到棧幀記憶體上

垃圾回收

概述

  • 如何判定物件為垃圾物件
    • 引用計數法
    • 可達性分析法
  • 如何回收
    • 回收策略
      • 標記-清除演算法
      • 複製演算法
      • 標記-整理演算法
      • 分代收集演算法
    • 垃圾回收器
      • Serial
      • Parnew
      • Cms
      • G1
  • 何時回收

如何判定物件為垃圾物件

引用計數演算法

  • 在物件新增一個引用計數器,當有地方引用這個物件的時候,引用計數器的值就+1,當引用失效的時候,計數器的值就-1
  • 列印垃圾回收日誌資訊引數
    • -verbose:gc :列印簡略的垃圾回收日誌資訊
    • -xx:+PrintGCDetail :配合上面的命令,可以列印詳細的垃圾回收日誌資訊

可達性分析演算法

  • 用GCRoots的物件,一個一個判斷堆記憶體中有哪些物件被引用,堆記憶體中剩下沒有被引用的就會被垃圾回收機制回收
  • 作為GCRoots的物件
    • 虛擬機器棧
    • 方法區的類屬性所引用的物件
    • 方法區中常量所引用的物件
    • 本地方法棧中引用的物件

回收策略

標記清除演算法

  • 要回收的物件被標記,垃圾回收執行緒執行時,清除被標記的物件
  • 問題
    • 效率問題
    • 空間問題(使用時間越長,剩餘的空間越零碎)

複製演算法

  • 將記憶體分割槽,垃圾回收完後的物件被重新放到一個空閒記憶體中以此保證記憶體地址連續,但是存在記憶體浪費問題。
    • 新生代
      • Eden 伊甸園
      • Survivor 存活區
      • Tenured Gen
    • 老年代
  • 方法區
  • 棧、 本地方法棧、程式計數器

標記整理演算法和分代收集演算法

  • 標記整理演算法
    • 需要回收的物件被標記後移到垃圾回收區的記憶體區,GC執行緒執行時,清空垃圾回收記憶體區的所有物件
  • 分代收集演算法
    • 將標記整理演算法和複製演算法混合使用的演算法

垃圾回收器

Serial收集器

  • 最基本、發展最悠久
  • 單執行緒垃圾收集器(使用者執行緒和GC執行緒併發進行,也就是說在某一個時間點,執行的要麼是GC執行緒,要麼是使用者執行緒,不能同時存在)
  • 桌面應用

Parnew收集器

  • 與Serial收集器類似,只是GC執行緒由單執行緒程式設計多個GC執行緒同時執行,以達到縮短GC處理時間

Parallel Scavenge收集器

  • 複製演算法(新生代收集器)
  • 多執行緒收集器
  • 達到可控制的吞吐量
    • 吞吐量:CPU使用者執行使用者程式碼的時間 與 CPU消耗的總時間的比值
    • 吞吐量:(執行使用者程式碼時間)/(執行使用者程式碼時間+垃圾回收所佔用的時間)
  • -XX:MaxGCPauseMillis:垃圾收集器停頓時間(毫秒)
  • -XX:GCTimeRatio:吞吐量大小
    • 範圍:0~99
    • 預設:99

CMS收集器Concurrent Mark Sweep

  • 工作過程
    • 初始標記
    • 併發標記
    • 重新標記
    • 併發清理
  • 優點
    • 併發收集
    • 低停頓
  • 缺點
    • 佔用大量的CPU資源
    • 無法處理浮動的垃圾
    • 出現Concurrent Mode Failure
    • 空間碎片

G1 收集器

  • 歷史
  • 優勢
    • 並行與併發
    • 分代收集
    • 空間整合
    • 可預測的停頓
  • 步驟
    • 初始標記
    • 併發標記
    • 最終標記
    • 篩選標記
  • 與CMS比較

虛擬機器工具

介紹

  • jdk自帶的有虛擬機器工具包,在jdk根目錄下的bin目錄中,裡面很多工具都依賴了lib包中的tools.jar包,需要時,專案中可以匯入該jar包在專案中使用虛擬機器工具包。
  • 需要介紹以下工具
    • Jps
    • Jstat
    • Jinfo
    • Jmap
    • Jhat
    • Jstack
    • Jconsole

Jps

  • Jps:Java process status (Java程序狀態)
  • 本地虛擬機器唯一id:lvmid(local virtual machine id)
  • jps -m:執行時傳入主類的引數
  • jps -v:虛擬機器引數
  • jps -l:執行的主類全名或者jar包名稱

Jstat

  • 類裝載、記憶體、垃圾收集、jit編譯的資訊
  • 元空間的本質和永久代類似,都是對jvm規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制

Jinfo

  • 實時檢視和調整虛擬機器的各項引數

Jmap

  • 堆快照資訊

Jhat

  • Jhat:JVM heap analysis tool(JVM堆分析工具)

Jstack

  • 用於生成當前虛擬機器的執行緒快照(定位執行緒問題)

Jconsole

  • 記憶體執行緒等視覺化監控工具
  • 記憶體監控
  • 執行緒監控 使用示例 示例一:
public class ThreadTest {

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		sc.next();
	}

}

  1. 啟動後,用Jconsole工具進入該方法的執行緒監控,步驟參考上面(點選跳轉
  2. 可以發現在main執行緒發生阻塞,需要手動輸入值,如圖 在這裡插入圖片描述
  3. 控制檯輸入值後,main執行緒執行完成,jconsole就會提示執行緒連線斷開,如圖: 在這裡插入圖片描述

示例二: 測試程式碼

public class ThreadTest {

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		sc.next();

		new Thread(new Runnable() {
			@Override
			public void run() {
				while (true) {
				}
			}
		},"while true").start();
		
		sc.next();
		testWait(new Object());
		
		

	}

	private static void testWait(Object object) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (object) {
					try {
						object.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		},"wait").start();
	}

}

  1. 執行程式碼,用jconsole連線測試程式碼執行緒,介面如下 在這裡插入圖片描述

  2. 控制檯隨便輸入值後,執行了while true執行緒,點開while true執行緒檢視狀態,如圖: 在這裡插入圖片描述

  3. 控制檯再次輸入值後,就會又啟動一個wait執行緒,wait執行緒狀態如圖: 在這裡插入圖片描述

此時main執行緒已經執行完成,線上程列表中是找不到的。

示例三,死鎖: 示例程式碼

//類1
public class MainTest {

	public static void main(String[] args) {
		Object obj1 = new Object();
		Object obj2 = new Object();
		
		new Thread(new DeadLock(obj1, obj2)).start();
		new Thread(new DeadLock(obj2, obj1)).start();
		
	}
}

//類2
public class DeadLock implements Runnable{

	private Object obj1;
	private Object obj2;
	
	public DeadLock(Object obj1, Object obj2) {
		this.obj1 = obj1;
		this.obj2 = obj2;
	}
	
	
	@Override
	public void run() {
		synchronized (obj1) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (obj2) {
				System.out.println("Hello world!");
			}
		}
	}

}
  1. 啟動main方法後,會發現執行緒死鎖,jconsole介面顯示內容如下: 在這裡插入圖片描述 為了方便對比,我開了三個視窗觀察,左一看的是執行緒一的狀態,中間看的是執行緒二的狀態資訊,右一是點選下面的檢測死鎖之後檢測出來的死鎖執行緒。

VisualVM

介面如圖: 在這裡插入圖片描述

jdk目錄下也自帶該工具,如圖: 在這裡插入圖片描述 工具介面 在這裡插入圖片描述

效能調優案例實戰

案例一

在這裡插入圖片描述

  • 問題:經常有使用者反映長時間出現卡頓的現象。
  • 處理思路
    • 優化sql
    • 監控CPU
    • 監控執行緒
    • 監控記憶體
      • Full GC 20~30S
  • 總結經驗
    • 問題的根本原因:堆記憶體比較大的時候,老年代記憶體也比較大,裡面的有許多的大物件並且資源也多,Full GC回收老年代記憶體時費時
    • 解決方案:部署多個web容器,減小每個web容器的堆記憶體,這樣就減小了Full GC的老年代回收時間。

案例二

在這裡插入圖片描述

  • 問題:不定期記憶體溢位,把堆記憶體加大也無濟於事。匯出堆轉儲快照資訊,沒有任何資訊。記憶體監控,正常。
  • 處理思路
    • 定位問題:通過長時間的監控,監控到NIO(NIO是申請堆外的機器記憶體,而專案中機器記憶體較小,在申請堆外記憶體時,如果申請不到足夠的堆外記憶體,就會出現記憶體溢位)的一個StringBuffer引起的記憶體溢位。

認識類的檔案結構

Class檔案簡介和發展歷史

Class檔案結構概述

  • Class檔案是一組以8為位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊的排列在Class檔案之中,中間沒有新增任何分隔符,整個Class檔案中儲存的內容幾乎全部是程式執行的必要資料,沒有空隙存在。
  • 當遇到8位位元組以上的空間的資料項時,則會按照高位在前的方式分割成若干個8位位元組進行儲存。
  • Class檔案中有兩種資料型別,分別是無符號數和表。
  • Class檔案結構
    1. 魔數
    2. Class檔案版本
    3. 常量池
    4. 訪問標誌
    5. 類索引、父類索引、介面索引集合
    6. 欄位表集合
    7. 方法表集合
    8. 屬性表集合 在這裡插入圖片描述

Class檔案設計理念以及意義

  • 執行在jvm之上的語言:Clojure、groovy、JRuby、Jython、Scala

檔案結構

魔數

工具及準備

  • 工具準備
    1. 開啟class檔案,介面如下: 在這裡插入圖片描述

    2. 測試和編譯demo程式碼 3.1. 編寫helloworld測試程式碼 3.2. eclipse Terminal編譯helloworld,如圖: 在這裡插入圖片描述 注意:測試的時候不要加package,否則會如下報錯: 在這裡插入圖片描述 去掉包名後測試成功, 如圖: 在這裡插入圖片描述

    3. win10自帶的進位制轉換計算器 4.1. win+r 輸入calc 開啟後選單上選擇程式設計師,介面如圖: 在這裡插入圖片描述

class檔案的前四位表示魔數(固定的),第二個四位表示版本,如圖 在這裡插入圖片描述

不同的版本編號

在這裡插入圖片描述 將16進位制的34轉換為10進製為52,如圖: 在這裡插入圖片描述

常量池

在這裡插入圖片描述

訪問標誌

類索引

欄位表集合

方法表集合

屬性表集合

位元組碼指令簡介

  • java虛擬機器的指令由一個位元組長度的,代表著某種特定操作含義的數字,稱之為操作碼,以及跟隨其後的零至多個代表次操作所需引數的運算元而構成。
  • 操作碼的長度為1個位元組,因此最大隻有256條
  • 基於棧的指令集架構

位元組碼與資料型別

類載入機制

位元組碼執行引擎

虛擬機器編譯及執行時優化

java執行緒高階