JVM漫談

原文地址 http://liuzhengyang.top/2016/10/05/gossip-jvm/

title: gossip_jvm
date: 2016-10-05 12:59:01
tags:
- Java

- JVM

背景介紹

jvm已經是Java開發的必備技能了,jvm相當於Java的操作系統。
JVM,java virtual machine, 即Java虛擬機,是運行java class文件的程序。
Java代碼經過Java編譯器編譯,會編譯成class文件,一種平臺無關的代碼格式,class文件按照jvm規範,包括了java代碼運行的數據和代碼等內容。jvm加載class文件後,就可以執行java代碼了。
JVM有不同的實現,有我們熟悉的Hotspot虛擬機,JRockit等。在各個操作系統上,又回有各自的虛擬機實現,從而形成了Java代碼 > class文件 > JVM規範 > JVM實現的層次。再加上其他語言如scala、groovy也能夠生成class文件,這樣不僅實現了平臺無關性,也實現了語言無關性。

JVM體系,分爲JVM內存結構,Class文件結構,Java ByteCode,垃圾收集算法和實現,調優和監控工具,以及Java內存模型(JMM)。

JVM內存結構

通常,認爲大概分爲線程共享的區域和線程私有的區域。共享區域在JVM啓動時創建,
私有區域伴隨這線程的啓動和結束。

私有區域

一個線程擁有的結構有

程序計數器(Program Counter, PC)

Java天生支持多線程,多線程會有線程切換的問題,當一個線程從可運行狀態得到CPU調度進入運行狀態,CPU需要知道從哪裏開始執行,並且Java是一種基於棧的執行架構(區別於基於寄存器的架構)。當執行一個Java方法時,PC會指向下一條指令的位置。執行native方法時,PC是未定義。操作指令可能會有0個或多個操作數。JVM的執行流程大概可以描述爲:

while(true) {
opcode = code[pc];
oprand = getOperan(opcode);
pc = code[pc + len(oprand)];
execute(opcode, oprand);
}

Java虛擬機棧(Java Virtual Machine Stack)

Java虛擬機棧,或者叫方法棧,會伴隨這方法的調用和返回進行相應的入棧和出棧。棧的元素是棧幀(Stack Frame), 棧幀中的內容包括: 操作數棧,本地變量表,動態鏈接等信息。當線程調用一個方法的時候,會組裝對應的棧幀入棧。

本地變量表(Local Variable Table)

本地變量表存儲方法的參數、方法內部創建的局部變量。本地變量表的大小在編譯時就確定了。本地變量表會根據變量的作用範圍選擇重用一個位置。本地變量表會存放
int,char,byte,float,double,long,address(實例引用)。其中除了double和long其他變量佔用一個slot,一個slot指一個抽象的位置,在32位虛擬機中是32bit大小,
double和long佔用兩個slot。
值得注意的時,如果一個方法是實例方法,Java編譯器會將this作爲第一個參數傳入本地變量表。另外Java中面向對象,方法調用可以這樣理解

實例方法
obj.method(var1, var2, var3) => method invoke obj var1 var2 var3

操作數棧

操作數棧用於方法內執行保存中間結果,Java方法中的代碼邏輯就是通過操作數棧來實現的。和本地方法表一樣,操作數棧也是在編譯時就確定最大大小了,即最大深度。操作數棧可以和本地變量表交互,進行數據的存放和讀取。下面用一個簡單的例子展示一下。

int add(int a, int b) {
   return a + b;
}

這個實例方法經過Java編譯器編譯後生成的字節碼

本地變量表
slot0  this 
slot1  a 
slot2  b

方法字節碼
iload_1 #讀位置是1的本地變量(本地變量表從0開始,位置0是this引用)
此時操作數棧是 a
iload_2 #讀位置是2的本地變量,即b
此時操作數棧是 a b
iadd    #進行int類型的add操作,會取出棧頭的兩個元素取出進行相加並將結果入棧。
此時操作數棧是 c (相加的結果)
ireturn #ireturn指令會將棧頭元素返回給調用方法的棧幀 

線程共享區域

堆(Heap區)

