JVM 內(nèi)存分布
- 線程共享數(shù)據(jù)區(qū):
方法區(qū)->類信息,靜態(tài)變量
堆->數(shù)組對象 - 線程隔離區(qū)
虛擬機棧-> 方法
本地方法棧->本地方法庫 native - 堆、程序計數(shù)器
-
JVM 運行數(shù)據(jù)
程序計數(shù)器
線程隔離 ,比較小的內(nèi)存空間,當(dāng)前線程所執(zhí)行的字節(jié)碼的行號
線程是一個獨立的執(zhí)行單元,由 CPU執(zhí)行
唯一沒有 OOM 的地方,由虛擬機維護(hù),所以不會出現(xiàn) OOM
虛擬機棧
執(zhí)行的是Java方法
方法的調(diào)用就是棧幀入虛擬機棧的過程
棧幀:局部變量表(變量) 、操作數(shù)棧(存放a+b的結(jié)果 )、 動態(tài)鏈接(對對象引用的地址),方法出口(return的值)
線程請求的棧深度大于虛擬機所允許的深度StackOverflowError
本地方法棧
執(zhí)行的是 native 方法的一塊 java內(nèi)存區(qū)域,一樣有 棧幀
hotspot將 Java 虛擬機棧和本地方法棧合二為一
jvm標(biāo)準(zhǔn)是 java 虛擬機棧和本地方法棧分開
堆
java內(nèi)存中存放對象實例的區(qū)域,幾乎所有的對象實例都在這里分配
所有線程共享
新生代、老年代
jmap -heap pid;
方法區(qū)
各個線程共享的內(nèi)存區(qū)域
存儲已被虛擬機加載的類的信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)
Hotspot用永久代實現(xiàn)方法區(qū)(讓垃圾回收器可以管理方法區(qū)),對常量池的回收和卸載
方法區(qū)會拋出 OOM,當(dāng)他無法滿足內(nèi)存分配需求時
運行時常量池
運行時常量池是方法區(qū)的一部分,Class 中除了字段、方法、接口的 常量池,存放編譯器生成的字面量和符號引用,這部分內(nèi)容由類加載后進(jìn)入方法區(qū)的運行時常量池中存放。
StringTable是HashSet結(jié)構(gòu)
方法區(qū)的一部分,受到方法區(qū)的限制,依然會 OOM
Java 對象創(chuàng)建過程

<init> -> static方法 static代碼塊
- new 指令,判斷在常量池中有沒符號引用,有則已被加載過
- 判斷類是否被加載、解析、初始化
- 為新生對象在java堆里分配內(nèi)存空間
-
指針碰撞(內(nèi)存比較整齊)
步驟:1. 分配內(nèi)存 2. 移動指針,非原子步驟可能出現(xiàn)并發(fā)問題,Java虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性
2)空閑列表(內(nèi)存比較亂)
存儲堆內(nèi)存空閑地址
步驟:1.分配內(nèi)存 2. 修改空閑列表地址 非原子步驟可能出現(xiàn)并發(fā)問題,Java虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性
- 將分配到內(nèi)存空間都初始化零值
- 設(shè)置對象頭相關(guān)信息 (GC分代年齡、對象的HashCode、元數(shù)據(jù)信息)
- 執(zhí)行<init>方法
Java 對象內(nèi)存布局
對象屬性的值->實例數(shù)據(jù)
對象頭 64 位機器存 64 位,32 位機器存 32 位,8 的倍數(shù)

