程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA編程入門知識 >> JVM快速學習

JVM快速學習

編輯:JAVA編程入門知識

最近開始了全面的JAVA生態環境學習,因此,JVM的學習是必不可少的一個環節。和.NET的CLR一樣,一切的JAVA應用均跑在JVM虛擬機上,不過相對我們只能干看看的CLR,JVM有很大的靈活性,可以通過配置優化JVM的性能,同時針對JVM相關的監控軟件也非常豐富。這部分知識有一些晦澀,為了成為一名合格的JAVA程序員,再硬的骨頭也要啃下來,由於自身對這部分沒有特別多的心得,將借鑒參考資料中標注的文章進行學習。

 

首先通過數據類型來引入一個高級語言的核心概念,堆和棧。JAVA的基本類型包括:byte, short, int, long, returnAddress等,其存儲在棧上;引用類型包括:類類型,接口類型和數組,其存儲在堆上。在java中,一個線程就會有相應的線程棧與之對應,而堆則是所有線程共享的。棧是運行單位,因此裡面存儲的信息都是跟當前線程相關信息的,包括局部變量、程序運行狀態、方法返回值等;而堆只負責存儲對象信息。

之所以將對和棧分離,有如下幾點原因:棧代表了邏輯處理,而堆代表數據,滿足分治的思想;堆中的內容可以被多個棧共享,即提供數據交換的方式又節省空間;使得存儲地址動態增長成為可能,相應棧中只需要記錄堆中的一個地址即可;對面向對象的诠釋,對象的屬性就是數據,存放在堆中,對象的行為是運行邏輯,放在棧中;堆和棧中,棧是程序運行最根本的東西,程序運行可以沒有堆,但不能沒有棧,而堆是為棧進行數據存儲服務的,就是一塊共享的內存,這種思想使得垃圾回收成為可能。

Java對象的大小:一個空Object對象的大小是8byte,以及其地址空間4byte(32位),比如對於int這個基礎類型,其包裝類型Integer大小為8+4=12byte,但由於java對象大小需要時8byte的倍數,因而為16byte,因此包裝類型的消耗是基礎類型的2倍。

強引用、軟引用、弱引用和虛引用:強引用是一般虛擬機生成的引用,虛擬機嚴格的將通過它判斷是否需要回收;軟引用一般作為緩存使用,當內存緊張時,會對其進行回收;弱引用,也是作為緩存使用,不過一定會被垃圾回收。

 

JVM的組成,可以通過下圖對其有個大體的了解。

Class Loader:加載大Class文件,該文件的格式由《JVM Specification》規定,包括父類,接口,版本,字段,方法等元數據信息。

Execution Engine:執行引擎也叫解釋器,負責解釋命令,提交OS執行。所謂的JIT指的就是提前將中間語言字節碼轉化為目標文件obj。

Native Interface:為了融合不同語言,java開辟了一塊區域用於處理標記為native的代碼,現在已很少使用。

Runtime data area:運行數據區是JVM的重點,所寫的程序就被加載在這兒。

此外,jvm的寄存器包括:pc,java程序計數器;optop,指向操作數棧頂的指針;frame,指向當前執行方法的執行環境指針;vars,指向當前執行方法的局部變量區第一個變量的指針。

 

JVM的內存管理,所有的數據都是放在運行數據區,接下來介紹其中最復雜的棧(Stack),也叫棧內存,是java程序的運行區,在線程創建時創建,它的生命周期跟隨線程的生命周期,線程結束棧內存就釋放,對於棧來說不存在垃圾回收。棧中的數據是以棧幀(Stack Frame)來存放的,其是一塊內存區塊,是一個有關方法和運行期數據的數據集,當方法A被調用時就產生一個棧幀F1,並壓入到棧中,A方法又調用了B方法,於是產生的棧幀F2也被壓入棧,執行完畢後,先彈出F2,再彈出F1,遵循"先進後出"原則,JAVA Stack的大體結構如下所示。

 

回收算法的分類方式有很多,接下來通過一張表格對其進行一個簡單的介紹。

算法類別

原理闡述

按基本回收策略分

引用計數(Reference Counting)

針對某個對象,其每有一個引用,即增加一個計數,刪除一個就減少一個計數,垃圾回收時只收集計數為0的對象,缺點是無法處理循環引用的情況

標記-清除(Mark-Sweep)

