乾貨分享丨jvm系列:dump文件深度分析

摘要:java內存dump是jvm運行時內存的一份快照,利用它可以分析是否存在內存浪費,可以檢查內存管理是否合理,當發生OOM的時候,可以找出問題的原因。那麼dump文件的內容是什麼樣的呢?

JVM dump

java內存dump是jvm運行時內存的一份快照,利用它可以分析是否存在內存浪費,可以檢查內存管理是否合理,當發生OOM的時候,可以找出問題的原因。那麼dump文件的內容是什麼樣的呢?我們一步一步來

獲取JVM dump文件

獲取dump文件的方式分為主動和被動

i.主動方式:
1.利用jmap,也是最常用的方式:jmap -dump:[live],format=b,file=
2.利用jcmd,jcmd GC.heap_dump
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);

參考(//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一個類的成員變量有兩種類型

  1. 基本類型(8種基本類型),它們佔用byte數固定不變,每生成一個對象它們就需要給它們賦初始值,分配空間
  2. 是引用類型,表示一個對象,在類中只有一個引用,引用只是一個數值,所佔用的空間大小為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塊

  1. 字符串,由identifierSize個byte的字符串id,後面是(length-identifierSize)個byte的字符串內容(後續對字符串是直接引用的這裏面的id)
  2. 類,由4個byte的類序列(在棧楨中使用),identifierSize個byte的類id(解析類的時候用到),4個byte的序列id(暫未使用),identifierSize個byte的類名id
  3. 棧楨,由identifierSize個byte的楨id,identifierSize個byte的方法名id,identifierSize個byte的方法標識id,identifierSize個byte的類文件名id,4個byte的類序列,4個byte的行號
  4. 棧,由4個byte的棧序號,4個byte的線程序號,4個byte的楨數量,後面就是若干個identifierSize個byte的楨id
  5. dump塊就是所有對象的內容了,每個對象由1個byte的子類型,和對象內容結成,子類型有6種,gc root, 線程對象,類,對象,基本類型數組,對象數組

gc root

gc root有4種結構,8種類型

  1. identifierSize個byte的對象id,類型有SYSTEM_CLASS,BUSY_MONITOR, 及未UNKNOWN
  2. identifierSize個byte的對象id,4個byte的線程序列號,類型有NATIVE_STACK,THREAD_BLOCK
  3. identifierSize個byte的對象id,4個byte的線程序列號,4個byte的棧楨深度,類型有JAVA_LOCAL,NATIVE_LOCAL
  4. 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、基本信息:

  1. identifierSize個byte的類對象id
  2. 4個byte的棧序列號,
  3. identifierSize個byte的父類對象id,
  4. identifierSize個byte的classLoader對象id,
  5. identifierSize個byte的Signer對象id,
  6. identifierSize個byte的protection domain對象id,
  7. identifierSize個byte的保留id1和id2,
  8. 4個byte的類實例對象大小,
  9. 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)
  10. 2個byte的靜態變量個數,後面是每個靜態變量的,identifierSize個byte的變量名id, 1個byte的變量類型,和若干個byte的內容,內容根據類型來決定(見類對象基本信息的第9條)
  11. 2個byte的成員變量個數,後面是每個成員變量的,identifierSize個byte的變量名id,1個byte的變量類型

2、說明:
(1)類裏面的常量很多地方都沒有用上,所以常量個數一般為0
(2)類的靜態變量的名稱類型及值是放在類對象裏面的,成員變量的名稱和類型也是放在類對象裏面的,但是實例的值是放在實例對象裏面的

實例對象

1、基本信息:

  1. identifierSize個byte的實例對象id
  2. 4個byte的棧序列號
  3. identifierSize個byte的類id
  4. 4個byte的佔用位元組數
  5. 實例的變量的值

2、說明:

  1. 實例的值為實例對象的成員變量值,順序為當前類的變量值,順序為類對象基本信息中第11條中的順序,然後是父類的變量值
  2. 變量的值基本類型都有默認值,引用類型默認值為0,佔用位元組數(見類對象基本信息的第9條)

基本類型數組

1、基本信息:

  1. identifierSize個byte的數組對象id
  2. 4個byte的棧序列號
  3. 4個byte的數組長度
  4. 1個byte的元素類型
  5. 元素的值列表

2、說明:

  1. 元素的值(見類對象基本信息的第9條)

對象數組

1、基本信息:

  1. identifierSize個byte的數組對象id
  2. 4個byte的棧序列號
  3. 4個byte的數組長度
  4. identifierSize個byte的元素類id
  5. 元素的值列表

內存分配

當一個線程啟動的時候,進程會去系統內存生成一個線程棧
每當發生一次方法調用,就會向棧中壓入一個棧楨,當方法調用完之後,棧楨會退出
在運行過程中,如果有對象的new操作的時候,進程會去堆區申請一塊內存
關於運行時內存的詳細情況,可以查找相關的資料

內存回收規則

如果一個對象不能騎過gc root引用可達,那麼這個對象就可能要被回收
對象回收規則包括

  1. 實例屬性被實例引用,只有當實例被回收了實例屬性才能被回收(只針對強引用)
  2. 類對象被實例引用,只有當一個類的所有實例都被回收了,類才能被回收
  3. 類對象的父類,classLoader對象,signer對象, protection domain對象被類引用,只有當類被回收了,這些才能被回收
  4. 局部變量(線程棧中)的作用域為一個大括號
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程序員必須掌握的高級技能之一,你學會了嗎?

 

點擊關注,第一時間了解華為雲新鮮技術~