1宏悦、內(nèi)存管理 - 棧 or 堆
無論是java還是C镐确,內(nèi)存分配,本質(zhì)上就是 棧和堆兩個(gè)類型饼煞。簡單來說源葫,代碼邏輯處理在棧上,數(shù)據(jù)在堆上砖瞧。
I息堂、JVM內(nèi)存模型
堆:新生代(Eden,survivor)块促,年老代(Gen) -- 分配對(duì)象荣堰、數(shù)組等
非堆(棧):虛擬機(jī)棧,本地方法棧 -- 棧幀 分配局部變量竭翠、操作需要的空間比如方法鏈接
方法區(qū)-(永久代) -- 分配代碼振坚、全局變量、靜態(tài)變量
Object o = new Object()
首先代碼在 方法區(qū)中斋扰。
執(zhí)行時(shí)渡八,Object o 會(huì)存放在 java棧 的本地變量表中。
new Object 會(huì)在 java堆中褥实。
II呀狼、JVM 內(nèi)存分配過程
a、創(chuàng)建的對(duì)象都在堆的新生代(Eden)上分配空間损离;垃圾收集器回收時(shí)哥艇,把Eden上存活的對(duì)象和一個(gè)Survivor 的對(duì)象拷貝到另一個(gè)Survivor
一般是MinorGC
b、大對(duì)象直接進(jìn)入老年代
-- 所以對(duì)于大對(duì)象比較多的僻澎,年老代分配的內(nèi)存要多一點(diǎn)貌踏,年輕代分配的少一點(diǎn)
--可以配置對(duì)象大小的門限
c十饥、長期存活的對(duì)象進(jìn)入老年代
--在多次MinorGC時(shí)仍然存活的,進(jìn)入老年代
--可以配置門限MinorGC次數(shù)祖乳,默認(rèn)15次
原因很簡單逗堵,因?yàn)?年輕代一般是復(fù)制算法,多次復(fù)制代價(jià)很大
老年代是 Full GC
d眷昆、年齡判斷蜒秤,如果 Survivor 的同齡對(duì)象占所有對(duì)象的一半,大于這個(gè)年齡的就直接進(jìn)入老年代
MinorGC時(shí)亚斋,檢查晉升老年代的對(duì)象是否大于 老年代剩余空間作媚,如果大于則進(jìn)行 Full GC
e、空間分配擔(dān)保
在發(fā)生Minor GC 時(shí)帅刊,虛擬機(jī)檢測之前 晉升到老年代的空間平均大小 是否大于老年代剩余空間纸泡,如果大于則直接進(jìn)行 Full GC;如果小于赖瞒,則查看HandlePromotionFailure 設(shè)置是否允許擔(dān)保失敗女揭。如果允許,就進(jìn)行Minor GC栏饮,并把存活的對(duì)象移到老年代吧兔,如果不允許,則進(jìn)行Full GC
III抡爹、內(nèi)存溢出
a掩驱、OutOfMemoryError
首先芒划,堆內(nèi)存不夠分配冬竟、肯定會(huì)出現(xiàn) 內(nèi)存溢出的問題
永久代,加載的類太多民逼,也會(huì)有
棧內(nèi)存申請不到也有
本機(jī)native直接內(nèi)存溢出
內(nèi)存溢出會(huì)出現(xiàn)在各個(gè)內(nèi)存區(qū)域
b泵殴、StackOverflowError
遞歸調(diào)用(沒有關(guān)閉條件)
線程太多
c、內(nèi)存溢出定位過程
使用內(nèi)存映像分析工具(Eclipse Memory Analyzer)拼苍,對(duì)dump出的文件進(jìn)行分析
確認(rèn)內(nèi)存中的對(duì)象是否必要的笑诅。 即分清楚出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)
如果是內(nèi)存泄漏通過工具查看 泄漏對(duì)象到 GC root的引用鏈
如果不是泄漏,則 檢查 虛擬機(jī)的堆參數(shù)(-Xmx -Xms)疮鲫,從代碼上檢查是否存在某些對(duì)象生命周期過長吆你,持有狀態(tài)時(shí)間過長的情況。
2俊犯、內(nèi)存(垃圾)回收
在描述 java 垃圾回收之前妇多,想象一下 C ++ 內(nèi)存如何內(nèi)存管理 和 垃圾回收。
通常new 一片內(nèi)存區(qū)域燕侠,存儲(chǔ)一些數(shù)據(jù)者祖,假設(shè)就是 new int[]
頻繁的操作刪除后立莉,留下了很多內(nèi)存碎片
然后一般都是 memcpy 把數(shù)據(jù)轉(zhuǎn)移到內(nèi)存的一端,一般是都移動(dòng)到開始端七问。
事實(shí)上蜓耻,所有的內(nèi)存回收后的管理,基本都是 拷貝移動(dòng)已有數(shù)據(jù)械巡。比如 Redis的 ziplist 就是這么設(shè)計(jì)的刹淌。
垃圾回收兩個(gè)問題:
I、如何判斷 對(duì)象不再被使用讥耗?
a芦鳍、首先想到的是記錄每一個(gè)使用者 - 引用計(jì)數(shù)器,事實(shí)上早期的java垃圾回收就是如此葛账。
引用計(jì)數(shù)有個(gè)很大的困擾柠衅,幾個(gè)對(duì)象間的互相循環(huán)引用,怎么辦籍琳?引用計(jì)數(shù)一直存在菲宴。
b、標(biāo)記引用鏈 + 從根開始 - 根搜索法
通過引用鏈可以識(shí)別對(duì)象引用關(guān)系趋急;從根開始喝峦,就能識(shí)別脫離主鏈的 循環(huán)引用的問題。這樣利用有向圖呜达,從根開始尋找整個(gè)引用鏈谣蠢,把不再鏈上的對(duì)象都進(jìn)行標(biāo)記。
什么樣的對(duì)象適合做根對(duì)象 GCroot
靜態(tài)變量 - 程序加載首先進(jìn)內(nèi)存的對(duì)象查近,全局根
棧幀的變量 - 程序當(dāng)前執(zhí)行到的對(duì)象眉踱,臨時(shí)根 (因?yàn)閳?zhí)行完畢,棧幀的數(shù)據(jù)就會(huì)回收霜威,執(zhí)行過程中谈喳,作為當(dāng)前流程開始的對(duì)象同樣也是根)
II、如何操作回收不用的對(duì)象戈泼?
前面已經(jīng)描述過C++ 內(nèi)存回收方法婿禽,java也非常類似
標(biāo)記清除法 - 前面發(fā)現(xiàn)的對(duì)象,標(biāo)記完后大猛,進(jìn)行刪除扭倾, 類似 delete,這樣會(huì)產(chǎn)生很多碎片
復(fù)制算法 - 把存活的對(duì)象挽绩,統(tǒng)一拷貝到 另一塊完整內(nèi)存
標(biāo)記整理法 - 把存活對(duì)象移動(dòng)到一端膛壹,剩下的內(nèi)存統(tǒng)一清理,類似 memcpy琼牧,后delete
適用場景
復(fù)制算法恢筝,適用存活對(duì)象較少的場景哀卫,比如 新生代;標(biāo)記整理算法和清除算法撬槽,適用于存活對(duì)象較多的場景此改。
III、垃圾回收器
除了標(biāo)記清除法外侄柔,其他兩種需要移動(dòng)對(duì)象共啃,都會(huì)造成程序的卡頓(移動(dòng)過程中,對(duì)象不能被改變)暂题,這個(gè)問題數(shù)據(jù)庫備份過程中也有同樣的問題移剪。
a、復(fù)制算法收集器 -- 基本都用在新生代
Serial收集器 - 單線程條件下運(yùn)行 (一般client和默認(rèn)的)
ParNew收集器 - 多線程條件下運(yùn)行 (一般server模式適用)
Parallel Scanvenge收集器
ParNew VS Parallel Scanvenge
ParNew 關(guān)注卡頓時(shí)延薪者; Parallel Scanvenge 關(guān)注系統(tǒng)吞吐量
b纵苛、標(biāo)記整理算法收集器 -- 基本用在老年代
Serial Old收集器 - 單線程
Parallel Old收集器 - 多線程 關(guān)注吞吐量
c、標(biāo)記清除算法收集器 -- 用在老年代
CMS(Concurrent Mark Sweep)收集器 關(guān)注時(shí)延(因?yàn)楹臅r(shí)最多的標(biāo)記和清除不需要影響用戶業(yè)務(wù))
G1收集器(Garbage First)收集器
時(shí)延 or 吞吐量
可以這么理解言津,時(shí)延的目標(biāo)是單次回收要盡快攻人,減少單次時(shí)延,而整體卡頓累計(jì)時(shí)長可能更多悬槽,導(dǎo)致吞吐量下降怀吻;吞吐量則關(guān)注整體卡頓情況,累計(jì)時(shí)長要端初婆,吞吐量要高蓬坡,單次卡頓時(shí)延可能會(huì)較長。
在這兩種策略下磅叛,關(guān)注時(shí)延的 可能是多次頻繁小范圍的GC屑咳、關(guān)注吞吐量的可能是 一次就徹底的大范圍的GC
組合:
單線程版本 - Serial + Serial Old 用在 Client模式下 (一般很少使用)
吞吐量優(yōu)先組合 - Parallel Scanvenge + Parallel Old(Serial Old 老版本) 用在 Server模式下
時(shí)延優(yōu)先組合 - ParNew + CMS(Serial Old 備用)用在 Server 模式下
3、JVM 優(yōu)化
I宪躯、JVM crash
JVM 宕機(jī)的問題分析乔宿,首先 JVM 是一個(gè)C++進(jìn)程位迂,同樣可以采用 C++ coredump 的分析思路來分析 JVM (網(wǎng)上描述的访雪,好像用jmap生成的dump不能用GDB調(diào)試)
a、定位的文件素材
crash 日志
生成 -XX:ErrorFile=/path/xxx.log掂林;
執(zhí)行命令-XX:OnError="string"
-XX:+ShowMessageBoxOnError -- 打開實(shí)時(shí)GDB調(diào)試
程序自帶日志
coredump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/xx.log
linx: kill -3 | windows : Ctrl + Break
用JDK 自帶命令jmap 臣缀,或者工具 JConsole和VisualVM
如果不能生成 則檢查linux的 ulimit 配置
如果都找不到 到 /var/log/message中 找 cat messages|grep java java線程相關(guān)信息
b、分析文件
crash 日志
日志頭(概要信息): -- 得到在哪個(gè)大的部分出現(xiàn)的問題泻帮。粗略信息
SIGSEGV - 執(zhí)行 JNI 時(shí)出現(xiàn)的問題精置,一般是 編譯加載類、執(zhí)行JVM 外部代碼出現(xiàn)的問題
EXCEPTION_ACCESS_VIOLATION - 執(zhí)行 JVM 自身的代碼
EXCEPTION_STACK_OVERFLOW - 堆棧出錯(cuò)
執(zhí)行代碼類型 C J VM 等
線程信息: -- 得到crash時(shí)線程的工作情況
線程類型 - Java Thread | VMThread | CompilerThread | GCTaskThread | WatcherThread | ConcurrentMarkSweepThread
線程狀態(tài) - _thread_in_native | _thread_uninitialized | _thread_new | _thread_in_vm | _thread_in_Java | _thread_blocked
安全點(diǎn) safepoint 和鎖 Mutex
安全點(diǎn)是標(biāo)記線程運(yùn)行到一個(gè)區(qū)域锣杂,JVM將其掛起脂倦,以便執(zhí)行GC 等JVM操作番宁。沒有運(yùn)行到安全點(diǎn)的線程,GC是不能回收其內(nèi)存的赖阻;如果線程一直不到安全點(diǎn)可能會(huì)出現(xiàn)假死狀態(tài)
內(nèi)存heap情況
各個(gè)內(nèi)存區(qū)域使用情況
其他信息
JVM參數(shù)蝶押,系統(tǒng)環(huán)境
分析重點(diǎn):概要信息里,判斷Crash時(shí)正在執(zhí)行什么信息火欧;當(dāng)前線程狀態(tài)棋电;還有內(nèi)存使用情況。
經(jīng)典問題:內(nèi)存溢出苇侵,一般永久代因?yàn)榉峙漭^少赶盔,出現(xiàn)問題的情況比較多;堆棧溢出榆浓,主要是 jni 本地棧溢出的可能較多
threaddump / heapdump 文件
當(dāng)前線程運(yùn)行狀態(tài)于未、線程堆棧信息
類、對(duì)象使用情況
分析重點(diǎn):基本信息里面陡鹃,生成堆棧時(shí)的 異常線程和異常原因沉眶; wait/lock 等信息
經(jīng)典問題:內(nèi)存溢出
分析順序 crash日志 > thread dump > heap dump
II、JVM OOM 問題
分析的文件和 crash 一樣的杉适。分析過程也是類似谎倔;
另外,可以通過JDK工具和命令實(shí)時(shí)監(jiān)控分析猿推。
不過片习,OOM 不一定會(huì)出現(xiàn)crash的情況。一般都是分析 heap dump文件蹬叭。
a藕咏、分析內(nèi)存 堆、非堆的使用情況秽五; 看看是否是內(nèi)存分配參數(shù)設(shè)置不合理孽查。
b、分析 出現(xiàn)OOM的線程坦喘,正在操作的情況盲再。找到導(dǎo)致泄露的對(duì)象的 GC Root 鏈
c、分析 類實(shí)例數(shù)最多瓣铣、最大的 類的使用情況答朋。(大對(duì)象、多對(duì)象)
-- 找到上面這兩種情況下的 類和對(duì)象 是否需要/需要這么多棠笑,分清是 泄露還是對(duì)象生命周期不合理
d梦碗、分析 GC 的情況
-- 看看Full GC的情況
III、性能優(yōu)化
程序的性能優(yōu)化,無非就是 CPU洪规、內(nèi)存印屁、IO 三種資源的占用情況分析。
性能優(yōu)化的關(guān)鍵在于斩例,分段排查库车,逐步逼近的方式,確定問題代碼所在
先測量
再逐步逼近
找到問題代碼
分析原因樱拴,并給解決方法
第一步:Linux 命令查看
top -H -p 找到 java進(jìn)程和線程 中最耗資源的線程
第二步:在各種日志中找到對(duì)應(yīng)的線程進(jìn)行分析
a柠衍、卡頓時(shí)間較長 or 處理很慢 - 一般是CPU在高負(fù)荷運(yùn)轉(zhuǎn),說明線程在高負(fù)荷執(zhí)行;GC 時(shí)間較長等
查看GC 時(shí)長晶乔,GC 頻次珍坊,各種GC占比 使用的是什么垃圾處理器
(比如 GCViewer 工具),GC的具體情況 -XX:+PrintGCTimeStamps -Xloggc:/tmp/gc.log -XX:+PrintGCDetails
查看各個(gè)線程處理情況,lock wait/notify 等情況正罢;長時(shí)間運(yùn)行的線程
查看JNI線程使用情況
連續(xù)生成兩次的 線程堆棧(core) 文件阵漏,對(duì)比,查看 對(duì)象變化翻具,線程執(zhí)行變化履怯;如果執(zhí)行方法沒有變化的線程,一般就是有問題的線程
IV裆泳、JDK自帶工具
a叹洲、命令行工具
內(nèi)存信息 jmap工具
生成dump jmap dump:format=b,file=xxx pid
內(nèi)存統(tǒng)計(jì) jmap -heap
內(nèi)存跟蹤 jstat
線程堆棧跟蹤 jstack
配置信息 jinfo
分析工具 jhat
利用命令行就是 jmap+jstack,然后詳細(xì)信息通過jhat
b工禾、可視化工具
JConsole
JVisualVM - 其中 BTTrace 可以嵌入到每個(gè)方法追蹤每個(gè)方法的執(zhí)行(通過類似 asm/CGLib 字節(jié)碼加載替換)
對(duì)比兩個(gè) dump 的差異运提,找出對(duì)象的
V、性能分析工具
MAT
IBM Heap - 可以分析出OOM中的 內(nèi)存占用最大的地方闻葵,可以溯源GC root民泵,找到對(duì)象樹
VI、配置建議
a槽畔、內(nèi)存分配栈妆,各個(gè)代的分配,32位JVM下 堆分配 1G厢钧,年輕代 一半鳞尔,年老代一半。持久代64M
-Xmx512m
-Xms512m
-- 最大最小保持一致坏快,避免頻繁擴(kuò)容和收縮
-Xmn256m
-- 年輕代一般在 一半左右铅檩,官方推薦 3/8
-XX:PermSize=64m
-- 持久代一般固定在 64M左右
-XX:MaxPermSize=128m
b、垃圾收集器選擇
-XX:+UseParNewGC
--設(shè)置年輕代使用 ParNew收集器 并行
-XX:+UseConcMarkSweepGC
--設(shè)置年老代為CMS收集器 并發(fā)
c莽鸿、其他設(shè)置項(xiàng):內(nèi)存壓縮,對(duì)象晉級(jí)等等。
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
-- Full GC 5次后祥得,進(jìn)行內(nèi)存碎片壓縮
-XX:+HeapDumpOnOutOfMemoryError
-- 內(nèi)存溢出時(shí)生成dump文件
d兔沃、啟用運(yùn)行期編譯
Jit即時(shí)編譯器
C1 – Client (簡單優(yōu)化
C2 – Server(激進(jìn)優(yōu)化 運(yùn)行在Server模式下會(huì)更高效)
根據(jù)監(jiān)控,針對(duì)熱點(diǎn)代碼進(jìn)行優(yōu)化(比如方法內(nèi)聯(lián))
Jit的缺點(diǎn)是 編譯有耗時(shí)级及,另外乒疏,對(duì)于一些類裝載卸載比較多的場景也不適合。
Server模式啟動(dòng)時(shí)要慢一點(diǎn)饮焦,運(yùn)行時(shí)效率很高
也可以指定怕吴,運(yùn)行模式 解釋模式-Xint,編譯模式-XComp
比較理想的情況是县踢,編譯成Class转绷,運(yùn)行時(shí)可以動(dòng)態(tài)Server模式陕习;兼顧了效率和可移植性名眉。