1. 程式人生 > >沈澱再出發:jvm的本質

沈澱再出發:jvm的本質

所有 closed 用戶 指向 偏移量 運行時間 filename 內存限制 進程和線程

沈澱再出發:jvm的本質

一、前言

關於jvm,使用的地方實在是太多了,從字面意思上我們都能明白這也是一個虛擬機,那麽其他的虛擬機都會用來運行別的操作系統的,而jvm卻是實現了可以在不用的操作系統之上運行同樣的字節碼文件,以此來實現代碼的可移植性,大家可以看一下編譯原理,就知道了jvm運行代碼的本質其實是根據不同的平臺將字節碼文件(中間代碼)變成最終適合不同平臺的機器碼。同時jvm中也有很多的概念,肯定也是和編譯系統相關的了,數據和代碼如何存儲,數據分為哪幾種數據,需要什麽格式存儲,堆棧等等,以及相關數據的生存周期,垃圾回收機制,由此產生的一系列的問題,函數的存儲和調用,理解到這個程度,我們就能更好地理解使用java進行開發的其他軟件,比如hadoop等等。同樣的,對於進程和線程的存儲和執行的情況,並發以及volatile我們都有了更深的理解。

二、jvm初探

2.1、java平臺

Java平臺由Java虛擬機和Java應用程序接口搭建,Java語言則是進入這個平臺的通道,用Java語言編寫並編譯的程序可以運行在這個平臺上。這個平臺的結構如下圖所示:

技術分享圖片

2.2、JVM體系結構

  1) 類裝載器(ClassLoader)(用來裝載.class文件)
  2) 執行引擎(執行字節碼,或者執行本地方法)
  3) 運行時數據區(方法區、堆、java棧、PC寄存器、本地方法棧)

技術分享圖片

2.3、JVM生命周期

1  啟動:啟動一個Java程序時,一個JVM實例就產生了,任何一個擁有public static void
main(String[] args)函數的class都可以作為JVM實例運行的起點。 2 運行:main()作為該程序初始線程的起點,任何其他線程均由該線程啟動。 3 消亡:當程序中的所有非守護線程都終止時,JVM才退出;若安全管理器允許,程序也可以使用Runtime類或者System.exit()來退出。

  一個運行中的Java虛擬機有著清晰的任務:執行Java程序。程序開始執行時才運行,程序結束時就停止。在同一臺機器上運行三個程序,就會有三個運行中的Java虛擬機。 Java虛擬機總是開始於一個main()方法,這個方法必須是公有、返回void、只接受一個字符串數組。在程序執行時,必須給Java虛擬機指明main()方法的類名。main()方法是程序的起點,執行的線程初始化為初始線程,程序中其他的線程都由其來啟動。


  Java中的線程分為兩種:守護線程 (daemon)和普通線程(non-daemon)。守護線程是Java虛擬機自己使用的線程,比如負責垃圾收集的線程就是一個守護線程。當然,也可以把自己的程序設置為守護線程,包含main()方法的初始線程不是守護線程。只要Java虛擬機中還有普通的線程在執行,Java虛擬機就不會停止。如果有足夠的權限,可以調用exit()方法終止程序。

2.4、 JVM運行時數據區

技術分享圖片

Java堆(Heap)

被所有線程共享的一塊內存區域,在虛擬機啟動時創建用來存儲對象實例,可以通過-Xmx和-Xms控制堆的大小;OutOfMemoryError異常:當在堆中沒有內存完成實例分配,且堆也無法再擴展時。java堆是垃圾收集器管理的主要區域。java堆還可以細分為:新生代(New/Young)、舊生代/年老代(Old/Tenured)持久代(Permanent)在方法區,不屬於Heap。

1 新生代:新建的對象都由新生代分配內存。常常又被劃分為Eden區和Survivor區。Eden空間不足時會把存活的對象轉移到Survivor。新生代的大小可由-Xmn控制,也可用-XX:SurvivorRatio控制Eden和Survivor的比例。
2 舊生代:存放經過多次垃圾回收仍然存活的對象。
3 持久代:存放靜態文件,如Java類、方法等;持久代在方法區,對垃圾回收沒有顯著影響。

