Java入門系列之訪問修飾符作用範圍
摘要:java記憶體dump是jvm執行時記憶體的一份快照,利用它可以分析是否存在記憶體浪費,可以檢查記憶體管理是否合理,當發生OOM的時候,可以找出問題的原因。那麼dump檔案的內容是什麼樣的呢?
JVM dump
java記憶體dump是jvm執行時記憶體的一份快照,利用它可以分析是否存在記憶體浪費,可以檢查記憶體管理是否合理,當發生OOM的時候,可以找出問題的原因。那麼dump檔案的內容是什麼樣的呢?我們一步一步來
獲取JVM dump檔案
獲取dump檔案的方式分為主動和被動
i.主動方式:
1.利用jmap,也是最常用的方式:jmap -dump:[live],format=b,file=
3.使用VisualVM,可以介面操作進行dump記憶體
4.通過JMX的方式
MBeanServer server = ManagementFactory.getPlatformMBeanServer(); HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class); mxBean.dumpHeap(filePath, live);
參考(https://www.baeldung.com/java-heap-dump-capture)
ii.被動方式:
被動方式就是我們通常的OOM事件了,通過設定引數-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=
dump檔案分析
結構示意圖
結構詳解
dump檔案是堆記憶體的對映,由檔案頭和一系列內容塊組成
檔案頭
由musk, 版本,identifierSize, 時間4部分組成
1、musk:4個byte,內容為'J', 'A', 'V', 'A'即JAVA
2、version:若干byte,值有以下三種
" PROFILE 1.0\0", " PROFILE 1.0.1\0", " PROFILE 1.0.2\0"
3、identifierSize:4個byte數字,值為4或者8,表示一個引用所佔用的byte數
4、time:8個byte,dump檔案生成時間
說明:java一個類的成員變數有兩種型別
- 基本型別(8種基本型別),它們佔用byte數固定不變,每生成一個物件它們就需要給它們賦初始值,分配空間
- 是引用型別,表示一個物件,在類中只有一個引用,引用只是一個數值,所佔用的空間大小為identifierSize,被引用物件即將在堆中的另一個地方
例如定義一個類
public class Person { private int age;//4個byte private String name;//identifierSize個byte private double weight;//8個byte }
當我們在new Person()的時候
它就需要申請一個空間,空間大小為 物件頭大小+4+identifierSize+8個byte
物件大小的測量:
jdk提供一個測試物件佔用記憶體大小的工具Instrumentation,但是Instrumentation沒法直接引用到,需要通過agent來引用到
定義一個Premain類, javac Premain.java
//Premain.java public class Premain { public static java.lang.instrument.Instrumentation inst; public static void premain(String args, java.lang.instrument.Instrumentation inst) { Premain.inst = inst; } }
編寫一個Manifest檔案
manifest.mf Manifest-Version: 1.0 Premain-Class: Premain Can-Redefine-Classes: true Can-Retransform-Classes: true
打包
jar -cmf manifest.mf premain.jar Premain.class
定義一個執行類, javac PersonTest.java
//PersonTest.java public class PersonTest { public static void main(String[] args) throws Exception { Class clazz = Class.forName("Premain"); if (clazz != null) { Person p = new Person(); java.lang.instrument.Instrumentation inst = (java.lang.instrument.Instrumentation)clazz.getDeclaredField("inst").get(null); System.out.println("person size:[" + inst.getObjectSize(p) + "]B"); System.out.println("class size:[" + inst.getObjectSize(p.getClass()) + "]B"); } } }
帶agent執行
java -javaagent:premain.jar PersonTest
結果:
person size:[32]B class size:[504]B
內容塊
每個塊都是塊頭和塊體組成
塊頭
塊頭由1個byte的塊型別,4個byte的時間time,4個byte的長度表示此內容塊佔用byte數
type型別一般有5種,字串,類,棧楨,棧,及dump塊
- 字串,由identifierSize個byte的字串id,後面是(length-identifierSize)個byte的字串內容(後續對字串是直接引用的這裡面的id)
- 類,由4個byte的類序列(在棧楨中使用),identifierSize個byte的類id(解析類的時候用到),4個byte的序列id(暫未使用),identifierSize個byte的類名id
- 棧楨,由identifierSize個byte的楨id,identifierSize個byte的方法名id,identifierSize個byte的方法標識id,identifierSize個byte的類檔名id,4個byte的類序列,4個byte的行號
- 棧,由4個byte的棧序號,4個byte的執行緒序號,4個byte的楨數量,後面就是若干個identifierSize個byte的楨id
- dump塊就是所有物件的內容了,每個物件由1個byte的子型別,和物件內容結成,子型別有6種,gc root, 執行緒物件,類,物件,基本型別陣列,物件陣列
gc root
gc root有4種結構,8種類型
- identifierSize個byte的物件id,型別有SYSTEM_CLASS,BUSY_MONITOR, 及未UNKNOWN
- identifierSize個byte的物件id,4個byte的執行緒序列號,型別有NATIVE_STACK,THREAD_BLOCK
- identifierSize個byte的物件id,4個byte的執行緒序列號,4個byte的棧楨深度,型別有JAVA_LOCAL,NATIVE_LOCAL
- identifierSize個byte的物件id,identifierSize個byte的global refId(暫未使用),型別有NATIVE_STATIC
gc root示意圖
gc root為垃圾收集追溯的源頭,每個gc root都指向一個初始物件,無法追溯的物件是要被回收掉的
系統類,只有classLoader為null的類才是gc root,每個類都是一個gc root
執行緒棧,執行緒中方法引數,區域性變數都是gc root,每個物件都是一個gc root
系統保留物件,每個物件都是一個gc root
類物件
1、基本資訊:
- identifierSize個byte的類物件id
- 4個byte的棧序列號,
- identifierSize個byte的父類物件id,
- identifierSize個byte的classLoader物件id,
- identifierSize個byte的Signer物件id,
- identifierSize個byte的protection domain物件id,
- identifierSize個byte的保留id1和id2,
- 4個byte的類例項物件大小,
- 2個byte的常量個數,後面是每個常量的,2個byte的下標,1個byte的常量型別,和若干個byte的內容,內容根據型別來決定(boolean/byte為1個byte, char/short為2個byte,float/int為4個byte, double/long為8個byte,引用型別為identifierSize個byte)
- 2個byte的靜態變數個數,後面是每個靜態變數的,identifierSize個byte的變數名id, 1個byte的變數型別,和若干個byte的內容,內容根據型別來決定(見類物件基本資訊的第9條)
- 2個byte的成員變數個數,後面是每個成員變數的,identifierSize個byte的變數名id,1個byte的變數型別
2、說明:
(1)類裡面的常量很多地方都沒有用上,所以常量個數一般為0
(2)類的靜態變數的名稱型別及值是放在類物件裡面的,成員變數的名稱和型別也是放在類物件裡面的,但是例項的值是放在例項物件裡面的
例項物件
1、基本資訊:
- identifierSize個byte的例項物件id
- 4個byte的棧序列號
- identifierSize個byte的類id
- 4個byte的佔用位元組數
- 例項的變數的值
2、說明:
- 例項的值為例項物件的成員變數值,順序為當前類的變數值,順序為類物件基本資訊中第11條中的順序,然後是父類的變數值
- 變數的值基本型別都有預設值,引用型別預設值為0,佔用位元組數(見類物件基本資訊的第9條)
基本型別陣列
1、基本資訊:
- identifierSize個byte的陣列物件id
- 4個byte的棧序列號
- 4個byte的陣列長度
- 1個byte的元素型別
- 元素的值列表
2、說明:
- 元素的值(見類物件基本資訊的第9條)
物件陣列
1、基本資訊:
- identifierSize個byte的陣列物件id
- 4個byte的棧序列號
- 4個byte的陣列長度
- identifierSize個byte的元素類id
- 元素的值列表
記憶體分配
當一個執行緒啟動的時候,程序會去系統記憶體生成一個執行緒棧
每當發生一次方法呼叫,就會向棧中壓入一個棧楨,當方法呼叫完之後,棧楨會退出
在執行過程中,如果有物件的new操作的時候,程序會去堆區申請一塊記憶體
關於執行時記憶體的詳細情況,可以查詢相關的資料
記憶體回收規則
如果一個物件不能騎過gc root引用可達,那麼這個物件就可能要被回收
物件回收規則包括
- 例項屬性被例項引用,只有當例項被回收了例項屬性才能被回收(只針對強引用)
- 類物件被例項引用,只有當一個類的所有例項都被回收了,類才能被回收
- 類物件的父類,classLoader物件,signer物件, protection domain物件被類引用,只有當類被回收了,這些才能被回收
- 區域性變數(執行緒棧中)的作用域為一個大括號
public void test(){ Object a = new Object();//obj 1 Object b = new Object();//obj 2 { Object c = new Object();//obj 3 a = null;//obj 1可以被回收了 }//obj 3可以回收了 }//obj 2可以被回收了
分析工具簡介
分析dump檔案,我們可以用jdk裡面提供的jhat工具,執行
jhat xxx.dump
jhat載入解析xxx.dump檔案,並開啟一個簡易的web服務,預設埠為7000,可以通過瀏覽器檢視記憶體中的一些統計資訊
一般使用方法
1、瀏覽器開啟http:/127.0.0.1:7000
會列出一些功能,包括package下面各個類的概覽,及各個功能導航
2、點選頁面的堆記憶體統計
有一個表格,物件型別,例項個數,例項所佔用記憶體大小,哪種型別的物件佔用了記憶體最多一目瞭然
3、點選其中認為記憶體消耗太多的類名檢視類詳情
主要展現該類下面各個例項的大小,以及一些連結導航
4、點選references summary by type
如果某種型別的物件太多,那麼有可能是引用它的那個類的物件太多
基本上一些簡單頁面的查詢,結合原始碼,就可以初步定位記憶體洩漏的地方
綜上,dump檔案結構還是比較簡單的,這對於分析執行緒的執行情況非常有用,也是每一個Java程式設計師必須掌握的高階技能之一,你學會了嗎?