Java 對象的訪問
-
直接指針訪問
-
句柄訪問
對比:
- 訪問效率:直接指針訪問效率高(hotspot采用這種方式)
- 垃圾回收:句柄訪問效率高,垃圾回收只用更新句柄池,而直接指針訪問方式則要更新 reference地址
垃圾回收算法
-
引用計數(shù)器
當(dāng)對象實例分配給一個變量時,該變量計數(shù)設(shè)置為 1,當(dāng)任何其他變量被賦值為這個對象的引用的時,計數(shù)+1 (a =b,則b的引用對象實例計數(shù)器+1),當(dāng)一個對象實例的某個引用超過了生命周期(方法執(zhí)行完)或者被設(shè)置為一個新值,則該對象的實例引用計數(shù)器 -1
無法解決循環(huán)引用
可達(dá)性分析
GC Root (虛擬機棧中的引用的對象、本地方法棧中引用的對象、方法區(qū)靜態(tài)屬性引用的對象、方法區(qū)常量引用的對象) -
標(biāo)記-清除
標(biāo)記需要回收的對象,在標(biāo)記完成后統(tǒng)一回收
不足:
1.效率問題,標(biāo)記清除 2 個過程效率都不高
2.空間問題,標(biāo)記清除后產(chǎn)生大量不連續(xù)的內(nèi)存碎片,碎片過多當(dāng)程序需要分配較大的對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)一次垃圾回收動作 -
標(biāo)記-復(fù)制
內(nèi)存塊 A存活的對象復(fù)制到內(nèi)存塊 B (Survivor to)里,然后將內(nèi)存塊A (Eden + Survivor from)清空,
只有少部分對象移動,更多的對象是要被回收的
Eden:Survivor from:Survivor to=8:1:1
98%對象“朝生夕亡”,新生代可用內(nèi)存容量 90%(80%+10%),98%的對象可回收是一般情況,當(dāng)小于 90%的對象被回收的時候(10%以上的對象存活時),則 Survivor to 空間不夠,則需要依賴?yán)夏甏M(jìn)行分配擔(dān)保 -
標(biāo)記-整理
老年代不適合復(fù)制算法
復(fù)制操作增多 2. 額外 50%空間浪費 3. 經(jīng)常需要額外的空間分配擔(dān)保 4.可能老年代中對象 100% 存活
步驟:
- 標(biāo)記
- 整理 將存活的對象移動到一端(左上方),從不規(guī)整變成規(guī)整,然后直接清理掉邊界以外的內(nèi)存
Serial 收集器

單線程垃圾回收器,用戶線程到安全點先暫定,然后 GC 線程單線程串行進(jìn)行,等 GC 線程回收完,然后用戶線程再繼續(xù)
特點:Stop the world
場景:桌面應(yīng)用 (gc時間短)
用于新生代,client 端
ParNew 收集器
Serial收集器的多線程版本

用于新生代,唯一能和CMS 收集器配合工作,運行在 server 模式下
-XX:ParallelGCThreads 限制垃圾收集器線程數(shù) = CPU 核數(shù)(過多會導(dǎo)致上下文切換消耗)
并行:多條垃圾收集線程并行工作,用戶線程仍然處于等待狀態(tài)
并發(fā):用戶線程與垃圾收集器同時執(zhí)行,用戶線程和垃圾線程在不同 CPU 上執(zhí)行
Parallel Scavenge 收集器
新生代收集器,復(fù)制算法,并行的多線程收集器
關(guān)注吞吐量優(yōu)先的收集器(吞吐量 = CPU 運行用戶代碼執(zhí)行時間/CPU 執(zhí)行總時間 ,比如: 99%時間執(zhí)行用戶線程,1%時間回收垃圾,這時吞吐量為 99%)高吞吐量可以高效率利用 CPU 時間,盡快完成程序的運算任務(wù),適合在后臺運算而不需要太多的交互任務(wù)
CMS 關(guān)注縮短垃圾回收停頓時間,適合與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗
-XX:MaxGCPauseMillis 參數(shù) GC 停頓時間,參數(shù)過小會頻繁 GC
-XX:GCTimeRatio 參數(shù),默認(rèn) 99%(用戶線程時間占 CPU 總時間的 99%)
Serial Old 收集器
是Serial 收集器的老年代版本
單線程老年代收集器,采用“標(biāo)記-整理”算法
Parallel Old 收集器
是 Parallel Scavenge收集器的老年代版本
多線程老年代收集器,采用“標(biāo)記-整理”算法
CMS 收集器
獲取最短回收停頓時間為目標(biāo)的收集器,采用“標(biāo)記-清除”算法,用于互聯(lián)網(wǎng)、B/S 系統(tǒng)重視響應(yīng)的系統(tǒng)

步驟:
- 初始標(biāo)記(不和用戶線程一起運行,耗時短)—— 標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象,速度很快
- 并發(fā)標(biāo)記(和用戶線程一起運行,耗時長) —— 并發(fā)標(biāo)記階段就是進(jìn)行 GC RootsTracing,尋找 GC 引用鏈
- 重新標(biāo)記(不和用戶線程一起運行,耗時短)—— 為了修正并發(fā)標(biāo)記期間因用戶線程導(dǎo)致標(biāo)記產(chǎn)生變動的標(biāo)記記錄
- 并發(fā)清除(和用戶線程一起運行,耗時長)—— 掃描整個內(nèi)存區(qū)域
缺點 :
- 對 CPU 資源非常敏感(并發(fā)標(biāo)記階段時間長,占用用戶線程 CPU 時間)
- 無法處理浮動垃圾(程序在進(jìn)行并發(fā)清除時,用戶線程所產(chǎn)生的新垃圾)
- 標(biāo)記-清除產(chǎn)生空間碎片
G1 收集器
面向服務(wù)端應(yīng)用的垃圾收集器

Region->Remembered Set (解決 循環(huán)引用 )
檢查 Reference (程序?qū)eference類型寫操作,檢查 reference 引用類型)
步驟:
- 初始標(biāo)記 —— 標(biāo)記 GC Roots 能直接關(guān)聯(lián)到的對象
- 并發(fā)標(biāo)記 —— 從 GC Root 開始對堆中對象進(jìn)行可達(dá)性分析,找出存活對象 ,這一階段耗時較長,但可與用戶程序并發(fā)執(zhí)行
- 最終標(biāo)記(Remembered Set Logs->Remembered Set)—— 修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分標(biāo)記記錄,虛擬機將這段時間對象變化記錄在線程 Remembered Set Logs里面,最終標(biāo)記階段需要把 Remembered Set Logs的數(shù)據(jù)合并到 Remembered Set中
- 篩選回收(Live Data Counting and Evacuation)—— 只需要掃描 Remembered Set
優(yōu)勢: - 基于“標(biāo)記-整理” 為主和 Region 之間采用復(fù)制算法實現(xiàn)
- 可預(yù)測停頓,降低停頓時間,但G1 除了追求低停頓外,還能建立可預(yù)測的停頓時間模型
- G1 直接對 Java 堆中的 Region 進(jìn)行回收(新生代、老年代不再物理隔離,他們都是一部分 Region)
- 可預(yù)測的停頓時間模型,G1 跟蹤各個 Regions 里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經(jīng)驗值),在后臺維護(hù)一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的 Region
堆內(nèi)存分配
Java 堆分布圖