創建的對象(包括普通實例和數組)都分配在Heap區(不考慮一些虛擬機的棧上分配優化技術)。在細分的話,一般還分成年輕代和老年代。這是基於這樣一個類似28原理的統計,90%多的對象都是很快成爲垃圾的對象。所以化爲成兩個區域,分別使用不同的收集算法,年輕代的收集頻率更高,所需空間也相對較小。內存分配時,多個線程會有併發問題,主要通過兩種方式解決:1.CAS加上失敗重試分配內存地址。2. TLAB, 即Thread Local Allocation Buffer, 爲每個線程分配一塊緩衝區域進行對象分配。年輕代還可以分爲兩個大小相等的Survivor和一個Eden區域。對象在幾種情況下會進入老年代:1. 大對象,超過Eden大小或者PretenureSizeThreshold. 2. 在年輕代的年齡(經歷的GC次數)超過設定的值的時候 3. To Survivor存放不下的對象

方法區

方法區存放加載的類信息和運行時常量池等。

垃圾收集(Garbage Collect)

Java中不需要對內存進行手動釋放,JVM中的垃圾回收器幫助我們回收內存。

何時進行收集

一般來說,當某個區域內存不夠的時候就會進行垃圾收集。如當Eden區域分配不下對象時,就會進行年輕代的收集。還有其他的情況,如使用CMS收集器時配置CMSInitalize

如何判斷一塊內存是垃圾

即判斷一個對象不再使用,不再使用可以是沒有有效的引用。
一般來說,主要有兩種判斷方式

引用計數

當有對象引用自身時,就會計數器加1,刪除一個引用就減一,當計數爲0時即可判斷爲垃圾。python等語言使用引用計數。引用計數存在循環引用問題,如兩個落單的A和B互相引用,但是沒有其他對象指向它們這種情況。

可達性分析

通過一些根節點開始,分析引用鏈,沒有被引用的對象都可以被標記爲垃圾對象。根節點是方法棧中的引用、常量等。

垃圾收集算法

標記清除(Mark Sweep)

對非垃圾對象進行標記都,清除其他的對象。這種方式對對內存空間造成空隙,即內存碎片,最終導致有空餘空間,但沒有連續的足夠大小的空間分配內存。

標記整理(Mark Compact)

標記非垃圾對象後,將這些對象整理好,排列到內存的開始位置。這樣內存就是整齊的了。但是因爲會造成對象移動,所以效率會有降低。

標記清除整理(Mark Sweep Compact)

即組合兩種方式,在若干次清除後進行一次整理。

複製(Copy)

劃分成兩個相同大小的區域,收集時,將第一個區域的活對象複製到另一個區域,這樣不會有內存碎片問題。但是最多隻能存放一半內存。

垃圾收集器

垃圾收集器就是垃圾收集算法的相應實現。

Serial New

新生代單線程的收集器,是Client模式默認的垃圾收集器

Parallel New

Serial New的多線程版本。ParNew常和CMS拉配使用。這裏說明一些Parallel和Concurrent即並行和併發在垃圾收集這裏的表示的不同,並行表示有多個線程同時進行垃圾收集,併發是指垃圾收集線程和應用線程可以併發執行。

Parallel Scanvenge

PS收集器是注重吞吐量(ThroughPut)的收集器。

Serial Old。

老年代的單線程收集器

Parallel Old

Serial Old的多線程版本,由於Parallel Scavenge不能和CMS搭配使用,所以會是使用PS時的一種選擇。

CMS (Concurrent Mark Sweep)

注重延遲latency的收集器,在交互式應用中,如面向用戶的web應用,需要儘可能減少垃圾收集造成的停頓時間。在總的統計上,吞吐量可能沒有PS收集器高。
細分上,CMS還分爲4個階段
* 初始標記,標記GC Root可以直達的對象。STW
* 併發標記,從第一步標記的對象開始,進行可達性分析遍歷,和應用線程併發執行。
* 重新標記,SWT,修正上一階段併發執行造成的引用變化。
* 併發清除,併發的清除垃圾
CMS使用標記清除算法,所以有內存碎片問題,可能設置參數在進行若干次不帶整理的收集後進行一次帶整理(compact)的收集。另外,因爲垃圾收集是和應用線程併發執行的,在收集的同時可能還會有垃圾不斷產生,即產生了浮動垃圾。另外還需要預留出一定空間,到達這個值後進行收集,但是還會有收集速度趕不上生產的速度,這時就會出現Concurrent Mode Failure,CMS會退化成Serial Old進行GC。

G1 (Garbage First)

具有大內存收集和目標效率時間等控制能力,目標是代替CMS。G1通過將內存劃分成不同的區域(Region),並對不同區域計算分數,分析那個Region最具有收集價值。

一些JVM的GC參數

