寫作這篇文章的目的,是想以淺顯易懂的描述讓大家重新認識JVM,主要包含下面幾個方面
- JVM架構邏輯
- JVM堆內存結構模型
- JVM垃圾回收機制/常用垃圾回收器(Garbage Collector)
- JVM調優
1.JVM架構邏輯
這張圖描繪了JVM和系統調用之間的關係。
- 開始是將編譯好的字節碼文件,經過類加載器,將我們的字節碼加載到內存當中,就生成了Class對象。
- 綠色表示的方法區和堆區是所有線程共享的內存區域。
- 而沙棕色表示的java棧、本地方法棧和程序計數器是運行時線程私有的內存區域,相互獨立開。
- 方法區(Method Area):存放類信息、常量、靜態變量
- 堆(Heap):存放對象實例和數組
Java堆是jvm裏內存最大的一塊,幾乎所有的實例對象都是在這裏分配內存,同時堆也是垃圾收集器管理的主要區域,後面有詳細描述堆內存的結構。
當方法區和堆區無法滿足內存分配需求,也無法再擴展時,就會拋出我們最不想要的OutOfMemoryError異常。
2.怎麼找到垃圾?
JVM怎麼知道堆裏的對象是垃圾呢,有兩種方式:
- 引用計數法
每個對象有一個引用計數屬性,新增一個引用時計數加1,釋放引用時計數減1,計數爲0時標記爲可以回收。
這個方法雖然簡單,但無法解決對象相互引用的問題。
- 根查找/根可達(Root Searching)
從GC Roots開始向下搜索,搜索走過的路徑稱爲引用鏈。到一個對象沒有引用鏈相連時,則根不可達,該對象不可用標記爲垃圾。
畫個簡單的示意圖,像這樣:
順着線一直能找到的就不是垃圾,線斷的(根不可達)的是垃圾。
3.垃圾回收算法
垃圾回收算法,一直以來就三種,不同垃圾回收器只是用的算法不一樣。
- Mark-Sweep(標記清除)
- Copyting(複製)
- Mark-Compact(標記整理)
Mark-Sweep(標記清除)
在一塊內存中,將要回收的數據,標記直接清除。
壞處:碎片化,清除後內存一個洞一個洞的。
Copyting(複製)
把內存線分爲兩半,每次只允許用一半。進行垃圾回收的時候,將一半內存中的存活的數據直接複製到另一塊內存中,然後清理掉之前的內存區域。
效率是最高的,但會造成內存浪費。
Mark-Compact(標記整理)
將內存中的對象,標記集中移動到內存的一邊進行清理,把有用的對象落到前面去(整理)。
效率比Copy略低,好處是把空間讓出來了,後面的大對象可以直接在後面分配內存。
4.常用的垃圾回收器有哪些?
算法是GC的策略,而垃圾收集器,就是根據垃圾回收算法來進行垃圾處理的具體實現。
到現在爲止,垃圾回收器一共有10種,如下圖所示:
生產用的最多的是jdk1.8,默認是Parallel和Parallel Old。
- Serial
Serial(單線程)垃圾收集器是最基本、發展歷史最悠久的收集器。
垃圾回收時,stop-the-word 停掉所有工作線程(幾十毫秒),採用Copy複製算法。
- Serial Old
Serial Old(單線程)是應用在老年代的垃圾收集器,算法是Mark-Compact 標記整理算法。
- ParNew
ParNew垃圾收集器是Serial收集器的多線程版本,實現算法和Serial一樣是Copy複製算法。
- Parallel Scavenge
Parallel Scavenge與前面兩種新生代的收集器相比,不可以和CMS組合使用,使用算法也是Copy複製算法。它與前兩種最大的區別是,他關注的是吞吐量而不是延遲。
- Parallel Old
Parallel Old是Serial Old的多線程版本,應用在老年代的收集器,實現算法和Serial Old一樣是Mark-Compact 標記整理算法。
- CMS
Concurrent Mark Sweep,併發標記清理。在線程執行過程中也可也進行垃圾收集。分爲四個過程:1.初始標記 2.併發標記 3.重新標記 4.併發清理
- G1(Garbage First)
JDK7就已加入JVM的收集器大家庭中,成爲HotSpot重點發展的垃圾回收技術。同優秀的CMS垃圾回收器一樣,G1也是關注最小時延的垃圾回收器,也同樣適合大尺寸堆內存的垃圾收集,官方也推薦使用G1來代替選擇CMS。G1最大的特點是引入分區的思路,弱化了分代的概念,合理利用垃圾收集各個週期的資源,解決了其他收集器甚至CMS的衆多缺陷。
G1採用的標記整理算法。
- ZGC
zgc是jdk11中要發佈的最新垃圾收集器。完全沒有分代的概念,先說下它的優點吧,官方給出的是無碎片,時間可控,超大堆。
- Shenandoah
Shenandoah是一款concurrent及parallel的垃圾收集器,跟ZGC一樣也是面向low-pause-time的垃圾收集器,不過ZGC是基於colored pointers來實現,而Shenandoah GC是基於brooks pointers來實現的。
- Epsilon
Java11新的Epsilon垃圾收集器。
Epsilon(A No-Op Garbage Collector)垃圾回收器控制內存分配,但是不執行任何垃圾回收工作。一旦堆被耗盡,JVM就直接關閉。設計的目的是提供一個完全消極的GC實現,分配有限的內存分配,最大限度降低消費內存佔用量和內存吞吐時的延遲時間。
5.GC的演化
其實垃圾回收的演化,是隨着現在設備內存大小的增進而演進的。
- 最初幾兆-幾十兆
Serial單線程STW垃圾回收。
- 幾十兆-上百兆1G
Parallel 並行多線程進行垃圾回收,但線程不是越多越好,有線程切換耗時。
- 幾G-幾十G到上T
Concurrent GC
6.堆內存邏輯分區
在目前1.8中用的最多的,叫分代管理的辦法。
畫了個堆內存邏輯分區的示意圖,如下:
無論內存多大,將內存一分爲二。默認新生代和老年代的比例是1:2。新生代比例8:1:1,這個新生代的比例,是根據統計學結果得出的,因爲在YGC能回收90%的垃圾。新生代使用算法Copying複製算法,老年代使用的Mark Compact標記整理算法。
到一個對象誕生的時候,先放在新生代,當一個對象被清理了很多次都沒被回收就會放到老年代。不同回收收集器都不一樣,一般是15或6次。
JVM調優主要就是通過參數調比例。
7.垃圾回收過程
- 新生代回收 Young GC (YGC)
- 老年代回收 Old GC (OGC)
- 當YGC和OGC都發生,就叫Full GC
垃圾回收過程如下:
1.新誕生的對象放在Eden(伊甸園)區
2.當伊甸園區滿了,會清理垃圾,把活着的對象複製到survivor(倖存者區),然後把整個Eden清空,這就是一個完整的YGC。
3.如果再來一次,則把Eden活着的和survivor的對象複製到survivor2,同時把Eden和survivor1清空內存,2個survivor中始終保證有一個是空的。
8.JVM調優
JVM調優其實就是通過參數控制內存大小,優化垃圾回收過程。
如下圖所示:
最後彙總一下JVM常用參數配置:
類加載設置
- -XX:+TraceClassLoading 類加載日誌
- -XX:+TraceClassUnloading 類卸載日誌
堆設置
- -Xms 初始堆大小
- -Xmx 最大堆大小
- -XX:NewSize=n 設置年輕代大小
- -XX:NewRatio=n 設置年輕代和年老代的比值。如:爲3,表示年輕代與年老代比值爲1:3,年輕代佔整個年輕代年老代和的1/4
- -XX:SurvivorRatio=n 年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
- -XX:MaxPermSize=n 設置持久代大小
收集器設置
- -XX:+UseSerialGC 設置串行收集器
- -XX:+UseParallelGC 設置並行收集器
- -XX:+UseParalledlOldGC 設置並行年老代收集
- -XX:+UseConcMarkSweepGC 設置併發收集器
並行收集器設置
- -XX:ParallelGCThreads=n 設置並行收集器收集時使用的並行收集線程數
- -XX:MaxGCPauseMillis=n 設置並行收集最大暫停時間
- -XX:GCTimeRatio=n 設置垃圾回收時間佔程序運行時間的百分比,公式爲1/(1+n)
併發收集器設置
- -XX:+CMSIncrementalMode 設置爲增量模式,適用於單CPU
- -XX:ParallelGCThreads=n 設置併發收集器年輕代收集方式爲並行收集時,使用的收集線程數
垃圾回收統計信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename