JVM內(nèi)存模型你只要看這一篇就夠了

我是一只孤傲的魚鷹

讓我們不厭其煩的從內(nèi)存模型開始說起:作為一般人需要了解到的,JVM的內(nèi)存區(qū)域可以被分為:線程棧,堆,靜態(tài)方法區(qū)(實(shí)際上還有更多功能的區(qū)域,并且這里說的是JVM的內(nèi)存區(qū)域,實(shí)際上Java程序還可以調(diào)用native方法使用直接內(nèi)存)。
本文接下來就重點(diǎn)說說這三個區(qū)域。

1. 線程棧

簡介

注意這個棧和數(shù)據(jù)結(jié)構(gòu)中的stack有相似之處,但并不是用戶態(tài)的。準(zhǔn)確的講它壓入的每個棧幀(Stack Frame)是程序指令以及局部變量表,每個方法調(diào)用對應(yīng)一個棧幀。局部變量表包括各種基本數(shù)據(jù)類型:boolean、byte、char、short、int、float、long、double以及對象的引用。我們需要注意到每個線程都有獨(dú)立的棧并且是互相隔離的。

棧的大小

棧的大小可以受到幾個因素影響,一個是jvm參數(shù) -XSS,默認(rèn)值隨著虛擬機(jī)版本以及操作系統(tǒng)影響,從Oracle官網(wǎng)上我們可以找到:

In Java SE 6, the default on Sparc is 512k in the 32-bit VM, and 1024k in the 64-bit VM. On x86 Solaris/Linux it is 320k in the 32-bit VM and 1024k in the 64-bit VM.

我們可以認(rèn)為64位linux默認(rèn)是1m的樣子。
除了JVM設(shè)置,我們還可以在創(chuàng)建Thread的時候手工指定大?。?/p>

public Thread(ThreadGroup group, Runnable target, String name , long stackSize)

棧的大小影響到了線程的最大數(shù)量,尤其在大流量的server中,我們很多時候的并發(fā)數(shù)受到的是線程數(shù)的限制,這時候需要了解限制在哪里。
第一個限制在操作系統(tǒng),以ubuntu為例,/proc/sys/kernel/threads-max 和/proc/sys/vm/max_map_count 定義了總的最大線程數(shù)(根據(jù)資料windows總的來說線程數(shù)會更少)和mmap這個system_call的最大數(shù)量(也就是從內(nèi)存方面限制了線程數(shù))
第二個限制自然是在JVM,理論上我們能分配給線程的內(nèi)存除以單個線程占用的內(nèi)存就是最大線程數(shù)。所以說對Java進(jìn)程來講,既然分配給了堆,棧和靜態(tài)方法區(qū)(或叫永久代,perm區(qū)),我們可以大致認(rèn)為

線程數(shù) = (系統(tǒng)空閑內(nèi)存-堆內(nèi)存(-Xms, -Xmx)- perm區(qū)內(nèi)存(-XX:MaxPermSize)) / 線程棧大小(-Xss)

注意這只是幫助我們樹立一個概念,實(shí)際上還有許多因素影響。

棧的大小還影響到一個就是如果單個棧超過了這個大小,就會拋出StackOverflowError,一般來說遞歸調(diào)用是常見的原因。

如何查看線程棧

使用命令 jstack <pid>可以列出當(dāng)前pid對應(yīng)jvm的所有線程棧描述,描述主要包括了每個線程的狀態(tài)以及堆棧內(nèi)各棧幀的方法全限定名,代碼位置。注意這只是為了可閱讀性,并不是說棧里存著的就是這些字符串。
截取一段tomcat的jstack輸出(線程方面的知識可以參考另一篇拙作《Java多線程你只需要看這一篇就夠了》,本文不再贅述):

tomcat的jstack輸出片段

2.堆和垃圾收集

堆的結(jié)構(gòu)

對于大多數(shù)應(yīng)用來說,Java 堆(Java Heap)是Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。

分代的內(nèi)存管理

首先堆可以劃分為新生代和老年代。

新生代

然后新生代又可以劃分為一個Eden區(qū)和兩個Survivor(幸存)區(qū)。
按照規(guī)定,新對象會首先分配在Eden中(如果對象過大,比如大數(shù)組,將會直接放到老年代)。在GC中,Eden中的對象會被移動到survivor中,直至對象滿足一定的年紀(jì)(定義為熬過minor GC的次數(shù)),會被移動到老年代。

新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過參數(shù) –XX:NewRatio 來指定 )
默認(rèn)的,Eden : from : to = 8 : 1 : 1 ( 可以通過參數(shù) –XX:SurvivorRatio 來設(shè)定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

然后講講垃圾收集

堆內(nèi)存和垃圾收集是密不可分的兩個主題,講垃圾收集的資料很多,但總的來說講的比較混亂,在這里我試圖從一個系統(tǒng)的視角展示垃圾收集。

  • 垃圾收集的意義

    • 垃圾收集的出現(xiàn)解放了C++中手工對內(nèi)存進(jìn)行管理的大量繁雜工作,手工malloc,free不僅增加程序復(fù)雜度,還增加了bug數(shù)量。
    • 分代收集。即在新生代和老生代使用不同的收集方式。在垃圾收集上,目標(biāo)主要有:加大系統(tǒng)吞吐量(減少總垃圾收集的資源消耗);減少最大STW(Stop-The-World)時間;減少總STW時間。不同的系統(tǒng)需要不同的達(dá)成目標(biāo)。而分代這一里程碑式的進(jìn)步首先極大減少了STW,然后可以自由組合來達(dá)到預(yù)定目標(biāo)。
  • 可達(dá)性檢測

    • 引用計(jì)數(shù):一種在jdk1.2之前被使用的垃圾收集算法,我們需要了解其思想。其主要思想就是維護(hù)一個counter,當(dāng)counter為0的時候認(rèn)為對象沒有引用,可以被回收。缺點(diǎn)是無法處理循環(huán)引用。目前iOS開發(fā)中的一個常見技術(shù)ARC(Automatic Reference Counting)也是采用類似的思路。在當(dāng)前的JVM中應(yīng)該是沒有被使用的。
    • 根搜算法:思想是從gc root根據(jù)引用關(guān)系來遍歷整個堆并作標(biāo)記,稱之為mark,等會在具體收集器中介紹并行標(biāo)記和單線程標(biāo)記。之后回收掉未被mark的對象,好處是解決了循環(huán)依賴這種『孤島效應(yīng)』。這里的gc root主要指:
      • a.虛擬機(jī)棧(棧楨中的本地變量表)中的引用的對象
      • b.方法區(qū)中的類靜態(tài)屬性引用的對象
      • c.方法區(qū)中的常量引用的對象
      • d.本地方法棧中JNI的引用的對象
  • 整理策略

    • 復(fù)制:主要用在新生代的回收上,通過from區(qū)和to區(qū)的來回拷貝。需要特定的結(jié)構(gòu)(也就是Young區(qū)現(xiàn)在的結(jié)構(gòu))來支持,對于新生成的對象來說,頻繁的去復(fù)制可以最快的找到那些不用的對象并回收掉空間。所以說在JVM里YGC一定承擔(dān)了最大量的垃圾清除任務(wù)。
    • 標(biāo)記清除/標(biāo)記整理:主要用在老生代回收上,通過根搜的標(biāo)記然后清除或者整理掉不需要的對象。
整理的過程
清除的過程

這里可以看到清除會產(chǎn)生碎片空間,對內(nèi)存利用不是很好,但不是說整理優(yōu)于清除,畢竟整理會更慢。比如CMSGC就是使用清除而不是整理的。

思考一下復(fù)制和標(biāo)記清除/整理的區(qū)別,為什么新生代要用復(fù)制?因?yàn)閷π律鷣碇v,一次垃圾收集要回收掉絕大部分對象,我們通過冗余空間的辦法來加速整理過程(不冗余空間的整理操作要做swap,而冗余只需要做move)。同時可以記錄下每個對象的『年齡』從而優(yōu)化『晉升』操作使得中年對象不被錯誤放到老年代。而反過來老年代偏穩(wěn)定,我們哪怕是用清除,也不會產(chǎn)生太多的碎片,并且整理的代價也并不會太大。

  • 具體的垃圾收集器
    • 新生代收集器:有Serial收集器、ParNew收集器、Parallel Scavenge收集器
    • 老生代收集器:Serial Old收集器、Parallel Old收集器、CMS收集器、G1收集器
垃圾收集器大家庭

以上所有的垃圾收集器都會發(fā)生STW,只不過FGC的STW時間更長。

幾款重點(diǎn)研究的垃圾收集器:

CMSGC:

CMS(Concurrent Mark-Sweep)是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。對于要求服務(wù)器響應(yīng)速度的應(yīng)用上,這種垃圾回收器非常適合,因此我們又叫它低延遲垃圾收集器。在啟動JVM參數(shù)加上-XX:+UseConcMarkSweepGC ,這個參數(shù)表示對于老年代的回收采用CMS,注意此時新生代默認(rèn)使用的是ParNew。CMS采用的基礎(chǔ)算法是:標(biāo)記—清除。

MSCGC vs CMSGC

和普通序列化整理(MSC)區(qū)別在于有三個mark階段(實(shí)際上還有個預(yù)清理過程,但對于解釋清楚CMSGC沒有幫助就忽略了)。CMSGC的精髓在于因?yàn)樽龅搅瞬籗TW的情況下進(jìn)行mark,我們得到了更短的總STW時間,代價是因?yàn)椴⑿衜ark產(chǎn)生了『臟數(shù)據(jù)』即在mark的同時又生成了需要mark的對象,我們必須再進(jìn)行一次STW,并收尾(remark)。
同時,我們要注意到得到更短的STW的同時,我們犧牲了系統(tǒng)吞吐量,CMSGC總吞吐量比ParOld要更低。

G1GC

作為最新的垃圾收集器,有可能在jdk9中成為默認(rèn)的垃圾收集器。
主要思路是將新生代老生代進(jìn)一步分為多個region,每次gc可以針對部分region而不是整個堆內(nèi)存。由此可以降低stw的單次最長時間,代價是可能在總時間上會更高。
G1GC讓系統(tǒng)在整體吞吐量略降的情況下變得更加平滑穩(wěn)定。

為了比較ParOld,CMSGC和G1GC,附上從某篇博客上轉(zhuǎn)載的評測截圖:

靜態(tài)方法區(qū)

最后講一講靜態(tài)方法區(qū),又稱為永久代(Perm Generation)。它用于存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
常見的JVM配置包括:

-XX:MaxPermSize=512m

我們有時候會看到j(luò)ava進(jìn)程報(bào)一個錯誤類似

Exception in thread "State Saver" java.lang.OutOfMemoryError: PermGen space

說明我們此時要調(diào)整配置了,或者說代碼中有一些bug導(dǎo)致大量的perm區(qū)被占用,可能是用到了太多的靜態(tài)變量(一般懷疑map)或者說用到ASM框架導(dǎo)致產(chǎn)生了大量的類信息。

附錄

1.JVM的GC日志的主要參數(shù)

-XX:+PrintGC 輸出GC日志
-XX:+PrintGCDetails 輸出GC的詳細(xì)日志
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準(zhǔn)時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進(jìn)行GC的前后打印出堆的信息
-XX:+PrintGCApplicationStoppedTime // 輸出GC造成應(yīng)用暫停的時間
-Xloggc:../logs/gc.log 日志文件的輸出路徑
-XX:+HeapDumpOnOutOfMemoryError //發(fā)生OOM的時候自動dump堆棧方便分析

2.如何看垃圾收集策略

jmap -heap <pid>

3.如何實(shí)時看堆內(nèi)存的使用情況

jstat -gcutil [pid] [interval] //實(shí)時打印gc情況以及各代內(nèi)存占用比例
jmap -dump:format=b,file=f1 <pid> //dump內(nèi)存到二進(jìn)制文件
jmap -histo [pid] //按占大小倒序列出內(nèi)存中的實(shí)例類型

4.關(guān)于晉升到老年代的條件

對象有兩種可能會進(jìn)入old區(qū):

  1. 存活對象過多。在s1和s2都已經(jīng)溢出了。如果從eden遷往survior區(qū)時,發(fā)現(xiàn)放不下,則直接進(jìn)入 old Gen
  2. 從eden到s區(qū)來回拷貝次數(shù)達(dá)到一定的數(shù)量,總沒有回收掉,進(jìn)入old區(qū)。(從eden到survior1遷到,引用持有中,s1中放不下新遷對象,則清理s1,存活對象,晉升入s2;再下次或繼續(xù)遷移,就把s2中的。準(zhǔn)備說,可能是,這些個對象從s1<->s2來回拷貝一定次數(shù)后,會進(jìn)入old Gen)。這塊Servivor Space 調(diào)整合適的存活次數(shù) Threshold 通過-XX:MaxTenuringThreshold。但也只是一個建議,最終仍由虛擬機(jī)決定
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JVM架構(gòu) 當(dāng)一個程序啟動之前,它的class會被類裝載器裝入方法區(qū)(Permanent區(qū)),執(zhí)行引擎讀取方法區(qū)的...
    cocohaifang閱讀 1,845評論 0 7
  • 原文閱讀 前言 這段時間懈怠了,罪過! 最近看到有同事也開始用上了微信公眾號寫博客了,挺好的~給他們點(diǎn)贊,這博客我...
    碼農(nóng)戲碼閱讀 6,157評論 2 31
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理,因此不免有一些不準(zhǔn)確的地方,同時不同JDK版本的...
    高廣超閱讀 16,054評論 3 83
  • 作者:一字馬胡 轉(zhuǎn)載標(biāo)志 【2017-11-12】 更新日志 日期更新內(nèi)容備注 2017-11-12新建文章初版 ...
    beneke閱讀 2,329評論 0 7
  • Java 虛擬機(jī)有自己完善的硬件架構(gòu), 如處理器、堆棧、寄存器等,還具有相應(yīng)的指令系統(tǒng)。JVM 屏蔽了與具體操作系...
    尹小凱閱讀 1,751評論 0 10

友情鏈接更多精彩內(nèi)容