分為兩個階段,首先從引用根結點開始標識所有引用的對象,之後遍歷整個堆,把未標記的對象刪除,此算法需要暫停整個應用,同時會產生內存碎片

復制(Copying)

把內存空間劃分為2個相等區域,每次使用一個,當垃圾回收時,遍歷當前使用區域,把使用中對象賦值到另一個區域,該復制操作成本較小。並可以進行內存整理,缺點是需要兩倍的內存空間

標記-整理(Mark-Compact)

該算法結合了"標記-清除"和"復制"的優點,第一階段從根結點開始標記對象,第二階段遍歷整個堆,清除未標記對象並把存活對象壓縮到堆的其中一塊,按順序排放,同時解決碎片和空間問題。

按分區對待的方式分

增量收集(Incremental Collecting)

實時垃圾回收,即在應用進行的同時進行

分代收集(Generational Collecting)

基於對象生命周期分析得出的算法,把對象分為年輕代、年老代和持久代,對不同生命周期的對象使用不同的算法。

垃圾回收的判斷:由於引用計數方式無法解決循環引用,因而實際上,回收算法都是從根結點出發,遍歷整個對象引用,查找存活對象。搜索的起點為棧(例如java的Main函數)或者是運行時的寄存器,通過其代表的引用找到堆中對象,逐步迭代,直到以null引用或基本類型結束,該結果是一個對象樹,回收器會對未在該樹的對象進行回收。

分代的概念:由於不同對象的生命周期不同,根據其自己的特點采取不同的收集方式可以大幅提高回收效率。比如與業務相關的對象一般生命周期較長,而臨時變量生命周期很短,通過分代,可以避免長生命周期的對象被遍歷,以此來減少消耗。

如何分代:虛擬機分為年輕代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。所有新生成的對象首先是放在年輕代中,該代的目標就是盡快回收那些短生命周期的對象,其分為3個區,一個Eden區,兩個Survior區。大部分對象在Eden區生成,當該區滿時,將存活對象復制到Survivor區(兩個中的一個),當該區也滿了時,將存活對象復制到另一個Survivor,當這個Survivor也滿了時,將從第一個Survivor區復制過來的並且還存活的對象復制到年老區Tenured,因此在年老區中主要存放生命周期較長的對象。而持久代,用於存放靜態文件,如Java類、方法等。持久代對垃圾回收無顯著影響,但App使用較多反射時,需要增加持久代的大小,通過設置-XX:MaxPermSize=<N>。接下來通過一張圖,對該部分有個宏觀的了解。

垃圾回收算法的觸發:由於對象進行了分代處理,因此垃圾回收的區域和時間也有了不同,主要包括如下兩種類型的GC。

Scavenge GC:一般當新對象生成,並且在Eden申請空間失敗時,觸發。將清除Eden區的非存活對象,並把存貨對象移動到Survivor,然後整理兩個Survivor區。該方式不會影響到老年代,此外,該GC推薦使用速度快,效率高的算法,使Eden區盡快空閒出來。

Full GC:對整個堆進行整理,包括Young、Tenured和Perm,因此為了提高系統性能,需要減少FullGC的次數。發生FullGC的場景有:年老代寫滿,持久代被寫滿和System.gc()被顯示調用,上一次GC後Heap各域分配策略動態變化。

 接下來通過一個表格來連接不同的收集器的優缺點。

收集器名稱

诠釋

串性收集

使用單線程處理所有垃圾回收,簡單高效,適合數據量小的場景。通過-XX:+UseSerialGC打開

並行收集

對年輕代進行並行垃圾回收,因此可以減少垃圾回收時間,使用-XX:+UseParallelGC打開。

可以對老年代進行並行收集,默認使用單線程垃圾回收,使用-XX:+UseParallelOldGC打開

使用-XX:ParallelGCThreads=<N>設置並行垃圾回收的線程數,此值可以和機器處理器數相等

通過-XX:MaxGCPauseMillis=<N>設置最大垃圾回收暫定時間

通過-XX:GCTimeRatio=<N>垃圾回收時間與非垃圾回收時間的比值,那麼1/(1+N)即為當先系統的吞吐量,N默認值為99,即1%時間用於垃圾回收

並發收集

前兩者在垃圾回收時,應用會有明顯的暫停,該方式可以減少該影響,保證大部分工作並發進行(應用不停止),適合中大規模應用,使用-XX:+UseConcMarkSweepGC打開,由於並發收集比較復雜,接下來介紹幾個基本概念。

浮動垃圾:由於在應用運行時進行垃圾回收,所有有些垃圾可能在垃圾回收進行完成時產生,這樣就造成了"Floating Garbage",這些垃圾需要在下次垃圾回收周期才能回收,所以並發收集器需要保留20%的預留空間用於這些浮動垃圾。

Concurrent Mode Failure:由於在垃圾回收時系統運行,需要保證有足夠空間給程序使用,否則堆滿時,會發生"並發模式失敗",整個應用暫停,進行垃圾回收。可以通過設置-XX:CMSInitiatingOccupancyFraction=<N>指定還有多少神域堆空間時開始執行並發收集

 

新一代的垃圾回收算法(Garbage First, G1):該算法是為大型應用准備的,支持很大的堆和高吞吐量。該算法簡單來說,就是把整個堆劃分為一個個等大小的區域。內存的回收和劃分都以region為單位,同時汲取CMS特點,把垃圾回收過程分為幾個階段。G1在掃描了region以後,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間,因為活躍對象小,裡面可以認為多數都是垃圾,所有這種方式被稱為Garbage First,即垃圾優先回收,整個垃圾回收過程包含如下幾個步驟。

初始標記(Initial Marking):G1對於每個region都保存了兩個標記用的bitmap,一個為previous marking bitmap,一個next marking bitmap,bitmap中包含了一個bit的地址信息指向對象的起始點。在開始標記前,首先並發的清空next marking bitmap,然後停止所有應用線程,並掃描標識出每個region中root可直接訪問的對象,將region的top值放入next top at mark start(TAMS),之後恢復所有線程。

並發標記(Concurrent Marking):按照之前的標記掃描對象,以標識這些對象的下層對象的活躍狀態,將在此期間使用線程並發修改的先關記錄寫入remembered set logs中,新創建的對象則放入比top值更高的地址區間中,這些新創建的對象默認狀態即為活躍的,同時修改top值。

最終標記暫停(Final Marking Pause):當應用線程的remembered set logs未滿時,是不會放入filled RS buffers中的,因此需要在此步驟中處理remembered set logs並修改相應的remembered set。

存活對象計算並清除(Live Data Counting and Cleanup):該步驟的觸發依賴內存空間是否達到H(H=(1-h)*HeapSize, h為JVM Heap大小的百分比閥值)。

 

JVM的相關配置項非常的多,首先通過一個通用的配置理解堆相關的配置。

Java –Xmx3550m –Xms3550m –Xmn2g –Xss128k –XX:NewRatio=4 –XX:SurvivorRatio=4 –xx:MaxPermSize=64m –XX:MaxTenuringThreshold=0

-Xmx3550:設置JVM最大可用內存為3550M

-Xms3550:設置JVM的初始內存為3550M,此值可以與最大內存一致,避免每次垃圾回收後JVM重新分配內存

-Xmn2g:設置年紀代大小為2G,整個堆大小=年輕代大小+年老代大小+持久代大小。持久代默認大小為64m,所有增加年輕代會減少年老代大小,因此此值非常重要,推薦為整個堆大小的3/8

-Xss128k:設置線程的堆棧大小,默認為1M,實際中需要根據應用進行調整,一般OS推薦的線程數為3000-5000。

-XX:NewRatio=4:設置年輕代與老年代的比值,即年親代占年老代的1/4。

-XX:SurvivorRatio=4:設置年輕代中Eden區域Survivor區的大小比值,設置為4,即兩個Survior區與一個Eden區的比值為2:4。

-XX:MaxPermSize=64m:設置持久代大小為64m

-XX:MaxTenuringThreshold=0:設置垃圾最大年齡,如果設置為0,則年輕代將不經過Survivor區,直接進入老年代,適合老年代較多的場景。

 

接下裡介紹吞吐量優先的並行收集器和響應時間優先的並發收集器。Tip:這類應用推薦將年輕代設置的盡可能的大,尤其是吞吐量大的應用。

並行收集器

java -Xmx3550m -Xms3550 –Xmn2g –Xss128k –XX:+UseParallelGC –XX:ParallelGCThreads=20 –XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 –XX:UseAdaptiveSizePolicy

-XX:+UseParallelGC:選擇年輕代的垃圾收集器為並行收集器

-XX:ParallelGCThreads=20:設置並行收集器的線程數,最好和處理器數目一致

-XX:+UseParallelOldGC:配置年老代垃圾收集方式為並行收集

