1.Java內存區域與內存溢出異常
運行時數據區:
主要分爲線程私有和線程共享兩大塊。
線程私有(生命週期與線程相同,共生死):
- Java虛擬機棧(NativeStack)
- 本地方法棧(VM Stack)
- 程序計數器(PC)
線程共享(生命週期與JVM相同):
- 堆(heap)
- 方法區(Method area,邏輯分開,物理屬於堆)
程序計數器:
是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器.
記錄正在執行的虛擬機字節碼指令地址(如果正在執行的本地方法則爲空)
Java虛擬機棧(-Xss: 設置上限):
虛擬機棧描述的是Java方法執行的內存模式:每個方法在執行的同時都會創建一個棧幀(Stack Frame),用來存儲局部變量表,操作數棧,動態鏈接,方法出口的等信息.每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程.
局部變量表存放了編譯期可知的各種基本數據類型(8個基本類型),對象引用(reference類型)和returnAddress類型
這個區域規定了兩種異常:StackOverflowError(線程請求棧深入大於虛擬機棧的深度)異常,如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋OutOfMemoryError
本地方法棧:
本地方法棧與虛擬機棧幾乎一樣,只是本地方法棧執行的是native方法,虛擬機棧執行的是Java方法
堆(-Xmx:設置上限,-Xms:設置下限):
Java堆是Java虛擬機管理的內存最大一塊,此內存區域存儲對象實例以及數組.Java堆是垃圾收集器管理的主要區域,又叫GC堆.
Java堆分爲: 年輕代和老年代
年輕代分爲三個區Eden,Form Survivor,To Survivor(默認比例爲8:1),
從內存角度來看,Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer ,TLAB),又叫線程工作區,
堆可以在物理上不連續,但邏輯上要連續,當堆中無法分配內存給實例對象,將拋出OutOfMemoryError
方法區(-XX:MaxPermSize設置上限,-XX:PermSize設置下限):
它存放虛擬機加載的Class信息,常量,靜態變量,即時編譯器編譯後的代碼等數據;方法區又放在永久代(jdk8開始,永久代移除,方法區存放在本地內存,Native memory),當方法區,無法分配將拋出OutOfMemoryError
運行時常量池:
Runtime Constant Pool是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中,注意運行期也可能將新的常量放入池中,如String類的intern()方法.當常量池無法申請內存時拋出OutOfMemoryError
直接內存(-XX:MaxDirectMemorySize上限,默認與-Xmx一樣):
不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範定義的內存區域.但這部分內存也頻繁地使用,也可能導致OutOfMemoryError異常.
JDK1.4新加入NIO類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存(Native堆),然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作.這樣能在一些場景提高性能,避免Java堆和Native堆中來回複製數據,Native堆不會受到Java堆大小控制,但受本機內存大小限制,
2.垃圾收集器與內存分配策略
線程私有內存:線程結束就自動回收,和線程生命週期一致,
所以垃圾回收僅指回收線程共享內存中的堆。
判斷對象是否已死的算法:
- 引用計數算法:給對象添加一個計數器,每當有一個地方引用它,計數器就1;當引用失效時,計數器值就減1;計數器爲0說明該對象即將被回收.好處:簡單。缺點:不能解決對象之間相互循環引用問題。
- 可達性分析算法:通過一系列的稱爲GC Roots的對象作爲起點,從這些節點開始向下搜索,搜索所走的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
可作爲GC Roots的對象包括以下幾種:
- 虛擬機棧中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(既一般說的Native方法)引用的對象
引用分類
- 強引用(StrongReference):當虛擬機GC的時候不會回收這類對象。
Object obj = new Object();
- 軟引用(SoftReference):當虛擬機GC的時候,只有虛擬機內存不夠纔會回收此類對象。
Object obj = new Object(); SoftReference<Object> srf = new SoftReference<Object>(obj);
- 弱引用(WeakReference):當虛擬機GC的時候,不管虛擬機內存夠不夠都會回收。
Object obj = new Object();WeakReference<Object> wrf = new WeakReference<Object>(obj);
- 虛引用(PhantomReference):又稱爲幽靈引用或者幻影引用。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用取得一個對象。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被回收時收到一個系統通知。
Object obj = new Object();PhantomReference<Object> pf = new PhantomReference<Object>(obj);
方法區回收
因爲方法區主要存放永久代對象,而永久代對象的回收率比新生代低很多,因此在方法區上進行回收性價比不高。
主要是對常量池的回收和對類的卸載。
在大量使用反射、動態代理、CGLib 等 ByteCode 框架、動態生成 JSP 以及 OSGi 這類頻繁自定義 ClassLoader 的場景都需要虛擬機具備類卸載功能,以保證不會出現內存溢出。
類的卸載條件很多,需要滿足以下三個條件,並且滿足了也不一定會被卸載:
- 該類所有的實例都已經被回收,也就是堆中不存在該類的任何實例。
- 加載該類的 ClassLoader 已經被回收。
- 該類對應的 Class 對象沒有在任何地方被引用,也就無法在任何地方通過反射訪問該類方法。
可以通過 -Xnoclassgc 參數來控制是否對類進行卸載。
finalize()(此方法只調用一次,回收前調用)
finalize() 類似 C++ 的析構函數,用來做關閉外部資源等工作。但是 try-finally 等方式可以做的更好,並且該方法運行代價高昂,不確定性大,無法保證各個對象的調用順序,因此最好不要使用。
當一個對象可被回收時,如果需要執行該對象的 finalize() 方法,那麼就有可能在該方法中讓對象重新被引用,從而實現自救。自救只能進行一次,如果回收的對象之前調用了 finalize() 方法自救,後面回收時不會調用 finalize() 方法。
垃圾回收算法
主要分爲三種:標記清除算法,複製算法,標記整理算法
標記清除(Maker Sweep):
標記出要回收的對象,然後清除掉.
缺點:會空間產生內存碎片,效率不高
複製算法(Copying):
標記出存活的對象,然後把存活的對象複製到另一塊內存塊,再清理回收對象.目前常用的虛擬機把此算法應用在堆的新生代,並把堆的新生代分爲Eden,Survivor,Survivor區,因爲新生代存活對象很少,複製成本低,空間浪費較少,所以使用此算法。
HotSpot 虛擬機的 Eden 和 Survivor 的大小比例默認爲 8:1,保證了內存的利用率達到 90%。如果每次回收有多於 10% 的對象存活,那麼一塊 Survivor 空間就不夠用了,此時需要依賴於老年代進行分配擔保,也就是借用老年代的空間存儲放不下的對象。
標記整理算法(Maker-Compact):
標記出存活對象,然後清除未存活的對象,最後把剩下的存活對象往一端移動.好處解決了內存碎片問題,並且不會像複製算法浪費空間,常把該算法應用在老年代.因爲老年代不能使用複製算法,老年代存活對象比例高,剩下的10%Survivor可能裝不下。
垃圾收集器
以上是 HotSpot 虛擬機中的 7 個垃圾收集器,連線表示垃圾收集器可以配合使用。
- 單線程與多線程:單線程指的是垃圾收集器只使用一個線程進行收集,而多線程使用多個線程。
- 串行與並行:串行指的是垃圾收集器與用戶程序交替執行,這意味着在執行垃圾收集的時候需要停頓用戶程序;並行指的是垃圾收集器和用戶程序同時執行。
1.Serial收集器(新生代,串行)
Serial 翻譯爲串行,也就是說它以串行的方式執行。
它是單線程的收集器,只會使用一個線程進行垃圾收集工作。
它的優點是簡單高效,對於單個 CPU 環境來說,由於沒有線程交互的開銷,因此擁有最高的單線程收集效率。
它是 Client 模式下的默認新生代收集器,因爲在該應用場景下,分配給虛擬機管理的內存一般來說不會很大。Serial 收集器收集幾十兆甚至一兩百兆的新生代停頓時間可以控制在一百多毫秒以內,只要不是太頻繁,這點停頓是可以接受的。
2.ParNew收集器(新生代,並行)
它是 Serial 收集器的多線程版本。
是 Server 模式下的虛擬機首選新生代收集器,除了性能原因外,主要是因爲除了 Serial 收集器,只有它能與 CMS 收集器配合工作。
默認開啓的線程數量與 CPU 數量相同,可以使用 -XX:ParallelGCThreads 參數來設置線程數。
3.Parallel Scavenge收集器(新生代,並行)
與 ParNew 一樣是多線程收集器。
其它收集器關注點是儘可能縮短垃圾收集時用戶線程的停頓時間,而它的目標是達到一個可控制的吞吐量,它被稱爲“吞吐量優先”收集器。這裏的吞吐量指 CPU 用於運行用戶代碼的時間佔總時間的比值。
停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗。而高吞吐量則可以高效率地利用 CPU 時間,儘快完成程序的運算任務,適合在後臺運算而不需要太多交互的任務。
縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導致吞吐量下降。
可以通過一個開關參數打開 GC 自適應的調節策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例、晉升老年代對象年齡等細節參數了。虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。
4.SerialOld收集器(老年代,串行)
是 Serial 收集器的老年代版本,也是給 Client 模式下的虛擬機使用。如果用在 Server 模式下,它有兩大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 誕生以前)中與 Parallel Scavenge 收集器搭配使用。
- 作爲 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用。
5.Parallel Old收集器(老年代,並行)
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。
6.CMS收集器(老年代,並行)
CMS(Concurrent Mark Sweep),Mark Sweep 指的是標記 - 清除算法。
分爲以下四個流程:
- 初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,需要停頓。
- 併發標記:進行 GC Roots Tracing 的過程,它在整個回收過程中耗時最長,不需要停頓。
- 重新標記:爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,需要停頓。
- 併發清除:不需要停頓。
具有以下缺點:
- 吞吐量低:低停頓時間是以犧牲吞吐量爲代價的,導致 CPU 利用率不夠高。
- 無法處理浮動垃圾,可能出現 Concurrent Mode Failure。浮動垃圾是指併發清除階段由於用戶線程繼續運行而產生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收。由於浮動垃圾的存在,因此需要預留出一部分內存,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。如果預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機將臨時啓用 Serial Old 來替代 CMS。
- 標記 - 清除算法導致的空間碎片,往往出現老年代空間剩餘,但無法找到足夠大連續空間來分配當前對象,不得不提前觸發一次 Full GC。
7.G1收集器(並行)
G1(Garbage-First),它是一款面向服務端應用的垃圾收集器,在多 CPU 和大內存的場景下有很好的性能。HotSpot 開發團隊賦予它的使命是未來可以替換掉 CMS 收集器。
堆被分爲新生代和老年代,其它收集器進行收集的範圍都是整個新生代或者老年代,而 G1 可以直接對新生代和老年代一起回收。
G1 把堆劃分成多個大小相等的獨立區域(Region),新生代和老年代不再物理隔離。
通過引入 Region 的概念,從而將原來的一整塊內存空間劃分成多個的小空間,使得每個小空間可以單獨進行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預測的停頓時間模型成爲可能。通過記錄每個 Region 垃圾回收時間以及回收所獲得的空間(這兩個值是通過過去回收的經驗獲得),並維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region。
每個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。通過使用 Remembered Set,在做可達性分析的時候就可以避免全堆掃描。
如果不計算維護 Remembered Set 的操作,G1 收集器的運作大致可劃分爲以下幾個步驟:
- 初始標記
- 併發標記
- 最終標記:爲了修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 裏面,最終標記階段需要把 Remembered Set Logs 的數據合併到 Remembered Set 中。這階段需要停頓線程,但是可並行執行。
- 篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與用戶程序一起併發執行,但是因爲只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。
具備如下特點:
- 空間整合:整體來看是基於“標記 - 整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基於“複製”算法實現的,這意味着運行期間不會產生內存空間碎片。
- 可預測的停頓:能讓使用者明確指定在一個長度爲 M 毫秒的時間片段內,消耗在 GC 上的時間不得超過 N 毫秒。
3.內存分配與回收策略
- 對象優先分配在 Eden 區,如果 Eden 區沒有足夠的空間時,虛擬機執行一次 Minor GC。
- 大對象直接進入老年代(大對象是指需要大量連續內存空間的對象)。這樣做的目的是 避免在 Eden 區和兩個 Survivor 區之間發生大量的內存拷貝(新生代採用複製算法收集 內存)。
- 長期存活的對象進入老年代。虛擬機爲每個對象定義了一個年齡計數器,如果對象經過 了 1 次 Minor GC 那麼對象會進入 Survivor 區,之後每經過一次 Minor GC 那麼對象 的年齡加 1,直到達到閥值,對象進入老年區。
- 動態判斷對象的年齡。如果 Survivor 區中相同年齡的所有對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代。
- 空間分配擔保。每次進行 Minor GC 時,JVM 會計算 Survivor 區移至老年區的對象的 平均大小,如果這個值大於老年區的剩餘值大小則進行一次 Full GC,如果小於檢查 HandlePromotionFailure 設置,如果 true 則只進行 Monitor GC,如果 false 則進 行 Full GC。
4.JDK的命令行工具
jps:查看虛擬機進程的狀況, 如進程 ID。
jmap: 用於生成堆轉儲快照文件( 某一時刻的) 。
jhat: 對生成的堆轉儲快照文件進行分析。
jstack:用來生成線程快照( 某一時刻的) 。 生成線程快照的主要
目的是定位線程長時停頓的原因( 如死鎖, 死循環, 等待 I/O 等) ,
通過查看各個線程的調用堆棧, 就可以知道沒有響應的線程在後臺
做了什麼或者等待什麼資源。
jstat:虛擬機統計信息監視工具。 如顯示垃圾收集的情況, 內存使
用的情況。
Jconsole:主要是內存監控和線程監控。 內存監控: 可以顯示內存
的使用情況。 線程監控: 遇到線程停頓時, 可以使用這個功能。
5.JVM 常見的啓動參數
-Xms: 設置堆的最小值。
-Xmx: 設置堆的最大值。
-Xmn: 設置新生代的大小。
-Xss: 設置每個線程的棧大小。
-XX:NewSize: 設置新生代的初始值。
-XX:MaxNewSize : 設置新生代的最大值。
-XX:PermSize: 設置永久代的初始值。
-XX:MaxPermSize: 設置永久代的最大值。
-XX:SurvivorRatio: 年輕代中 Eden 區與 Survivor 區的大小比值。
-XX:PretenureSizeThreshold: 令大於這個設置值的對象直接在老年代分配
問題總結:
1.內存泄露的解決方案?
- 避免在循環中創建對象。
- 儘早釋放無用對象的引用。 ( 最基本的建議)
- 儘量少用靜態變量, 因爲靜態變量存放在永久代( 方法區) , 永久代基本不參與垃圾回收。
- 使用字符串處理, 避免使用 String, 應大量使用 StringBuffer, 每一個 String
2.在實際場景中,你怎麼查找內存泄露?
可以使用 Jconsole。
…
參考書籍<<深入理解Java虛擬機>>