java中對JVM的深度解析、調優工具、垃圾回收
jdk自帶的JVM調優工具
jvm監控分析工具一般分為兩類,一種是jdk自帶的工具,一種是第三方的分析工具。jdk自帶工具一般在jdk bin目錄下面,以exe的形式直接點選就可以使用,其中包含分析工具已經很強大,幾乎涉及了方方面面,但是我們最常使用的只有兩款:jconsole.exe和jvisualvm.exe;第三方的分析工具有很多,各自的側重點不同,比較有代表性的:MAT(Memory Analyzer Tool)、GChisto等。
jconsole
Jconsole(Java Monitoring and Management Console)是從java5開始,在JDK中自帶的java監控和管理控制檯,用於對JVM中記憶體,執行緒和類等的監控,是一個基於JMX(java management extensions)的GUI效能監測工具。jconsole使用jvm的擴充套件機制獲取並展示虛擬機器中執行的應用程式的效能和資源消耗等資訊。
直接在jdk/bin目錄下點選jconsole.exe即可啟動,介面如下:
在彈出的框中可以選擇本機的監控本機的java應用,也可以選擇遠端的java服務來監控,如果監控遠端服務需要在tomcat啟動指令碼中新增如下程式碼:
-Dcom.sun.management.jmxremote.port=6969 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
連線進去之後,就可以看到jconsole概覽圖和主要的功能:概述、記憶體、執行緒、類、VM、MBeans
- 概述,以圖表的方式顯示出堆記憶體使用量,活動執行緒數,已載入的類,CUP佔用率的折線圖,可以非常清晰的觀察在程式執行過程中的變動情況。
- 記憶體,主要展示了記憶體的使用情況,同時可以檢視堆和非堆記憶體的變化值對比,也可以點選執行GC來處罰GC的執行
- 執行緒,主介面展示執行緒數的活動數和峰值,同時點選左下方執行緒可以檢視執行緒的詳細資訊,比如執行緒的狀態是什麼,堆疊內容等,同時也可以點選“檢測死鎖”來檢查執行緒之間是否有死鎖的情況。
- 類,主要展示已載入類的相關資訊。
- VM 概要,展示JVM所有資訊總覽,包括基本資訊、執行緒相關、堆相關、作業系統、VM引數等。
- Mbean,檢視Mbean的屬性,方法等。
VisualVM
簡介
VisualVM 是一個工具,它提供了一個可視介面,用於檢視 Java 虛擬機器 (Java Virtual Machine, JVM) 上執行的基於 Java 技術的應用程式(Java 應用程式)的詳細資訊。VisualVM 對 Java Development Kit (JDK) 工具所檢索的 JVM 軟體相關資料進行組織,並通過一種使您可以快速檢視有關多個 Java 應用程式的資料的方式提供該資訊。您可以檢視本地應用程式以及遠端主機上執行的應用程式的相關資料。此外,還可以捕獲有關 JVM 軟體例項的資料,並將該資料儲存到本地系統,以供後期檢視或與其他使用者共享。
VisualVM 是javajdk自帶的最牛逼的調優工具了吧,也是我平時使用最多調優工具,幾乎涉及了jvm調優的方方面面。同樣是在jdk/bin目錄下面雙擊jvisualvm.exe既可使用,啟動起來後和jconsole 一樣同樣可以選擇本地和遠端,如果需要監控遠端同樣需要配置相關引數,主介面如下;
VisualVM可以根據需要安裝不同的外掛,每個外掛的關注點都不同,有的主要監控GC,有的主要監控記憶體,有的監控執行緒等。
如何安裝:
1、從主選單中選擇“工具”>“外掛”。2、在“可用外掛”標籤中,選中該外掛的“安裝”複選框。單擊“安裝”。3、逐步完成外掛安裝程式。
大家可能在這裡會遇到安裝不了的坑!!!!
其次從中找到自己的版本,我的是jdk1.8版本,我選擇了第二個,複製它的地址
複製地址到上面的url中,就可以去檢視可用外掛進行下載了
這裡以IntelliJ Platform (pid 15784)為例,雙擊後直接展開,主介面展示了系統和jvm兩大塊內容,點選右下方jvm引數和系統屬性可以參考詳細的引數資訊.
因為VisualVM的外掛太多,我這裡主要介紹三個我主要使用幾個:監控、執行緒、Visual GC
監控的主頁其實也就是,cpu、記憶體、類、執行緒的圖表
執行緒和jconsole功能沒有太大的區別
Visual GC 是常常使用的一個功能,可以明顯的看到年輕代、老年代的記憶體變化,以及gc頻率、gc的時間等。
以上的功能其實jconsole幾乎也有,VisualVM更全面更直觀一些,另外VisualVM非常多的其它功能,可以分析dump的記憶體快照,dump出來的執行緒快照並且進行分析等,還有其它很多的外掛大家可以去探索
JVM垃圾回收
JVM的新生代分為三個區域,一個Eden區和兩個Survivor區,它們之間的比例為(8:1:1),這個比例也是可以修改的。通常情況下,物件主要分配在新生代的Eden區上,少數情況下也可能會直接分配在老年代中。Java虛擬機器每次使用新生代中的Eden和其中一塊Survivor(From),在經過一次Minor GC後,將Eden和Survivor中還存活的物件一次性地複製到另一塊Survivor空間上(這裡使用的複製演算法進行GC),最後清理掉Eden和剛才用過的Survivor(From)空間。將此時在Survivor空間存活下來的物件的年齡設定為1,以後這些物件每在Survivor區熬過一次GC,它們的年齡就加1,當物件年齡達到某個年齡(預設值為15)時,就會把它們移到老年代中。
在新生代中進行GC時,有可能遇到另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活物件,這些物件將直接通過分配擔保機制進入老年代;
1. 新生代(Young Generation):也有叫做年輕代的,這裡使用《深入理解JAVA虛擬機器》中的叫法,下同。
其實看名稱就能看出一些,一般情況下,新建立的物件都會存放到新生代中(大物件除外)。
新生代中物件的特點是:很快就會被GC回收掉的或者不是特別大的物件。
為了方便垃圾收集,新生代又分出了一個Eden區,兩個 Survivor區。
JVM 每次只會使用 Eden區 和其中的一塊 Survivor 區域來為物件服務,另一塊Survivor區域是空的,用於垃圾回收。
舉個例子,第一次回收的時候,虛擬機器會將 Eden區+Survivor(from)區域的存活物件複製到Survivor(to)上(存活物件小於Survivor(to)的空間),清空Survivor(from),虛擬機器使用Eden區+Survivor(to);
第二次回收的時候,虛擬機器再將Eden區+Survivor(to)存活的物件複製到Survivor(from)。
這三個區域預設情況下是按照8:1:1分配,也可以手動配置。
2. Eden區
Eden區位於Java堆的新生代,是新物件分配記憶體的地方,由於堆是所有執行緒共享的,因此在堆上分配記憶體需要加鎖。而Sun JDK為提升效率,會為每個新建的執行緒在Eden上分配一塊獨立的空間由該執行緒獨享,這塊空間稱為TLAB(Thread Local Allocation Buffer)。在TLAB上分配記憶體不需要加鎖,因此JVM在給執行緒中的物件分配記憶體時會盡量在TLAB上分配。如果物件過大或TLAB用完,則仍然在堆上進行分配。如果Eden區記憶體也用完了,則會進行一次Minor GC(young GC)。
3.Survival from to
Survival區與Eden區相同都在Java堆的年輕代。Survival區有兩塊,一塊稱為from區,另一塊為to區,這兩個區是相對的,在發生一次Minor GC後,from區就會和to區互換。在發生Minor GC時,Eden區和Survivalfrom區會把一些仍然存活的物件複製進Survival to區,並清除記憶體。Survival to區會把一些存活得足夠舊的物件移至年老代。
4. 老年代(Old Generation):在新生代每進行一次垃圾收集後,就會給存活的物件“加1歲”,當年齡達到一定數量的時候就會進入老年代(預設是15,可以通過-XX:MaxTenuringThreshold來設定)。
另外,比較大的物件也會進入老年代,可以-XX:PretenureSizeThreshold進行設定。
如-XX:PretenureSizeThreshold3M,那麼大於3M的物件就會直接就進入老年代。
因此,老年代中存放的都是一些生命週期較長的物件或者特別大的物件。
5. 永久代(Permanent Generation ):即JVM的方法區。在這裡存放著一些被虛擬機器載入的類資訊(別忘了還有動態生成的類)的靜態檔案,這就導致了這個區中的東西比老年代和新生代更不容易回收。
永久代大小通過-XX:MaxPermSize=<N>進行設定。
6. 元空間(Metaspace):從JDK 8開始,Java開始使用元空間取代永久代,元空間並不在虛擬機器中,而是直接使用本地記憶體。
那麼,預設情況下,元空間的大小僅受本地記憶體限制。當然,也可以對元空間的大小手動的配置。
JVM常見的垃圾回收機制
Minor GC
Minor GC指新生代GC,即發生在新生代(包括Eden區和Survivor區)的垃圾回收操作,當新生代無法為新生物件分配記憶體空間的時候,會觸發Minor GC。因為新生代中大多數物件的生命週期都很短,所以發生Minor GC的頻率很高,雖然它會觸發stop-the-world,但是它的回收速度很快。
Major GC
Major GC清理Tenured區,用於回收老年代,出現Major GC通常會出現至少一次Minor GC。
Full GC
Full GC是針對整個新生代、老生代、元空間(metaspace,java8以上版本取代perm gen)的全域性範圍的GC。Full GC不等於Major GC,也不等於Minor GC+Major GC,發生Full GC需要看使用了什麼垃圾收集器組合,才能解釋是什麼樣的垃圾回收。
複製演算法:
將區域分成兩部分,其中一部分作為保留空間,另一部分作為使用空間、當發生垃圾回收時,首先檢查使用空間裡有哪些物件是存活的,檢查完之後把存活的物件複製到保留空間(這樣複製過來的好處是減少了記憶體碎片,如果直接在使用空間清除的話,那空間會很零散)裡,然後清洗使用空間。
Survivor的存在意義,就是減少被送到老年代的物件,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷(預設)15次Minor GC還能在新生代中存活的物件,才會被送到老年代。
為什麼要設定兩個Survivor區
設定兩個Survivor區最大的好處就是解決了碎片化,下面我們來分析一下。
為什麼一個Survivor區不行?第一部分中,我們知道了必須設定Survivor區。假設現在只有一個survivor區,我們來模擬一下流程: 剛剛新建的物件在Eden中,一旦Eden滿了,觸發一次Minor GC,Eden中的存活物件就會被移動到Survivor區。這樣繼續迴圈下去,下一次Eden滿了的時候,問題來了,此時進行Minor GC,Eden和Survivor各有一些存活物件,如果此時把Eden區的存活物件硬放到Survivor區,很明顯這兩部分物件所佔有的記憶體是不連續的,也就導致了記憶體碎片化。 我繪製了一幅圖來表明這個過程。其中色塊代表物件,白色框分別代表Eden區(大)和Survivor區(小)。Eden區理所當然大一些,否則新建物件很快就導致Eden區滿,進而觸發Minor GC,有悖於初衷。
碎片化帶來的風險是極大的,嚴重影響JAVA程式的效能。堆空間被散佈的物件佔據不連續的記憶體,最直接的結果就是,堆中沒有足夠大的連續記憶體空間,接下去如果程式需要給一個記憶體需求很大的物件分配記憶體。。。畫面太美不敢看。。。這就好比我們爬山的時候,揹包裡所有東西緊挨著放,最後就可能省出一塊完整的空間放相機。如果每件行李之間隔一點空隙亂放,很可能最後就要一路把相機掛在脖子上了。
那麼,順理成章的,應該建立兩塊Survivor區,剛剛新建的物件在Eden中,經歷一次Minor GC,Eden中的存活物件就會被移動到第一塊survivor space S0,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC,Eden和S0中的存活物件又會被複制送入第二塊survivor space S1(這個過程非常重要,因為這種複製演算法保證了S1中來自S0和Eden兩部分的存活物件佔用連續的記憶體空間,避免了碎片化的發生)。S0和Eden被清空,然後下一輪S0與S1交換角色,如此迴圈往復。如果物件的複製次數達到16次,該物件就會被送到老年代中。下圖中每部分的意義和上一張圖一樣,就不加註釋了。 上述機制最大的好處就是,整個過程中,永遠有一個survivor space是空的,另一個非空的survivor space無碎片。
那麼,Survivor為什麼不分更多塊呢?比方說分成三個、四個、五個?顯然,如果Survivor區再細分下去,每一塊的空間就會比較小,很容易導致Survivor區滿,因此,兩塊Survivor區是經過權衡之後的最佳方案。