-XX:MaxGCPauseMillis=100:設置每次年輕代垃圾回收的最長時間,如果滿足,則自動調整年親代大小以滿足此值。

-XX:+UseAdaptiveSizePolicy:設置此選項後,並行收集器自動選擇年輕代區大小和相應Survivor區比例,推薦一直打開。

 

並發收集器

java -Xmx3550m -Xms3550 –Xmn2g –Xss128k –XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC –XX:+UseParNewGC -XX:CMSFullGCBeforeCompaction=5 –XX:UseCMSCompactAtFullCollection

-XX:+UseConcMarkSweepGC(CMS):設置年老代為並發收集

-XX:+UseParNewGC:設置年輕代為並行收集,可以與CMS收集同時進行,現有版本無需設置

-XX:CMSFullGCBeforeCompaction=5:設置運行多少次GC後對內存空間進行壓縮、整理

-XX:UseCMSCompactAtFullCollection:打開年老代的壓縮,可以消除碎片但會影響性能

此外,還有一些展示GC輔助信息的配置: -XX:PrintGC, -XX:+PrintGCDetails, -XX:PrintGCTimeStamps, Xloggc:filename。

 

Java內存模型:不同的平台,內存模型是不一樣的,但jvm內存模型規范是統一的,java多線程並發問題都會反映在java的內存模型上,所謂線程安全就是要控制多個線程對某個資源的有序訪問和修改。總結的Java的內存模型,需要注意2個主要問題:可見性和有序性

Tip:這部分內容理解起來有一定難度,需要多復習。

可見性:多個線程之間是不能相互傳遞數據通信的,它們之間的溝通需要通過共享變量。Java內存模型規定了jvm有主內存,主內存是多個線程共享的,當new一個對象時,也是被分配子啊主內存中的,每個線程都有自己的工作內存,工作內存存儲了主存的某些對象的副本。當線程操作某個對象時,其執行順序為:從主內復制變量當前工作內存(read and load);執行代碼,改變共享變量值(use and assign);用工作內存數據刷新主存相關內容(store and write)。JVM規范定義了線程對主存的操作指令:read,load,use,assign,store,write。當一個共享變量在多個線程的工作內存中都有副本時,如果一個線程修改了這個共享變量,那麼其他線程應該可以看到這個被修改後的值,這就是多線程的可見性問題。

有序性:線程在引用變量時不能直接從主內存中引用,如果線程工作內存中沒有該變量,則會從主內存中拷貝一個副本到工作內存中,這個過程為read-load,完成後線程會引用該副本。當同一線程再度引用該字段時,就有可能重新從主內存中獲取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說read,load,use順序可以有JVM實現系統決定。線程不能直接為主存中字段賦值,它會將值指定給工作內存中的副本變量(assign),完成後這個變量副本會同步到主存儲去(store-write),至於何時同步過去,也有JVM決定。為了這部分操作的有序性,需要使用synchronized關鍵字,可以將方法變為同步方法public synchronized void add(),也可以增加同步變量static Object lock=new Object(),然後synchronized(lock)。每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要獲得鎖的線程,阻塞隊列存儲被阻塞的線程,當一個線程被喚醒(nitify)後,才能進入到就緒隊列,等待cpu調度。例如,當一個線程a第一次執行account.add方法是,jvm會檢查鎖對象account的就緒隊列是否已經有線程在等待,如果有說明account被占用,此時是第一次運行,因此account就緒隊列為空,所以線程a獲得鎖,執行方法。如果恰好這是線程b要執行account.withdraw方法,由於線程a獲得的鎖還未釋放,因此b要進入account的就緒隊列,等得到鎖再執行。

簡單來說,一個線程執行臨界區代碼過程為:獲得同步鎖李晴空工作內存;從主存拷貝變量副本到工作內存;對這些變量進行計算;將變量從工作內存寫回到主存;釋放鎖。

生產者-消費者模型:這是一個非常經典的線程同步模型,有時不光需要保證多個線程多一個共享資源操作的互斥性,往往多個線程見都是有協作的,一個簡單的例子如下所示。

 public class Plate {
     private List<Object> eggs = new ArrayList<Object>();
     public synchronized Object getEgg() {
         if (eggs.size() == 0) {
             try {
                 wait();
             } catch (InterruptedException e) {
             }
         }
         Object egg = eggs.get(0);
         eggs.clear();
         notify();
         return egg;
     }
 
     public synchronized void putEgg(Object egg) {
         if (eggs.size() > 0) {
             try {
                 wait();
             } catch (InterruptedException e) {
             }
         }
         eggs.add(egg);
         notify();
     }
 }