常用的參數設置有
* -Xms=4g -Xmx=4g 設置Java堆的初始大小和最大大小均爲4g,即避免了堆大小調整
* -Xmn=1g 設置年輕代的總大小爲1g
* -SurvivorRatio=8, 設置Eden和一個Survivor的比例爲8:1
* -XX:+PringGCDetails

堆外內存(Non Heap)

Nio中的DirectByteBuffer就是堆外內存的一部分,這部分內存只能通過Full Gc進行清理。一些框架會通過System.gc調用手動觸發gc,但是在啓動參數中可能設置了禁止調用System.gc()。另外當設置堆過大時可能會造成堆外內存不夠導致OOM。

監控工具

監控工具幫助我們在運行時或問題發生後分析現場,分析內存分佈狀態,哪裏導致內存泄漏等(本該被釋放的對象仍然被引用)。

命令行工具

HotspotJVM的bin目錄下有很多可用的工具。

jps

jps
jps -l
jps -lv

即java版的ps,可以查看當前用戶啓動了哪些java進程。

jstat

pid指jps命令查看的java進程號

jstat -gcutil pid 1000 10 

jstat是一個多種用途的工具,更多需要man jstat或直接輸入jstat查看提示。

jmap

jmap可以查看內存狀況

jmap -histo:live pid
jmap 
dump下來的內存文件可以通過MAT進行分析,通過分析引用鏈等分析內存泄漏位置

jstack

查看Java線程狀況

jstack pid
jstack -F pid
可以查看線程的狀態、名稱、代碼位置

javap (Java Printer)

javap 可以用可讀的方法查看class文件內容,在遇到線上class文件問題,如NoSucheMethodError發生時,可以快速進行判斷分析。如分析一個A.class文件,查看它的私有方法和字段。

javap -p -c -v A.class

可視化工具

JVisualVM

$JAVA_HOME/bin/jvisualvm

JMC

$JAVA_HOME/bin/jmc

JConsole

$JAVA_HOME/bin/jconsole

Class文件結構

Java編譯器將Java代碼編譯成class文件格式。
其中步驟包括了我們熟悉的詞法分析將源文件轉換成token流。語法分析將token流轉換成抽象語法樹(AST)。語義分析分析語義是否正確。源代碼優化。目標代碼生成和目標代碼優化等步驟。最終得到了class文件。之後在虛擬機中,class文件可以通過解釋器解釋執行和通過即時編譯器(JIT-just in time)編譯成native代碼執行兩種方式執行。
class文件是有嚴格定義的。符合定義的class文件才能夠被JVM加載、驗證、初始化、執行。
我們通過javap可以查看一個class文件的內容。
Class文件可以分爲以下幾個部分
* Magic Number (0xCAFEBABY)
* minor version, major version 如 0x0033 代表 00,51, 是java8版本
* constant pool 常量池,常量池中包括了字段、方法、類的名稱的符號引用,符號引用會在運行時經過鏈接轉換爲直接引用。
* access flags 類的private、public等修飾詞
* this class 表明當前類的名稱
* super class 父類
* interfaces 實現的接口列表
* fields class中定義的字段,每個field又是一個結構體
* methods 方法,包括MaxLocal, Max Stack,方法名,signature,access flags等。 代碼保存在方法的名稱爲Code的屬性中。
* attributes

字節碼指令集

bytecode保存在class文件的方法的Code屬性中。用一個byte表示操作指令,所以最多有256個指令。一個指令可能會有多個操作數。
操作指令可以分爲以下幾類:
* 數學運算,如iadd,i2c,imul,idiv
* 條件分支, 如ifeq,if_icompeq, if_icmplt
* 操作數棧和本地變量表的操作,如iload_0, iconst_0, ldc i bipush 100, astore_1,
iinc, dup, swap, dup_x1,put_field, get_field, get_static, put_static等。
* class操作,如new, checkcast, instanceof
* 方法調用:1.invokespecial:調用構造器、私有方法和父類方法;2.invokestatic:調用靜態方法;3.invokevirtual:調用虛方法,一般的實例方法都是invokevirtual調用;4.invokeinterface:調用接口類的方法;5.invokedynamic,java中對動態語言的支持。
invokevirtual和invokeinterface通過第一個參數查找方法,動態分派,從而實現多態。

最後總結

以上知識是通過閱讀書籍、官方文檔、規範得來的,會有過時、不準確的情況。
還需通過查看源碼、自身探索,真像就在那代碼中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章