天天看點

運作時資料區02--本地方法棧、本地方法接口、堆

運作時資料區02--本地方法棧、本地方法接口、堆

本地方法接口

簡單地講,一個 native method 就是一個 java 調用非 java 代碼的接口。一個 native method 是這樣一個 java 方法:該方法的實作由非 java 語言實作,比如 c。這個特征并非 java 所特有,很多其它的程式設計語言都有這一機制,比如在 c++中,你可以用 extern"c“告知 c++編譯器去調用一個 c 的函數。

a native method is a java method whose implementation is provided by non-java code

在定義ー個 native method 時,并不提供實作體(有些像定義ー個 java interface),因為其實作體是由非 java語言在外面實作的。

本地接口的作用是融合不同的程式設計語言為java所用,它的初衷是融合c/c++程式。

java 使用起來非常友善,然而有些層次的任務用 java 實作起來不容易,或者我們對程式的效率很在意時,問題就來了。

與 java 環境外互動:

有時 java 應用需要與 java 外面的環境互動,這是本地方法存在的主要原因。你可以想想 java 需要與一些底層系統,如作業系統或某些硬體交換資訊時的情。本地方法正是這樣一種交流機制:它為我們提供了一個非常簡潔的接口,而且我們無需去了解 java 應用之外的繁瑣的細節。

與作業系統互動:

jvm 支援着 java 語言本身和運作時庫,它是 java 程式賴以生存的平台,它由一個解釋器(解釋位元組碼)和一些連接配接到本地代碼的庫組成。然而不管怎樣,它畢竟不是一個完整的系統,它經常依賴于一些底層系統的支援。這些底層系統常常是強大的作業系統。通過使用本地方我們得以用 java 實作了 jre 的與底層系統的互動,甚至 jvm 的一些部分就是用 c 寫的。還有,如果我們要使用一些 java 語言本身沒有提供封裝的作業系統的特性時,我們也需要使用本地方法。

sun 's java

sun 的解釋器是用 c 實作的,這使得它能像一些普通的 c 一樣與外部互動。jre 大部分是用 java 實作的,它也通過一些本地方法與外界互動。例如:類 java.lang. thread 的 setpriority()方法是用 java 實作的,但是它實作調用的是該類裡的本地方法 setpriority0()。這個本地方法是用 c 實作的,并被植入 jvm 内部,在 windows95 的平台上,這個本地方法最終将調用 win32 setpriority () api。這是一個本地方法的具體實作由 jvm 直接提供,更多的情況是本地方法由外部的動态連結庫(external dynamic link library)提供,然後被 jvm 調用。

目前該方法使用的越來越少了,除非是與硬體有關的應用,比如通過 java 程式驅動列印機或者 java 系統管理生産裝置,在企業級應用中已經比較少見。因為現在的異構領域間的通信很發達,比如可以使用 socket 通信,也可以使用 web service 等等,不多做介紹。

本地方法棧

java虛拟機棧用于管理java方法的調用,而本地方法棧用于管理本地方法的調用

本地方法棧,也是線程私有的

允許被實作成固定或者是可動态擴充的記憶體大小。(在記憶體溢出方面是相同的)

如果線程請求配置設定的容量超過本地方法棧允許的最大容量,java 虛拟機将會抛出一個 stackoverflowerror 異常。

如果本地方法可以動态擴充,并且在嘗試擴充的時候無法申請到足夠的記憶體,或者在建立新的線程時沒有足夠的記憶體去建立對應的本地方法棧,那麼 java 虛拟機将會抛出一個 outofmemoryerror 異常。

本地方法是使用c語言實作的

它的具體做法是 native method stack 中登記 native 方法,在 execution engine 執行時加載本地方法庫。

當某個線程調用一個本地方它就進入了一個全新的并且不再受虛拟機限制的世界。它和虛拟機擁同樣的權限。

本地方法可以通過本地方法接口來通路虛拟機内部的運作時資料區

它甚至可以直接使用本地處理器中的寄存器

直接從本地記憶體的堆中配置設定任意數量的記憶體

并不是所有的 jvm 都支援本地方法。因為 java 虛拟機規範并沒有明确要求本地方法的使用語言、具體實作方式、資料結構等。如果 jvm 産品不打算支援 native 方法,也可以無需實作地方法棧

在 hotspot jvm 中,直接将本地方法和虛拟機合二為一

一個 jvm 執行個體隻存在一個堆記憶體,堆也是 java 記憶體管理的核心區域。

java 堆區在 jvm 啟動的時候即被建立,其空間大小也就确定了。是 jvm 管理的最大一塊記憶體空間。堆記憶體的大小是可以調節的。

《java 虛拟機規範》規定,堆可以處于實體上不連續的記憶體空間中,但在邏輯上它應該被視為連續的。

所有的線程共享 java 堆,在這裡還可以劃分線程私有的緩沖區(thread local allocation buffer, tlab)。

《java虛拟機規範》中對java堆的描述是:所有的對象執行個體以及數組都應當在運作時配置設定在堆上。

“幾乎”所有的對象執行個體都在這裡配置設定記憶體。----從實際的角度看的

數組和對象可能永遠不會存儲在棧上,因為棧幀中儲存引用,這個引用指向對象或者數組在堆中的位置。

在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾回收時才會被移除。

堆,是gc(garbage collection)執行垃圾回收的重點區域。

運作時資料區02--本地方法棧、本地方法接口、堆

現代垃圾收集器大部分都基于分代收集理論設計,堆空間細分為:

運作時資料區02--本地方法棧、本地方法接口、堆

堆空間的内部結構(jdk7&jdk8)

運作時資料區02--本地方法棧、本地方法接口、堆
運作時資料區02--本地方法棧、本地方法接口、堆

java 堆區用于存儲 java 對象執行個體,那麼堆的大小在 jvm 啟動時就已經設定好了,大家可以通過選項“-xmx“和“-xms“來進行設定。

“-xms“用于表示堆區的起始記憶體,等價于-xx: initialheapsize

“-xmx“則用于表示堆區的最大記憶體,等價于-xx: maxheapsize

一旦堆區中的記憶體大小超過“-xmx“所指定的最大記憶體時,将會抛出 outofmemoryerror 異常。

通常會将-xms 和 -xmx 兩個參數配置相同的值,其目的是為了能夠在 java 垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小,進而提高性能。

預設情況下, 初始記憶體大小:實體電腦記憶體大小1/64

最大記憶體大小:實體電腦記憶體大小1/4

存儲在jvm中的java對象可以被劃分為兩類:

一類是生命周期較短的瞬時對象,這類對象的建立和消亡都非常迅速

另一類對象的生命周期卻非常長,在某些極端的情況下還能夠與jvm生命周期保持一緻。

java 堆區進一步細分的話,可以劃分為年輕代(younggen)和老年代(oldgen)其中年輕代又可以劃分為 eden 空間、survivor0 空間和 survivor1 空間(有時也叫做 from 區、to 區)。

運作時資料區02--本地方法棧、本地方法接口、堆

新生代和老年代的比例一般為1:2,預設- xx:newratio=2,表示新生代占1,老年代占2,新生代占整個堆的1/3。

在 hotspot 中,eden 空間和另外兩個 survivor空間預設所占的比例是 8:1:1 當然開發人員可以通過選項“-xx: survivorratlo”調整這個空間比例。比如-xx: survivorratio=8

幾乎所有的 java 對象都是在 eden 區被 new 出來的。

絕大部分的 java 對象的銷毀都在新生代進行了。

ibm 公司的專門研究表明,新生代中 80%的對象都是“朝生夕死”的

可以使用選項-xmn“設定新生代最大記憶體大小

這個參數一般使用預設值就可以了。

為新對象配置設定記憶體是一件非常嚴謹和複雜的任務,jvm 的設計者們不僅需要考慮記憶體如何配置設定、在哪裡配置設定等問題,并且由于記憶體配置設定算法與記憶體回收算法密切相關,是以還需要考慮 gc 執行完記憶體回收後是否會在記憶體空間中産生記憶體碎片。

new 的對象先放伊甸園區。此區有大小限制。

當伊甸園的空間填滿時,程式又需要建立對象,jvm 的垃圾回收器将對伊甸園區進行垃圾回收(minor gc),将伊甸園區中的不再被其他對象所引用的對象進行銷毀。再加載新的對象放到伊甸園區

然後将伊甸園中的剩餘對象移動到幸存者0區

如果再次觸發垃圾回收,此時上次幸存下來的放到幸存者 0 區的,如果沒有回收,就會放到幸存者 1 區。

如果再次經曆垃圾回收,此時會重新放回幸存者 0 區,接着再去幸存者 1 區。

啥時候能去養老區呢?可以設定次數。預設是 15 次。

可以設定參數:<code>--xx:maxtenuringthreshold=&lt;n&gt;</code>進行設定

在養老區,相對悠閑。當養老區記憶體不足時,再次觸發 gc: major gc,進行養老區的記憶體清理

若養老區執行了 maior gc 之後發現依然無法進行對象的儲存,就會産生 oom 異常

java.lang.outofmemoryerror:java heap space

運作時資料區02--本地方法棧、本地方法接口、堆

針對幸存者s0,s1區的總結:複制之後有交換,誰空誰是to。

關于垃圾回收:頻繁發生在新生區,很少在養老區收集,幾乎不在永久區/元空間收集。

運作時資料區02--本地方法棧、本地方法接口、堆

jvm 在進行 gc 時,并非每次都對上面三個記憶體(新生代、老年代、方法區)區域一起回收的,大部分時候回收的都是指新生代。

針對 hotspot vm 的實作,它裡面的 gc 按照回收區域又分為兩大種類型:一種是部分收集(partial gc),一種是整堆收集(full gc)

部分收集:不是完整收集整個java 堆的垃圾收集。其中又分為

新生代收集(minor gc/ young gc):隻是新生代的垃圾收集

老年代收集 (major gc/old gc):隻是老年代的垃圾收集。

目前,隻有 cms gc 會有單獨收集老年代的行為。

注意,很多時候 major gc 會和 full gc 混淆使用,需要具體分辨是老年代回收還是整堆回收。

混合收集(mixed gc):收集整個新生代以及部分老年代的垃圾收集。

目前,隻有 g1 gc 會有這種行為

整堆收集(full gc):收集整個 java 堆和方法區的垃圾收集。

年輕代gc(minor gc)觸發機制:

當年輕代空間不足時,就會觸發 minor gc,這裡的年輕代滿指的是 eden 代滿,survivor 滿不會引發 gc。(每次 minor gc 會清理年輕代的記憶體。)

因為 java 對象大多都具備朝生タ滅的特性,是以 minor gc 非常頻繁,一般回收速度也比較快。這一定義既清晰又易于了解

minor gc會引發 stw,暫停其它使用者的線程,等垃圾回收結束,使用者線程才恢複運作。

運作時資料區02--本地方法棧、本地方法接口、堆

老年代gc(major gc/full gc)觸發機制:

指發生在老年代的 gc,對象從老年代消失時,我們說“major gc”或“full gc”發生了。

出現了 major gc,經常會伴随至少ー次的 minor gc(但非絕對的,在 parallel scavenge 收集器的收集政策裡就有直接進行 major gc 的政策選擇過程)

也就是在老年代空間不足時,會先嘗試觸發 minor gc。如果之後空間還不足則觸發 major gc

major gc 的速度一般會比 minor gc慢10倍以上,stw 的時間更長。

如果 major gc 後,記憶體還不足,就報 oom 了。

full gc觸發機制:(後面詳細介紹)

觸發 fu11gc 執行的情況有如下五種:

調用 system.gc()時,系統建議執行 full gc,但是不必然執行

老年代空間不足

方法區空間不足

通過 minor gc 後進入老年代的平均大小大于老年代的可用記憶體

由 eden 區、survivor space0 (from space)區向 survivor space1 (to space)區複制時,對象大小大于 to space 可用記憶體,則把該對象轉存到老年代,且老年代的可用記憶體小于該對象大小

說明:full gc 是開發或調優中盡量要避免的。這樣暫時時間會短一些。

為什麼需要把java堆分代?不分代就不能正常工作了嗎?

經研究,不同對象的生命周期不同。70%-99%的對象都是臨時對象。

新生代:有eden、兩塊大小相同的survivor構成,to總為空

老年代:存放新生代中經曆多次gc仍然存活的對象

其實不分代完全可以,分代的唯一理由就是優化 gc 性能。如果沒有分代,那所有的對象都在一塊,就如同把一個學校的人都關在一個教室。gc 的時候要找到哪些對象沒用這樣就會對堆的所有區域進行掃描。而很多對象都是朝生夕死的,如果分代的話,把新建立的對象放到某一地方,當 gc 的時候先把這塊存儲“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。

如果對象在 eden 出生并經過第一次 minor gc 後仍然存活,并且能被 survivor 容納的話,将被移動到 survivor 空間中,并将對象年齡設為 1。對象在 survivor 區中每熬過一次 minorgc,年齡就增加 1 歲,當它的年齡增加到一定程度(預設為 15 歲,其實每個 jvm、每個 gc 都有所不同)時,就會被晉升到老年代中

對象晉升老年代的年齡門檻值,可以通過選項-xx: maxtenuringthreshold 來設定

針對不同年齡段的對象配置設定原則如下所示:

優先配置設定到 eden

大對象直接配置設定到老年代

盡量避免程式中出現過多的大對象

長期存活的對象配置設定到老年代

動态對象年齡判斷

如果 survivor 區中相同年齡的所有對象大小的總和大于 survivor 空間的一半,年齡大于或等于該年齡的對象可以直接進入老年代,無須等到 maxtenuringthreshold 中要求的年齡。

空間配置設定擔保

-xx: handlepromotionfailure

堆區是線程共享區域,任何線程都可以通路到堆區中的共享資料

由于對象執行個體的建立在 jvm 中非常頻繁,是以在并發環境下從堆區中劃分記憶體空間是線程不安全的

為避免多個線程操作同一位址,需要使用加鎖等機制,進而影響配置設定速度

從記憶體模型而不是垃圾收集的角度,對 eden 區域繼續進行劃分,jvm 為每個線程配置設定了一個私有緩存區域,它包含在 eden 空間内。

多線程同時配置設定記憶體時,使用 tlab 可以避免一系列的非線程安全問題,同時還能夠提升記憶體配置設定的吞吐量,是以我們可以将這種記憶體配置設定方式稱之為快速配置設定政策。

據我所知所有 open jdk 衍生出來的 jvm 都提供了 tlab 的設計。

運作時資料區02--本地方法棧、本地方法接口、堆

盡管不是所有的對象執行個體都能夠在 tlab 中成功配置設定記憶體,但 jvm 确實是将tlab 作為記憶體配置設定的首選

在程式中,開發人員可以通過選項“-xx: usetlab”設定是否開啟tlab 空間

預設情況下,tlab 空間的記憶體非常小,僅占有整個 eden 空間的 1%,當然我們可以通過選項“-xx: tlabwastetargetpercent”設定 tlab 空間所占用 eden 空間的百分比大小。

一旦對象在 tlab 空間配置設定記憶體失敗時,jwm 就會嘗試着通過使用加鎖機制確定資料操作的原子性,進而直接在 eden 空間中配置設定記憶體。

運作時資料區02--本地方法棧、本地方法接口、堆
運作時資料區02--本地方法棧、本地方法接口、堆
運作時資料區02--本地方法棧、本地方法接口、堆

在發生 minor gc 之前,虛拟機會檢查老年代最大可用的連續空間是否大于新生代所有對象的總空間。

如果大于,則此次 minor gc 是安全的

如果小于,則虛拟機會檢視-xx: handlepromotionfailure 設定值是否允許擔保失敗。

如果 handlepromotionfailure=true,那麼會繼續檢査老年代最大可用連續空間是否大于曆次晉升到老年代的對象的平均大小。

如果大于,則嘗試進行一次 minor gc,但這次 minor gc 依然是有風險的

如果小于,則改為進行一次 full gc。

如果 handlepromotionfailure= false,則改為進行一次 full gc

在 jdk6 update24 之後,handlepromotionfailure 參數不會再影響到虛拟機的空間配置設定擔保政策,觀察 openjdk 中的源碼變化,雖然源碼中還定義了handlepromotionfailure 參數,但是在代碼中已經不會再使用它。jdk6 update 24 之後的規則變為隻要老年代的連續空間大于新生代對象總大小或者曆次晉升的平均大小就會進行 minor gc,否則将進行 full g。

在《深入了解 java 虛拟機》中關于 java 堆記憶體有這樣一段描述:

随着 java 編譯期的發展與逃逸分析技術逐漸成熟,棧上配置設定、标量替換優化技術将會導緻一些微妙的變化,所有的對象都配置設定到堆上也漸漸變得不那麼“絕對”了。

在 java 虛拟機中,對象是在 java 堆中配置設定記憶體的,這是一個普遍的常識。但是,有種特殊情況,那就是如果經過逃逸分析(escape analysis)後發現,一個對象并沒有逃逸出方法的話,那麼就可能被優化成棧上配置設定。這樣就無需在堆上配置設定記憶體,也無須進行垃圾回收了。這也是最常見的堆外存儲技術。

此外,前面提到的基于 openjdk 深度定制的 taobao vm,其中創新的 gcih (gc invisible heap)技術實作。off-heap,将生命周期較長的 java 對象從 heap 中移至 heap 外,并且 gc 不能管理 gcih 内部的 java 對象,以此達到降低 gc 的回收頻率和提升 gc 的回收效率的目的。

如何将堆上的對象配置設定到棧,需要使用逃逸分析手段。

這是一種可以有效減少 java 程式中同步負載和記憶體堆配置設定壓力的跨函數全局資料流分析算法

通過逃逸分析,java hotspot 編譯器能夠分析出一個新的對象的引用的使用範圍進而決定是否要将這個對象配置設定到堆上

逃逸分析的基本行為就是分析對象動态作用域:

當一個對象在方法中被定義後,對象隻在方法内部使用,則認為沒有發生逃逸。

當一個對象在方法中被定義後,它被外部方法所引用,則認為發生逃逸。例如作為調用參數傳遞到其他地方中。

運作時資料區02--本地方法棧、本地方法接口、堆

沒有發生逃逸的對象,則可以配置設定到棧上,随着方法執行的結束,棧空間就被移除。

運作時資料區02--本地方法棧、本地方法接口、堆

上述代碼如果想要stringbuffer sb不逃出方法,可以這樣寫:

運作時資料區02--本地方法棧、本地方法接口、堆
運作時資料區02--本地方法棧、本地方法接口、堆

參數設定

在 jdk6u23 版本之後,hotspot 中預設就已經開啟了逃逸分析。如果使用的是較早的版本,開發人員則可以通過

選項“-xx: + doescapeanalysis“顯式開啟逃逸分析

通過選項“-xx:+ printescapeanalysis“看逃逸分析的篩選結果。

結論:

開發中能使用局部變量的,就不要使用在方法外定義。

使用逃逸分析,編譯器可以對代碼做如下優化:

棧上配置設定。将堆配置設定轉化為棧配置設定。如果一個對象在子程式中被配置設定,要使指向該對象的指針永遠不會逃逸,對象可能是棧配置設定的候選,而不是堆配置設定

同步省略。如果一個對象被發現隻能從一個線程被通路到,那麼對于這個對象的操作可以不考慮同步

分離對象或标量替換。有的對象可能不需要作為一個連續的記憶體結構存在也可以被通路到,那麼對象的部分(或全部)可以不存儲在記憶體,而是存儲在 cpu 寄存器中

jit編譯器在編譯期間根據逃逸分析的結果,發現如果一個對象并沒有逃逸出方法的話,就可能被優化成上配置設定。配置設定完成後,繼續在調用内執行,最後線程結束,棧空間被回收,局部變量對象也被回收。這樣就無須進行垃圾回收了。

常見的上配置設定的場景

在逃逸分析中,已經說明了。分别是給成員變量指派、方法傳回值、執行個體引用傳遞。

線程同步的代價是相當高的,同步的後果是降低并發性和性能

在動态編譯同步塊的時候,jit 編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對象是否隻能夠被一個線程通路而沒有被釋出到其他線程。如果沒有,那麼 jit 編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高并發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除

如下代碼:

運作時資料區02--本地方法棧、本地方法接口、堆

代碼中對 hollis 這個對象進行加鎖,但是 hollis 對象的生命周期隻在 f()方法中,并不會被其他線程所通路到,是以在 jit 編譯階段就會被優化掉。優化成:

運作時資料區02--本地方法棧、本地方法接口、堆

标量(scalar)是指一個無法再分解成更小的資料的資料。java 中的原始資料類型就是标量。

相對的,那些還可以分解的資料叫做聚合量(aggregate), java 中的對象就是聚合量,因為他可以分解成其他聚合量和标量。

在 jit 階段,如果經過逃逸分析,發現一個對象不會被外界通路的話,那麼經過 jvm 優化,就會把這個對象拆解成若幹個其中包含的若幹個成員變量來代替。這個過程就是标量替換

運作時資料區02--本地方法棧、本地方法接口、堆

以上代碼經過标量替換後,就會變成:

運作時資料區02--本地方法棧、本地方法接口、堆

可以看到,point 這個聚合量經過逃逸分析後,發現他并沒有逃逸,就被替換成兩個聚合量了。那麼标量替換有什麼好處呢?就是可以大大減少堆記憶體的占用。因為旦不需要建立對象了,那麼就不再需要配置設定堆記憶體了

标量替換為棧上配置設定提供了很好的基礎。

标量替換參數設定

參數 xx: +elimilnateallocations:開啟了标量替換(預設打開),允許将對象打散配置設定在棧上。

上述代碼在主函數中進行了 1 億次 alloc。調用進行對象建立,由于 user 對象執行個體需要占據約 16 位元組的空間,是以累計配置設定空間達到将近 1.5gb。如果堆空間小于這個值,就必然會發生 gc。使用如下參數運作上述代碼:

關于逃逸分析的論文在 1999 年就已經發表了,但直到 jdk1.6 才有實作,而且這項技術到如今也并不是十分成熟的。

其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經過逃逸分析可以做标量替換、棧上配置設定、和鎖消除。但是逃逸分析自身也是需要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。

一個極端的例子,就是經過逃逸分析之後,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。

雖然這項技術并不十分成熟,但是它也是即時編譯器優化技術中一個十分重要的手段。

注意到有一些觀點,認為通過逃逸分析,jvm 會在棧上配置設定那些不會逃逸的對象,這在理論上是可行的,但是取決于 jvm 設計者的選擇。據我所知,oracle hotspot jvm 中并未這麼做,這一點在逃逸分析相關的文檔裡已經說明,是以可以明确所有的對象執行個體都是建立在堆上。

目前很多書籍還是基于 jdk7 以前的版本,jdk 已經發生了很大變化,intern 字元串的緩存和靜态變量曾經都被配置設定在永久代上,而永久代已經被中繼資料區取代。但是, intern 字元串緩存和靜态變量并不是被轉移到中繼資料區,而是直接在堆上配置設定,是以這一點同樣符合前面一點的結論:對象執行個體都是配置設定在堆上

年輕代是對象的延生、成長、消亡的區域,一個對象在這裡産生、應用,最後被垃圾回收器收集、結束生命。

老年代放置長生命周期的對象,通常都是從 survivor 區域篩選拷貝過來的 java 對象。當然,也有特殊情況,我們知道普通的對象會被配置設定在 tlab 上;如果對象較大,jvm 會試圖直接配置設定在 eden 其他位置上;如果對象太大,完全無法在新生代找到足夠長的連續空閑空間,jvm 就會直接配置設定到老年代。

當 gc 隻發生在年輕代中,回收年輕代對象的行為被稱為 minor gc。當 gc 發生在老年代時則被稱為 major gc 或者 full gc。一般的,minor gc 的發生頻率要比 major gc 高很多,即老年代中垃圾回收發生的頻率将大大低于年輕代。