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、逐步完成插件安裝程序。

大家可能在這裏會遇到安裝不了的坑!!!!

解決:首先用瀏覽器瀏覽:https://visualvm.github.io/pluginscenters.html

其次從中找到自己的版本,我的是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,有悖於初衷。 
一個Survivor區帶來碎片化

碎片化帶來的風險是極大的,嚴重影響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避免碎片化 
上述機制最大的好處就是,整個過程中,永遠有一個survivor space是空的,另一個非空的survivor space無碎片。

那麼,Survivor爲什麼不分更多塊呢?比方說分成三個、四個、五個?顯然,如果Survivor區再細分下去,每一塊的空間就會比較小,很容易導致Survivor區滿,因此,兩塊Survivor區是經過權衡之後的最佳方案。

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