對象分配的規(guī)則:
- 對象主要分配在新生代的 Eden 區(qū) ( Eden區(qū),F(xiàn)rom 區(qū)存活對象復(fù)制到 To區(qū),Eden區(qū),F(xiàn)rom區(qū)被回收,然后 To區(qū)對象拷貝到From區(qū),再進(jìn)行下一次垃圾回收 )
- 如果啟動了本地線程分配緩沖,將按線程優(yōu)先在 TLAB 上分配
- 少數(shù)情況下也可能直接分配到老年代 (放不下From和To區(qū)的都直接放到老年代)
大對象分配
大對象是指需要大量連續(xù)內(nèi)存空間的 Java 對象,最典型的大對象是是那種很長的字符串以及數(shù)組
-XX:PretenureSizeThreshold 設(shè)置大于該值的對象直接分配在老年代,避免在 Eden 區(qū)以及 2 個Survivior區(qū)之間發(fā)生大量的內(nèi)存復(fù)制
逃逸分析和棧上分配
逃逸分析:分析對象動態(tài)作用域,當(dāng)一個對象在方法中被定義后,它可能被外部方法所引用,稱為方法逃逸。甚至還有可能被外部線程訪問到,比如賦值給類變量或其他線程中訪問的實例變量,稱為線程逃逸。
棧上分配:把方法中的變量和對象直接分配到棧上,方法執(zhí)行完后自動銷毀,不需要垃圾回收介入,從而提高系統(tǒng)性能
-XX:+DoEscapeAnalysis 開啟逃逸分析(jdk1.8默認(rèn)開啟 )
-XX:-DoEscapeAnalysis 關(guān)閉逃逸分析
命令
- ps -ef | grep java
- jps -m(啟動參數(shù)) -l(類名) -v (JVM 參數(shù))
-
jstat -gc 27660 250 20 監(jiān)視虛擬機各種運行狀態(tài)信息
- jinfo 27660 查看和調(diào)整進(jìn)程虛擬機(未被顯示指定的)參數(shù)信息
- jmap 生成堆轉(zhuǎn)儲快照 -XX:+HeapDumpOnOutOfMemoryError
jmap -heap 9366;
jmap -histo 9366 | more; 顯示堆中對象統(tǒng)計
jmap -dump:format=b,file=/Users/mousycoder/Desktop/a.bin 9366 生成dump文件
-Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/mousycoder/Desktop/
jhat /Users/mousycoder/Desktop/java_pid9783.hprof 圖形分析Heap
select s.toString() from java.lang.String s where (s.value != null && s.value.length > 1000 ) - jstack 線程快照(虛擬機內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合,主要用于定位線程問題)
shutdownHook 在關(guān)閉之前執(zhí)行的任務(wù)
jstack -l -F pid 強制輸出
線程狀態(tài)
- NEW
- RUNNABLE
- BLOCKED 一個正在阻塞等待一個監(jiān)視器的線程處于這個狀態(tài)(Entry Set)被動的阻塞
- WAITING 一個正在無限期等待另一個線程執(zhí)行一個特別的動作的線程處于這一狀態(tài) (Wait Set)主動顯式申請的阻塞
- TIMED_WAITING 一個正在限時等待另一個線程執(zhí)行一個動作的線程處于這一狀態(tài)
- TERMINATED 線程完成一個excution

JConsole
基于 JMX 的可視化監(jiān)視、管理工具
開啟 JMX 端口
nohup java -Xms800m -Xmx800m -Djava.rmi.server.hostname=192.168.1.250 -Dcom.sun.management.jmx
remote.port=1111 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -jar hc-charging-
server.jar &
互聯(lián)網(wǎng)開發(fā)流程

Jconsole 內(nèi)存分析思考過程

FullGC
Minor GC:當(dāng) Eden 區(qū)滿,觸發(fā) Minor GC
FullGC:
- 調(diào)用System.gc() 建議虛擬機進(jìn)行 Full GC,可通過 -XX:+DisableExplicitGC 來進(jìn)制 RMI 調(diào)用System.gc()
- 老年代空間不足 大對象直接進(jìn)入老年代,長期存活的對象進(jìn)入老年代,當(dāng)執(zhí)行 Full GC后空間仍然不足,則拋出 OutOfMemoryError,為了避免上面原因引起 Full GC,調(diào)優(yōu)時盡量做到讓對象在 Minor GC 階段被回收,讓對象在新生代多存活一段時間以及不要創(chuàng)建過大的對象和數(shù)組
- 空間分配擔(dān)保失敗 使用復(fù)制算法的 Minor GC 需要老年代的內(nèi)存空間作為擔(dān)保,如果出現(xiàn)了 HandlePromotionFailure 擔(dān)保失敗,則會觸發(fā) Full GC
建議: - 減少-Xmx大小,縮短 GC 時間(堆內(nèi)存設(shè)置越大,F(xiàn)ull GC 時間越長,停頓時間也會越長)
- 集群部署
互聯(lián)網(wǎng)問題
- 白名單問題
解決方法:list.contain->set.contain->布隆過濾器(用戶量大和用戶量小系統(tǒng)解決方案不一樣) - 死鎖
解決方法:jstack 以及 new thread帶上名稱 - 堆內(nèi)存泄露
FullGC 出現(xiàn)正常頻率為一天 1~2 次
解決方案:jmap , heap dump on oom + jhat - 堆外內(nèi)存泄露
heap堆使用率很低,但是有 OOM 以及 Full GC
解決方法:btrace
學(xué)習(xí)秘籍
- 知識體系
- 面試前看下知識體系導(dǎo)圖
- 堅持就勝利