技術分享圖片

方法區

線程間共享,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器(JIT)編譯後的代碼等數據;OutOfMemoryError異常:當方法區無法滿足內存的分配需求時。JVM用持久代(Permanet Generation)來存放方法區。
運行時常量池:方法區的一部分,用於存放編譯期生成的各種字面量與符號引用,如String類型常量就存放在常量池;OutOfMemoryError異常:當常量池無法再申請到內存時。

java虛擬機棧(VM Stack)

線程私有,生命周期與線程相同;存儲方法的局部變量表(基本類型、對象引用)、操作數棧、動態鏈接、方法出口等信息。 java方法執行的內存模型,每個方法執行的同時都會創建一個棧幀,每一個方法被調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。

1     StackOverflowError異常:當線程請求的棧深度大於虛擬機所允許的深度
2     OutOfMemoryError異常:如果棧的擴展時無法申請到足夠的內存

 JVM棧是線程私有的,每個線程創建的同時都會創建JVM棧,JVM棧中存放的為當前線程中局部基本類型的變量、部分的返回結果以及Stack Frame。其他引用類型的對象在JVM棧上僅存放變量名和指向堆上對象實例的首地址。

本地方法棧(Native Method Stack)

與虛擬機棧相似,主要為虛擬機使用到的Native方法服務,在HotSpot虛擬機中直接把本地方法棧與虛擬機棧二合一。用於支持native方法的執行,存儲了每個native方法調用的狀態。對於本地方法接口,實現JVM並不要求一定要有它的支持,甚至可以完全沒有。Sun公司實現Java本地接口(JNI)是出於可移植性的考慮,當然我們也可以設計出其它的本地接口來代替Sun公司的JNI。但是這些設計與實現是比較復雜的事情,需要確保垃圾回收器不會將那些正在被本地方法調用的對象釋放掉。

程序計數器(Program Counter Register)

當前線程所執行的字節碼的行號指示器當前線程私有,不會出現OutOfMemoryError情況。

直接內存(Direct Memory)

直接內存並不是虛擬機運行的一部分,也不是Java虛擬機規範中定義的內存區域,但是這部分內存也被頻繁使用;NIO可以使用Native函數庫直接分配堆外內存,堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。大小不受Java堆大小的限制,受本機(服務器)內存限制。OutOfMemoryError異常:系統內存不足時。

Java對象實例存放在堆中;
常量存放在方法區的常量池;
虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據放在方法區;
以上區域是所有線程共享的。
棧是線程私有的,存放該方法的局部變量表(基本類型、對象引用)、操作數棧、動態鏈接、方法出口等信息。
一個Java程序對應一個JVM,一個方法(線程)對應一個Java棧。

2.4、Java代碼的編譯和執行過程

技術分享圖片

Java代碼的編譯和執行包括了三個重要機制:

(1)Java源碼編譯機制(.java源代碼文件 -> .class字節碼文件)
(2)類加載機制(ClassLoader)
(3)類執行機制(JVM執行引擎)

2.4.1、Java源碼編譯機制

  Java源代碼是不能被機器識別的,需要先經過編譯器編譯成JVM可以執行的.class字節碼文件,再由解釋器解釋運行。即Java源文件(.java) -- Java編譯器 --> Java字節碼文件 (.class) --> Java解釋器 --> 執行。流程圖如下:
技術分享圖片

字節碼文件(.class)是平臺無關的。Java中字符只以Unicode一種形式存在。字符轉換發生在JVM和OS交界處(Reader/Writer)。最後生成的class文件由以下部分組成:

1  結構信息:包括class文件格式版本號及各部分的數量與大小的信息
2  元數據:對應於Java源碼中聲明與常量的信息。包含類/繼承的超類/實現的接口的聲明信息、域與方法聲明信息和常量池
3  方法信息:對應Java源碼中語句和表達式對應的信息。包含字節碼、異常處理器表、求值棧與局部變量區大小、求值棧的類型記錄、調試符號信息

2.4.2、類加載機制(ClassLoader)

  Java程序是由多個獨立的類文件組成。這些類文件並非一次性全部裝入內存,而是依據程序逐步載入。JVM的類加載是通過ClassLoader及其子類來完成的,類的層次關系和加載順序可以由下圖來描述:

技術分享圖片

 1 1、Bootstrap ClassLoader
 2     JVM的根ClassLoader,由C++實現
 3     加載Java的核心API:$JAVA_HOME中jre/lib/rt.jar中所有class文件的加載,這個jar中包含了java規範定義的所有接口以及實現。
 4     JVM啟動時即初始化此ClassLoader
 5 2、Extension ClassLoader
 6      加載Java擴展API(lib/ext中的類)
 7 3、App ClassLoader
 8     加載Classpath目錄下定義的class
 9 4、Custom ClassLoader
10     屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據J2EE規範自行實現ClassLoader。
1 雙親委派機制:JVM在加載類時默認采用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸。如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
2   作用:1)避免重復加載;2)更安全。如果不是雙親委派,那麽用戶在自己的classpath編寫了一個java.lang.Object的類,那就無法保證Object的唯一性。所以使用雙親委派,即使自己編寫了,但是永遠都不會被加載運行。
3 
4 破壞雙親委派機制:雙親委派機制並不是一種強制性的約束模型,而是Java設計者推薦給開發者的類加載器實現方式。線程上下文類加載器,這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那麽這個類加載器就是應用程序類加載器。像JDBC就是采用了這種方式。這種行為就是逆向使用了加載器,違背了雙親委派模型的一般性原則。

2.4.3、類執行機制

Java字節碼的執行是由JVM執行引擎來完成,流程圖如下所示:

技術分享圖片

JVM是基於棧的體系結構來執行class字節碼的,線程創建後,都會產生程序計數器(PC)和棧(Stack),程序計數器存放下一條要執行的指令在方法內的偏移量,棧中存放一個個棧幀,每個棧幀對應著每個方法的每次調用,而棧幀又是有局部變量區和操作數棧兩部分組成,局部變量區用於存放方法中的局部變量和參數,操作數棧中用於存放方法執行過程中產生的中間結果。

主要的執行技術:解釋,即時編譯,自適應優化、芯片級直接執行

1     解釋屬於第一代JVM,
2     即時編譯JIT屬於第二代JVM,
3     自適應優化(目前Sun的HotspotJVM采用這種技術)則吸取第一代JVM和第二代JVM的經驗,采用兩者結合的方式
4     開始對所有的代碼都采取解釋執行的方式,並監視代碼執行情況。
對那些經常調用的方法啟動一個後臺線程,將其編譯為本地代碼,並進行優化。
若方法不再頻繁使用,則取消編譯過的代碼,仍對其進行解釋執行。

2.5、JVM垃圾回收(GC)

GC的基本原理:將內存中不再被引用的對象(垃圾)進行回收,GC中用於回收的方法稱為收集器。由於GC需要消耗一些資源和時間,Java在對對象的生命周期特征進行分析後,按照新生代、舊生代的方式來對對象進行收集,以盡可能的縮短GC對應用造成的暫停。

1 對新生代的對象的收集稱為minor GC;
2 對舊生代的對象的收集稱為Full GC;
3 程序中主動調用System.gc()的GC為Full GC。 

  Java垃圾回收是單獨的後臺線程gc執行的,自動運行無需顯示調用。即使主動調用了java.lang.System.gc(),該方法也只會提醒系統進行垃圾回收,但系統不一定會回應,可能會不予理睬。
判斷一塊內存空間是否符合回收標準:

(1)對象賦予了空值,且之後再未調用(obj = null;)
(2)對象賦予了新值,即重新分配了內存空間(obj = new Obj();)

內存泄漏:程序中保留著對永遠不再使用的對象的引用。因此這些對象不回被GC回收,卻一直占用內存空間卻毫無用處。即:1)對象是可達的;2)對象是無用的。滿足這兩個條件即可判定為內存泄漏。內存泄露的原因:1)全局集合;2)緩存;3)ClassLoader。
  應確保不需要的對象不可達,通常采用將對象字段設置為null的方式,或從容器collection中移除對象。局部變量不再使用時無需顯式設置為null,因為對局部變量的引用會隨著方法的退出而自動清除。

2.6、內存調優

調優目的:減少GC的頻率尤其是Full GC的次數,過多的GC會占用很多系統資源影響吞吐量。特別要關註Full GC,因為它會對整個堆進行整理。
主要手段:JVM調優通過配置JVM的參數來提高垃圾回收的速度,合理分配堆內存各部分的比例。

導致Full GC的幾種情況和調優策略:

1     舊生代空間不足
2     調優時盡量讓對象在新生代GC時被回收、讓對象在新生代多存活一段時間和不要創建過大的對象及數組避免直接在舊生代創建對象
3     持久代(Pemanet Generation)空間不足
4     增大Perm Gen空間,避免太多靜態對象
5     統計得到的GC後晉升到舊生代的平均大小大於舊生代剩余空間
6     控制好新生代和舊生代的比例
7     System.gc()被顯示調用
8     垃圾回收不要手動觸發,盡量依靠JVM自身的機制 

堆內存比例不良設置導致的後果:

12)新生代設置過大
一是新生代設置過大會導致舊生代過小(堆總量一定),從而誘發Full GC;
二是新生代GC耗時大幅度增加;
一般說來新生代占整個堆1/3比較合適;
34)Survivor設置過大
-XX:MaxTenuringThreshold=n來控制新生代存活時間,盡量讓對象在新生代被回收。

JVM提供兩種較為簡單的GC策略的設置方式:

1 1)吞吐量優先
2     JVM以吞吐量為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,來達到吞吐量指標。這個值可由-XX:GCTimeRatio=n來設置
3 2)暫停時間優先
4    JVM以暫停時間為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,盡量保證每次GC造成的應用停止時間都在指定的數值範圍內完成。這個值可由-XX:MaxGCPauseRatio=n來設置。

JVM常見配置:

技術分享圖片
 1 堆設置
 2         -Xms:初始堆大小
 3         -Xmx:最大堆大小
 4         -XX:NewSize=n:設置年輕代大小
 5         -XX:NewRatio=n:設置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
 6         -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。註意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5
 7         -XX:MaxPermSize=n:設置持久代大小
 8 收集器設置
 9         -XX:+UseSerialGC:設置串行收集器
10         -XX:+UseParallelGC:設置並行收集器
11         -XX:+UseParalledlOldGC:設置並行年老代收集器
12         -XX:+UseConcMarkSweepGC:設置並發收集器
13 垃圾回收統計信息
14         -XX:+PrintGC
15         -XX:+PrintGCDetails
16         -XX:+PrintGCTimeStamps
17         -Xloggc:filename
18 並行收集器設置
19         -XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。
20         -XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間
21         -XX:GCTimeRatio=n:設置垃圾回收時間占程序運行時間的百分比。公式為1/(1+n)
22 並發收集器設置
23         -XX:+CMSIncrementalMode:設置為增量模式。適用於單CPU情況。
24         -XX:ParallelGCThreads=n:設置並發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。
View Code

三、總結

通過對jvm的學習,我們可以深刻地理解到程序的執行原理,以及背後的內存和CPU的處理情況,對我們理解多線程,高並發,內存管理,內存優化,代碼優化等有著重要的作用。

參考文獻:https://www.cnblogs.com/IUbanana/p/7067362.html

沈澱再出發:jvm的本質