View Code

Volatile關鍵字:volatile是java的一種輕量級同步手段,它只提供多線程內存的可見性,不保證執行的有序性。其意義在於,任何線程對volatile修飾的變量進行修改,都會馬上被其他線程讀取到,因為直接操作主存,沒有線程對工作內存和主存同步。其使用場景為:對變量的寫操作不依賴於當前值;該變量沒有包含在具有其他變量的不定式中。

 JVM調用工具:常見的包括Jconsole、JProfile和VisualVM,推薦使用VisualVM。所有的調優都源於對線上應用的監控和分析,主要需要觀察內存的釋放情況、集合類檢查、對象樹等。如下圖所示,通過查看集合實例的情況來分析。通過這類堆信息查看,可以分析出年老代年輕代劃分是否合理、內存是否洩漏、垃圾回收算法是否合適等問題。

此外,還可以通過線程監控了解系統的線程數量和線程的狀態,是否死鎖等;通過抽樣器查看CPU和內存熱點的情況;通過快照來了解不同時刻相關狀態的差異。

內存洩漏的檢查:內存洩漏一般可以理解為系統資源在錯誤使用的情況下,導致使用完畢的資源無法回收,從而導致新的資源分配請求無法完成,引起系統錯誤。其常見場景為:年老代堆空間被占滿(java.lang.OutOfMemoryError:Java heap space),可以通過堆大小的變化發現問題;持久代被占滿(java.lang.OutOfMemoryError:PermGen space),在大量使用反射時會出現;堆棧溢出(java.lang.StackOverflowError),一般因為錯誤的遞歸和循環造成;線程堆棧滿(Fatal:Stack size too small),可以通過修改-Xss解決,不過還是主要注意是否是因為線程棧過深造成;系統內存被占滿(java.lang.OutOfMemoryError:unable to create new native thread),由於OS沒有足夠的資源來產生線程造成的,可以考慮減少單個線程的消耗或重新設計這部分程序。

 

常見問題

1.堆和棧的區別:堆是存放對象的,但是對象內臨時變量是存在棧內存中的。棧是跟隨線程的,有線程就有棧,堆是跟隨JVM的,有JVM就有堆內存。

2.堆內存中到底存在什麼:對象,包括對象變量和對象方法。

3.類變量和實例變量有什麼區別:靜態變量(有static修飾)是類變量,非靜態變量是實例變量。靜態變量存在方法區中,實例變量存在堆內存中。有個說法是類變量是在JVM啟動時就初始化好了,其實不對。

4.Java的方法到底是傳值還是傳引用:都不是,而是以傳值的方式傳遞地址,具體的說就是原始數據類型傳遞的值,引用類型傳遞的地址。對於原始數據類型,JVM的處理方法是從Method Area或Heap中拷貝到Stack,然後運行Frame中方法,運行完畢再將變量拷貝回去。

5.為什麼會產生OutOfMemory:原因是Heap內存中沒有可用空間了或永久區滿了,有時會發現對象不多仍出現該情況,一般是由繼承層次過多造成,因為Heap中產生的對象都是先產生父類,然後產生子類。

6.為什麼會產生StackOverFlowError:因為線程把棧空間消耗完了,一般都是遞歸函數造成的。

7.JVM中那些共享的,那些是私有的:Heap和Method Area是共享的,其他都是私有的。

8.還有那些需要注意的補充概念:常量池(constant pool),按照順序存放程序中的常量,且進行索引編號,默認0到127放在常量池,string也是;安全管理器(Security Manager),提供java運行期的安全控制,類加載器只有在通過認證後才能加載class文件;方法索引表(Methods table),記錄每個method的地址信息,Stack和Heap中的地址指針其實指向Methods table的地址。

9.為什麼不能調用System.gc():因為該操作會進行Full GC並停止所有活動。

10.CGLib是什麼:用於Spring和Hibernate等技術對類進行增強時,其可以直接操作字節碼動態生成Class文件。

 

參考資料:

  1. 周志明. 深入理解Java虛擬機:JVM高級特性與最佳實踐(第2版)[M]. 北京:機械工業出版社, 2013.
  2. JVM調優[EB/OL]. http://hllvm.group.iteye.com/group/wiki/?category_id=301.
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved