5.JVM內存管理
JAVA虛擬機在執行java程序的過程中,會把它管理的內存分成若干個不同的數據區域。
------------------------------------------------------------------------------------—
| 運行時數據區 |
| ----------- -------- ----------------- |
| | 方法區 | | 棧 | | 本地方法棧 | |
| | | | | | | |
| ----------- -------- ----------------- |
| |
| --------------------------- ----------------- |
| | 堆 | | 程序計數器 | |
| | | | | |
| --------------------------- ----------------- |
| ?? ?? ?? ?? |
| ---------------------------- ------------------ ----------------- |
| | 執行引擎 | | 本地庫接口 | ?? | 本地方法庫 | |
| | | | | | | |
| ---------------------------- ------------------ ----------------— |
|
| 其中,堆和方法區,是所有線程共有區;
| 棧,本地方法棧,程序計數器,是線程私有區。
|------------------------------------------------------------------------------------
(1) 內存區域
a.程序計數器(Program Counter Register),線程私有,不會拋出任何內存異常
I.可以這么理解,當前線程所執行字節碼的行號指示器。字節碼解釋器,就是通過程序計數器的值,來選取下一條要執行的字節碼指令(分支、循環、跳轉等基礎功能都需要依賴計數器)。
II.java虛擬機的多線程是通過,各個線程之間輪流切換并分配內存來實現的。在任何一個確定的時刻,一個處理器都只會執行一條線程中的指令。因此,為了線程切換回來之后能夠
恢復到正確的執行位置。每條線程都需要一個獨立的程序計數器。
III.如果線程正在執行的是一個java方法,計數器記錄的是正在執行的虛擬機字節碼指令的地址;
如果正在執行的是native方法,計數器的值為空。
b.java虛擬機棧 (Java Virtual Machine Stack) , 線程私有,會有 StackOverFlow 和 OutOfMemoryError異常 ,通過-Xss分配內存大小
I.每個方法在執行時,都會創建一個棧幀(Stack Frame),用于存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。
每一個方法從調用到執行完成的過程,就對應一個 棧幀 在虛擬機棧中,入棧到出棧的過程。
II.棧中的局部變量表,所需的內存空間,在編譯器間完成分配。在進入一個方法時,這個方法需要在幀(Stack Frame) 中分配多大的內存空間是確定的,在這個方法運行期間,
不會 改變 局部變量表 所占用 內存空間 的大小。
III.當java啟動一個線程時,虛擬機會計算出這個線程所需要的棧深度,(比如10),當線程請求的棧深度(每調一個方法壓一個棧幀,用掉一個棧深度),大于虛擬機給Stack分配的
棧深度,會拋出StackOverFlow異常。(用javap javap -verbose Test 查看程序的字節碼,Code 屬性, stack=2 , 可以查看運行的詳細過程,包括棧深度,和每個棧幀需要多少個slot)
當一個可擴展棧(棧有可擴展的有固定長度的,由使用的JAVA虛擬決定的),動態擴展時,無法請求到足夠的內存(比如我需要10M內存,但是JVM只給我5M),會拋出,
OutOfMemoryError異常。
c.本地方法棧 (Native Method Stack) ,線程私有,會有 StackOverFlow 和 OutOfMemoryError異常,通過-Xss分配內存大小
I.和java虛擬機棧基本一樣。區別不過是,
JVM Stack 為 虛擬機 執行java方法 服務;
Native Method Stack 為 虛擬機 執行本地方法服務
II.也會拋出StackOverFlow 和 OutOfMemoryError異常
d.java堆 , 線程共享 , 會拋出OutOfMemoryError異常。,通過-Xms分配內存最小值,-Xmx分配內存最大值
I.存放 對象實例 和 數組。
II.是垃圾回收的主要區域。
III. java堆,可以處于物理上的不連續空間,邏輯上連續即可。可動態擴展,通過-Xms控制大小。
IV.如果堆中沒有完成內存分配,并且堆也無法擴展是,將會拋出OutOfMemoryError異常。
e.方法區 , 線程共享 , 會拋出OutOfMemoryError異常。
I.用于存儲,已經被虛擬機加載的,類的信息、常量、靜態變量、編譯后的代碼等數據
II.不需要連續的內存,可以選擇固定大小和可擴展
III.當方法區無法完成內存分配需求時,會拋出OutOfMemoryError異常。
e-slave. 運行時常量池,會拋出OutOfMemoryError異常。
是方法區的一部分。
I.Class文件中,除了有類的 版本、 字段、方法、接口、等描述信息外,還有一項就是常量池(Constant Pool Table),用于存放編譯期生成的,
各種字面變量和符號引用(),這部分將在類加載后進入方法區的常量池。
II.并非預置在Class文件中常量池中的內容,才能進入方法區;運行期間也可能將新的常量放入池中。
f.直接內存,并不是java虛擬機內存的一部分,而是機器內存的一部分,會拋出OutOfMemoryError異常
I.NIO引入了一種類似于 通道(Channel) 和 緩沖區(Buffer) ,可以使用Native函數庫直接分配堆外內存。
然后,通過一個存儲在java堆中的,DirectByteBuffer對象作為這塊內存的引用進行操作。
這樣避免了在java堆和native堆中來回復制數據,提高了性能。
II.當直接內存和JVM內存之和大于機器內存時,拋出OutOfMemoryError內存。
(2)對象創建細節
a.內存分配方式
I.指針碰撞, 如果java堆中內存是絕對規整的,為對象分配空間的任務,等同于把一塊確定大小的內存從java堆中劃分出來。
如果java堆中內存是絕對規整的,所有用過的內存放在一邊,空閑的內存放在另一邊,中間放著一個指針作為臨界點,那么分配內存就是,
把指針向空閑空間那邊挪動等同于對象大小的距離,這種分配方式成為 指針碰撞。
II.空閑列表, 如果內存是不規則的,虛擬機就必須維護一個表,記錄那些內存塊是可用的,分配的時候找到一塊足夠大的內存塊分給對象實例,
并更新列表的記錄,這種方式稱為 空閑列表(Free List)
b.選擇哪種分配方式是java堆是否規整決定的,java堆是否規整,是由采用的垃圾收集器是否帶有壓縮功能決定的。因此,
使用Serial、ParNew燈光帶有壓縮(Compact)過程的收集器時,系統采用的分配算法是指針碰撞。
使用CMS這種基于 Mark-Sweep(標記-移除) 算法的收集器,系統采用的分配算法是空閑列表。
c.空閑列表問題,及解決方案
問題:
首先,堆是線程共有的,所以,當多線程創建對象是,有這樣一個問題,當Thread A分配一塊內存完成后,還沒更新列表,這時Thread B給
自己的對象分配了同一塊內存,這就造成了沖突。
方案:
I.對分配內存的動作,進行同步,這種造成性能下降。
II.每個線程在堆中,預先分配一小塊內存作為緩沖區,稱為(Thread Local Allocation Buffer , TLAB) ,哪個線程需要給自己的對象分配
內存,就在自己的TLAB上分配,只有自己的TLAB上分配完了,才需要同步鎖定。通過-XX:+/-UseTLAB參數來設定。(性能調優)
d.內存分配完成后,虛擬機將分配到的內存空間初始化為零值。這一步操作保證了Java代碼中可以不賦初始值就可以使用
e.接下來,虛擬機對對象進行必要的設置,例如這個對象是哪個類的實例、對象的hash-code、對象的GC分代年齡,等信息。存在對象頭中。
這樣從JVM的角度來說,對象創建完成。
f.對象的內存布局,對象在內存中的存儲可以分為3塊區域:
I.對象頭 (Header) : 包括兩部分信息,
第一部分,存儲對象自身運行時數據,如HashCode,GC分代年齡,鎖定狀態標志,線程持有的鎖。
第二部分,類型指針,即對象指向它的 類 元數據的指針,虛擬機通常用這個指針確定對象屬于哪個類。
還有記錄數組長度的信息。
II.實例數據 (Instance Data) : 程序定義的個字段的內容。包括父類和子類。
III.對齊填充 (Padding) : 占位符,換句話說,就是保證對象大小必須是 8字節(byte) 的整數倍
g.對象的訪問定位
java程序需要使用棧上面的reference,引用數據來操作堆上的具體對象。
I. 句柄
這種方式,Java堆中會分配一塊內存,作為句柄池,reference存儲的就是對象句柄池地址。
句柄中包含了對象實例數據 (在堆上),和類型數據(類數據,在常量池)具體地址信息。
好處:GC后reference不需要修改
II.直接指針
reference存儲的就是對象地址。
好處:速度快,節省了一次指針定位開銷。
Sun HotSpot使用直接指針
(3)堆溢出,OutOfMemoryError 后面跟 Heap
-Xms 和 -Xmx 設置堆的最大和最小內存
堆的最小參數 -Xms 和 最大參數 -Xmx 設置為一樣,就可以避免堆擴展。
a.解決思路:
I.用內存映像分析工具(如,Eclipse Memory Analyzer) 堆Dump出來的堆轉儲快照進行分析。
II.分析的重點是確認內存中的對象是否是必要的,即先確認是否有 內存泄漏(Memory Leak,當創建
的對象沒有使用,又無法被GC回收,就是內存泄漏)
III.如果是內存泄漏,查看泄漏對象到GC Roots的引用鏈信息,就能找到泄漏對象是通過怎樣的路徑與GC Roots相關聯,
并導致GC無法自動回收他們的。通過引用鏈信息,定位到泄漏代碼的位置,review代碼。
IV.如果沒有內存泄漏,即,內存中的對象都必須存活。那就看虛擬機堆參數(-Xms和-Mmx)和內存相比,看是否還可以調大。
從代碼上檢查是否有,某些對象生命周期過長,持有時間過長的情況,嘗試優化這些代碼,從而減少運行期的內存消耗。
(4)棧溢出 StackOverFlow
-Xss設置棧占用內存大小。默認1024K,也就是1M
a.虛擬機啟動時,有棧大小的默認參數,當所有的棧幀(Stack Frame),內存加起來超過棧內存大小時,就會拋出StackOverFlow異常。
在棧深度,默認情況下,大多數棧深度達到1000-2000幀沒有問題,對于普通遞歸是夠用了(但是棧幀大小是不確定的,所以,只能是大多數情況下。)
b.建立線程數量過多,導致內存溢出
I.操作系統,分配給每個進程的內存是有限制的。如果給一個java分配了1G內存,
虛擬機提供了參數,來控制堆和方法區所占用內存大小,如果沒有指定棧占用的內存大小,忽略其它,剩余的內存 1G - 堆內存 - 方法區內存,被本地方法棧和虛擬機棧
瓜分,棧是線程私有的,棧分配的內存越大,可以建立的線程數就越少,建立新線程時候,容易把剩下的內存耗盡。這種情況,可以減少最大堆,和減少棧容量,換取更多
的線程,避免內存溢出。
(5)方法區,內存溢出 。OutOfMemoryError后面跟隨PermGen
-XX:PerSize 和 -XX:MaxPermSize限制方法區大小
String.intern()是一個native方法,作用:如果字符串常量池中,已經包含一個等于此String 對象的字符串,則返回常量池中,代表此字符串的對象。
否則,將此String對象添加到常量池中。
a.Spring Hibernate在對類進行增強時,都會使用到CGLib這類字節碼技術,增強的類越多,就需要越大的方法區,容易導致方法區的內存溢出。
b.JSP第一次運行時,要編譯成java類,大量的jsp也有可能導致方法區內存溢出。
(6) 本機直接內存溢出 OutOfMemoryError Unsafe.allocateMemory
DirectMemory 容量可以通過:-XX:MaxDirectMemorySize指定,如果不指定,則默認與java堆最大值 (-Xmx)一樣。
如果內存溢出,在堆的Dump文件很小,或者沒有明顯的異常,又或者程序中使用了NIO,可以考慮是 本機直接內存溢出。
轉載于:https://www.cnblogs.com/fubaizhaizhuren/p/5938480.html