建立了一個技術類公衆號: 一塊兒源碼分析,裏面會分享最新的開源代碼、源碼解讀、開發技巧等,歡迎你們關注。 java
JVM已是Java開發的必備技能了,JVM至關於Java的操做系統。python
JVM,java virtual machine, 即Java虛擬機,是運行java class文件的程序。web
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)。 vim
<!-- more -->數組
一般,認爲大概分爲線程共享的區域和線程私有的區域。共享區域在JVM啓動時建立, 私有區域伴隨這線程的啓動和結束。緩存
一個線程擁有的結構有安全
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虛擬機棧,或者叫方法棧,會伴隨這方法的調用和返回進行相應的入棧和出棧。棧的元素是棧幀(Stack Frame), 棧幀中的內容包括: 操做數棧,本地變量表,動態連接等信息。當線程調用一個方法的時候,會組裝對應的棧幀入棧。
本地變量表存儲方法的參數、方法內部建立的局部變量。本地變量表的大小在編譯時就肯定了。本地變量表會根據變量的做用範圍選擇重用一個位置。本地變量表會存放 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區(不考慮一些虛擬機的棧上分配優化技術)。在細分的話,通常還分紅年輕代和老年代。這是基於這樣一個相似28原理的統計,90%多的對象都是很快成爲垃圾的對象。因此化爲成兩個區域,分別使用不一樣的收集算法,年輕代的收集頻率更高,所需空間也相對較小。內存分配時,多個線程會有併發問題,主要經過兩種方式解決:1.CAS加上失敗重試分配內存地址。2. TLAB, 即Thread Local Allocation Buffer, 爲每一個線程分配一塊緩衝區域進行對象分配。年輕代還能夠分爲兩個大小相等的Survivor和一個Eden區域。對象在幾種狀況下會進入老年代:1. 大對象,超過Eden大小或者PretenureSizeThreshold. 2. 在年輕代的年齡(經歷的GC次數)超過設定的值的時候 3. To Survivor存放不下的對象
方法區存放加載的類信息和運行時常量池等。
在Java應用中不須要也不能經過代碼對內存進行手動釋放,JVM中的垃圾器幫助咱們自動回收沒有程序引用的對象。除了進行內存釋放,JVM還需對內存進行整理,由於有內存碎片的問題。GC的優勢是加快開發效率,不須要關心內存釋放,而且避免了不少內存安全問題。缺點是會帶來性能損耗。 GC必需要作兩件事情,找出垃圾對象和回收它們的內存。
通常來講,當某個區域內存不夠的時候就會進行垃圾收集。如當Eden區域分配不下對象時,就會進行年輕代的收集。還有其餘的狀況,如使用CMS收集器時配置CMSInitiatingOccupancyFraction設置何時觸發Old區的回收。
即判斷一個對象再也不使用,再也不使用能夠是沒有有效的引用。 通常來講,主要有兩種判斷方式
當有對象引用自身時,就會計數器加1,刪除一個引用就減一,當計數爲0時便可判斷爲垃圾。python等語言使用引用計數。引用計數存在循環引用問題,如兩個落單的A和B互相引用,可是沒有其餘對象指向它們這種狀況.
經過一些根節點開始,分析引用鏈,沒有被引用的對象均可以被標記爲垃圾對象。根節點是方法棧中的引用、常量等。根節點集合和具體的實現相關,可是會包括: 線程棧幀中的本地變量和操做數棧中的對象引用,靜態變量、常量以及已經加載的類的常量池中的隊形引用等。全部可以經過引用鏈引用到的對象都被認爲是活對象。 JVM中廣泛使用的是可達性分析。
對非垃圾對象進行標記都,清除其餘的對象。這種方式對對內存空間形成空隙,即內存碎片,最終致使有空餘空間,但沒有連續的足夠大小的空間分配內存。
標記非垃圾對象後,將這些對象整理好,依次排列內存。這樣內存就是整齊的了。可是由於會形成對象移動,因此效率會有下降。
即組合兩種方式,在若干次清除後進行一次整理。
劃分紅兩個相同大小的區域,收集時,將第一個區域的活對象複製到另外一個區域,這樣不會有內存碎片問題。可是最多隻能存放一半內存,並且全部的活對象都須要拷貝。
爲了保證明際GC過程當中對象的一致性,GC每每須要停頓全部的Java應用線程,也就是常說的StopTheWorld。 目前主流的虛擬機能夠知道哪一個位置保存着對象引用,在HotSpot中,經過OopMap的數據結構在快速的GC Root枚舉。 安全點(Safe Point): 程序並不是在全部時刻都能停頓下來開始GC,只有到達安全點才能暫停。安全點知識程序可能長時間執行的可能的指令,例如方法調用、循環跳轉、異常跳轉等。發生GC時須要讓全部線程停下來,有搶先式中斷和主動式中斷兩種方式。爲了解決主動式中斷線程一直不響應中斷請求的問題,又引入了安全區域(Safe Region)的概念,安全區域是在一段代碼片斷之中引用關係不會發生變化,線程離開安全區域時,要檢查系統是否已經完成了根節點枚舉,若是沒有則一直等待。
垃圾收集器就是垃圾收集算法的相應實現。 在大多數的應用中,有基本能統計到如下的現象:
新生代單線程的收集器,是Client模式默認的垃圾收集器
Serial New的多線程版本。ParNew常和CMS拉配使用。這裏說明一些Parallel和Concurrent即並行和併發在垃圾收集這裏的表示的不一樣,並行表示有多個線程同時進行垃圾收集,併發是指垃圾收集線程和應用線程能夠併發執行。
PS收集器是注重吞吐量(ThroughPut)的收集器。
老年代的單線程收集器
Serial Old的多線程版本,因爲Parallel Scavenge不能和CMS搭配使用,因此會是使用PS時的一種選擇。
注重延遲latency的收集器,在交互式應用中,如面向用戶的web應用,須要儘量減小垃圾收集形成的停頓時間。在總的統計上,吞吐量可能沒有PS收集器高。 細分上,CMS還分爲4個階段
具備大內存收集和目標效率時間等控制能力,目標是代替CMS。G1經過將內存劃分紅不一樣的區域(Region),並對不一樣區域計算分數,分析那個Region最具備收集價值。
經常使用的參數設置有
Nio中的DirectByteBuffer就是堆外內存的一部分,這部份內存只能經過Full Gc進行清理。一些框架會經過System.gc調用手動觸發gc,可是在啓動參數中可能設置了禁止調用System.gc()。另外當設置堆過大時可能會形成堆外內存不夠致使OOM。
監控工具幫助咱們在運行時或問題發生後分析現場,分析內存分佈狀態,哪裏致使內存泄漏等(本該被釋放的對象仍然被引用)。
HotspotJVM的bin目錄下有不少可用的工具。
jps jps -l jps -lv
即java版的ps,能夠查看當前用戶啓動了哪些java進程。
pid指jps命令查看的java進程號
jstat -gcutil pid 1000 10
jstat是一個多種用途的工具,更多須要man jstat或直接輸入jstat查看提示。
jmap能夠查看內存情況
jmap -histo:live pid jmap -dump:file=dump.bin,format=b,live jmap -dump:file=dump.bin,format=b dump下來的內存文件能夠經過MAT進行分析,經過分析引用鏈等分析內存泄漏位置
查看Java線程情況
jstack pid jstack -F pid 能夠查看線程的狀態、名稱、代碼位置
javap 能夠用可讀的方法查看class文件內容,在遇到線上class文件問題,如NoSucheMethodError發生時,能夠快速進行判斷分析。如分析一個A.class文件,查看它的私有方法和字段。
javap -p -c -v A.class
$JAVA_HOME/bin/jvisualvm
$JAVA_HOME/bin/jmc
$JAVA_HOME/bin/jconsole
Java編譯器將Java代碼編譯成class文件格式。 其中步驟包括了咱們熟悉的詞法分析將源文件轉換成token流。語法分析將token流轉換成抽象語法樹(AST)。語義分析分析語義是否正確。源代碼優化。目標代碼生成和目標代碼優化等步驟。最終獲得了class文件。以後在虛擬機中,class文件能夠經過解釋器解釋執行和經過即時編譯器(JIT-just in time)編譯成native代碼執行兩種方式執行。 class文件是有嚴格定義的。符合定義的class文件纔可以被JVM加載、驗證、初始化、執行。 咱們經過javap能夠查看一個class文件的內容。 Class文件能夠分爲如下幾個部分
下面以一個簡單的類
public class Inc { public static void main() { } private int count; public void inc() { count++; } }
看一下它的class文件,經過vim打開,在Normal模式下,按: 輸入%!xxd,便可轉換成16進製表示。而後能夠經過%!xxd -r轉換回來
0000000: cafe babe 0000 0034 0013 0a00 0400 0f09 .......4........ 0000010: 0003 0010 0700 1107 0012 0100 0563 6f75 .............cou 0000020: 6e74 0100 0149 0100 063c 696e 6974 3e01 nt...I...<init>. 0000030: 0003 2829 5601 0004 436f 6465 0100 0f4c ..()V...Code...L 0000040: 696e 654e 756d 6265 7254 6162 6c65 0100 ineNumberTable.. 0000050: 046d 6169 6e01 0003 696e 6301 000a 536f .main...inc...So 0000060: 7572 6365 4669 6c65 0100 0849 6e63 2e6a urceFile...Inc.j 0000070: 6176 610c 0007 0008 0c00 0500 0601 0003 ava............. 0000080: 496e 6301 0010 6a61 7661 2f6c 616e 672f Inc...java/lang/ 0000090: 4f62 6a65 6374 0021 0003 0004 0000 0001 Object.!........ 00000a0: 0002 0005 0006 0000 0003 0001 0007 0008 ................ 00000b0: 0001 0009 0000 001d 0001 0001 0000 0005 ................ 00000c0: 2ab7 0001 b100 0000 0100 0a00 0000 0600 *............... 00000d0: 0100 0000 0100 0900 0b00 0800 0100 0900 ................ 00000e0: 0000 1900 0000 0000 0000 01b1 0000 0001 ................ 00000f0: 000a 0000 0006 0001 0000 0003 0001 000c ................ 0000100: 0008 0001 0009 0000 0027 0003 0001 0000 .........'...... 0000110: 000b 2a59 b400 0204 60b5 0002 b100 0000 ..*Y....`....... 0000120: 0100 0a00 0000 0a00 0200 0000 0700 0a00 ................ 0000130: 0800 0100 0d00 0000 0200 0e0a ............
經過javap來看一下它的結構
javap -v -p -c -s -l Inc Classfile /Users/liuzhengyang/study/java/Inc.class Last modified Oct 6, 2016; size 315 bytes MD5 checksum 770dcaa972162765744184ffc14bc3c6 Compiled from "Inc.java" public class Inc minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#15 // java/lang/Object."<init>":()V #2 = Fieldref #3.#16 // Inc.count:I #3 = Class #17 // Inc #4 = Class #18 // java/lang/Object #5 = Utf8 count #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 inc #13 = Utf8 SourceFile #14 = Utf8 Inc.java #15 = NameAndType #7:#8 // "<init>":()V #16 = NameAndType #5:#6 // count:I #17 = Utf8 Inc #18 = Utf8 java/lang/Object { private int count; descriptor: I flags: ACC_PRIVATE public Inc(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public static void main(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=0, locals=0, args_size=0 0: return LineNumberTable: line 3: 0 public void inc(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field count:I 5: iconst_1 6: iadd 7: putfield #2 // Field count:I 10: return LineNumberTable: line 7: 0 line 8: 10 } SourceFile: "Inc.java"
bytecode保存在class文件的方法的Code屬性中。用一個byte表示操做指令,因此最多有256個指令。一個指令可能會有多個操做數。 操做指令能夠分爲如下幾類:
現代計算機的一本基本思想是分層模型,例如網絡上的分層。在存儲上,爲解決CPU和內存磁盤的速度有指數級差異的問題加入了不少緩存,利用局部性原理加快速度,從CPU寄存器到L1Cache、L2Cache、內存、磁盤,各個層的速度依次下降、空間增大、單位bit造價下降。最近CPU的處理能力的垂直增長彷佛遇到瓶頸,轉而向多核方向發展,多個cpu核可能各自緩存本身的內容,又出現了緩存一致性問題。CPU有一些緩存一致性協議,MESI等。CPU還可能會對機器指令進行亂序執行。JVM爲了屏蔽底層的這些差別,提出了Java內存模型,即JMM(Java Memory Model),來保證Write One Run Anywhere。開發者面向JMM編程,經過JMM提供的一致性保證和工具,就能保證一致性問題。 JMM模型中,每一個線程會有一個私有的內存區域用於緩存讀和寫,各個線程共享一個主內存。 一個重要的概念是happen-before原則。 happen-before用來描述兩個操做的偏序關係,若是Ahappen-beforeB,那個A的操做的結果、產生的影響可以被B看到。 若是咱們有兩個動做x和y,咱們記hb(x,y)爲x happen before y JMM提供的基礎的happen-before規則有
happen-before並不要求在以前發生,只需可以看到操做的結果便可,對應的實現能夠進行重排序或消除鎖,只要保證外觀正確。
以上的總結梳理權當拋磚引入,幫助你們梳理知識結構,更多細節還需經過查看源碼、親自探索,真像就在那代碼中。並且每一個知識點又可以引出一篇筆記分析,以後後補充更多細節文章。
博客地址: liuzhengyang