您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何分析Java性能優化中的垃圾回收機制,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
在 Java 虛擬機規范中,提及了如下幾種類型的內存空間:
◇棧內存(Stack):每個線程私有的。
◇堆內存(Heap):所有線程公用的。
◇方法區(Method Area):有點像以前常說的“進程代碼段”,這里面存放了每個加載類的反射信息、類函數的代碼、編譯時常量等信息。
◇原生方法棧(Native Method Stack):主要用于 JNI 中的原生代碼,平時很少涉及。
其實 Java 虛擬機規范中并未規定垃圾回收的相關細節。垃圾回收具體該怎么搞,完全取決于各個 JVM 的設計者。所以,不同的 JVM 之間,GC 的行為可能會有一定的差異。下面咱拿 SUN 官方的 JVM 來簡單介紹一下 GC 的機制。
一般情況下,當 JVM 發現堆內存比較緊張、不太夠用時,它就會著手進行垃圾回收工作。但是大伙兒要認清這樣一個殘酷的事實:JVM 進行 GC 的時間點是無法準確預知的。因為 GC 啟動的時刻會受到各種運行環境因素的影響,隨機性太大。
雖說咱們無法準確預知,但如果你想知道每次垃圾回收執行的情況,還是蠻方便的。可以通過 JVM 的命令行參數“-XX:+PrintGC”把相關信息打印出來。
另外,調用 System.gc() 只是建議 JVM 進行 GC。至于 JVM 到底會不會真的去做,只有天曉得。所以,通常不建議自己手動調用 System.gc(),還是讓 JVM 自行決定比較好。另外,使用 JVM 命令行參數“-XX:+DisableExplicitGC”可以讓 System.gc() 不起作用。
一般情況下,JVM 會有一個或多個專門的垃圾回收線程,由它們負責清理回收垃圾內存。
垃圾回收線程會從“根集(Root Set)”開始進行對象引用的遍歷。所謂的“根集”,就是正在運行的線程中,可以訪問的【引用變量】的集合(比如所有線程當前函數的參數和局部變量、當前類的成員變量等等)。垃圾回收線程先找出被根集直接引用的所有對象(不妨叫集合1),然后再找出被集合1直接引用的所有對象(不妨叫集合2),然后再找出被集合2直接引用的所有對象......如此循環往復,直到把能遍歷到的對象都遍歷完。
凡是從“根集”通過上述遍歷可以到達的對象,都稱為可達對象或有效對象;反之,則是不可達對象或失效對象(也就是垃圾)。
通過上述階段,就把垃圾對象都找出來。然后垃圾回收線程會進行相應的清理和回收工作,包括:把垃圾內存重新變為可用內存、進行內存的整理以消除內存碎片、等等。這個過程會涉及到若干算法,限于篇幅,咱就不深入聊了。
早期的 JVM 是不采用分代技術的,所有被 GC 管理的對象都存放在同一個堆里面。這么做的缺點比較明顯:每次進行GC都要遍歷所有對象,開銷很大。其實大部分的對象生命周期都很短(短命對象),只有少數對象比較長壽;在這些短命對象中,又只有少數對象占用的內存空間大;其它大量的短命對象都屬于小對象(很符合二八原理)。
有鑒于此,從 JDK 1.2 之后,JVM 開始使用分代的垃圾回收(Generational Garbage Collection)。JVM 把 GC 相關的內存分為“年老代”(Tenured)和“年輕代”(Nursery)、“持久代”(Permanent,對應于 JVM 規范的“方法區”)。【大部分】對象在剛創建時,都位于“年輕代”。如果某對象經歷了幾輪 GC 還活著(大齡對象),就把它移到“年老代”。另外,如果某個對象在創建時比較大,可能就直接被丟到年老代。經過這種策略,使得年輕代總是保存那些短命的小對象。在空間尺寸上,“年輕代”相對較小,而“年老代”相對較大。
因為有了分代技術,JVM 的 GC 也相應分為兩種——主要收集(Major Collection)和次要收集(Minor Collection)。“主要收集”同時清理年老代和年輕代,因此開銷很大,不常進行;“次要收集”僅僅清理年輕代,開銷很小,經常進行。
剛才介紹了GC的大致原理,那GC對性能會造成哪些影響捏?主要有如下幾個方面:
早期的 GC 比較弱智。在它工作期間,所有其它的線程都被暫停(以免影響垃圾回收工作)。等到 GC 干完活,其它線程再繼續運行。所以,早期 JDK 的 GC 一旦開始工作,整個程序就會陷入假死狀態,失去各種響應。
經過這些年的技術改進(包括采用分代技術),從 JDK 1.4 開始,GC 已經比較精明了。在它干活期間,只是偶爾暫停一下其它線程的運行(從長時間假死變為暫時性休克)。
試想如果JVM中的對象很多,那遍歷完所有可達對象肯定是比較費勁的工作,這個開銷可不小。
遍歷完對象引用之后,對垃圾的清理和回收也有較大的開銷。這部分開銷可能包括復制內存塊、更新對象引用等等。
因為今天聊的是性能的話題,必然會提到衡量 GC 性能的兩個重要指標:吞吐量(Throughput)和停頓時間(Pause Time)。吞吐量這個詞不是很直觀,解釋一下:就是 JVM【不用于】GC 的時間占總時間的比率。“吞吐量”是越大越好,“停頓時間”是越小越好。
不同的應用程序對這兩個指標的關注點不一樣(后面具體會說),也就是所謂的“眾口難調”。很多 JVM 廠商為了迎合“眾口”,不得不提供多種幾種垃圾收集器供使用者選擇。不同的收集器,采用的收集策略是不一樣的,下面具體介紹。
使用命令行選項“-XX:+UseSerialGC”指定。
這種收集器是最傳統的收集器。它使用單線程進行垃圾回收,對于“單 CPU 機器”比較合適。另外,小型應用或者對上述兩個指標沒有特殊要求的,可以使用串行收集器。
顧名思義,這種收集器使用多個線程進行垃圾回收以達到高吞吐量。垃圾回收線程的數量通過命令行選項“-XX:ParallelGCThreads=n”指定。可以設置該數值以便充分利用“多CPU 或 多核”。
當使用命令行選項“-XX:+UseParallelGC”時:它會針對年輕代使用多個垃圾回收線程,對年老代依然使用單個線程的串行方式。此選項最早在JDK 1.5引入。
當使用命令行選項“-XX:+UseParallelOldGC”時:它針對年輕代和年老代都使用多個垃圾回收線程的方式。不過此選項從 JDK 1.6 才開始引入。
使用命令行選項“-XX:+UseConcMarkSweepGC”指定。
這種收集器優先保證程序的響應。它會盡量讓垃圾回收線程和應用自身的線程同時運行,從而降低停頓時間。此選項從JDK 1.4.1開始支持。
自從 JDK 1.4.2 以來,SUN 官方就停止維護該收集器了。所以俺就節省點口水,不多說了。
由于 GC 是針對存儲在堆內存的對象進行的。咱們如果在程序中減少引用對象的分配(也就相應降低堆內存分配),那對于提高 GC 的性能是很有幫助滴。上次“ 字符串過濾實戰 ”的帖子給出了一個例子,示范了如何通過降低堆內存的分配次數來提升性能。
JVM 的堆內存是有講究的,不能太大也不能太小。如果堆內存太小,JVM 老是感覺內存不夠用,可能會導致頻繁進行垃圾回收,影響了性能;如果堆內存太大,以至于操作系統的大部分物理內存都被 JVM 自個兒霸占了,那可能會影響其它應用程序甚至操作系統本身的性能。
另外,年輕代的大小(或者說“年輕代”與“年老代”的比值)對于 GC 的性能也有明顯影響。如果年輕代太小,可能導致次要收集很頻繁;如果年輕代太大,導致次要收集的停頓很明顯。
JVM 提供了若干和堆內存大小相關的命令行選項,具體如下:
------------------------------
-Xms 設置初始堆內存
-Xmx 設置最大堆內存
-Xmn 設置年輕代的大小
-XX:NewRatio=n 設置年輕代與年老代的比例為“n”
-XX:NewSize=n 設置年輕代大小為“n”
------------------------------
一般情況下,JVM 的默認參數值已經夠用。所以沒事兒別輕易動用上述選項。如果你非調整不可,一定要做深入的性能對比測試,保證調整后的性能確實優于默認參數值。
前面提到了不同應用的眾口難調。常見的口味有兩種:(1)看重吞吐量,對停頓時間無所謂;(2)側重于停頓時間。
對于某些在后臺的、單純運算密集型的應用,屬于第一種。比如某些科學計算的應用。這時候建議使用并行收集器。
對于涉及用戶 UI 交互的、實時性要求比較高、程序需要快速響應的,屬于第二種。比如某些桌面游戲、某些電信交換系統。這時候建議使用并發收集器。
關于如何分析Java性能優化中的垃圾回收機制就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。