1. 程式人生 > 實用技巧 >Java入門系列之訪問修飾符作用範圍

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=

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);

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

  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程式設計師必須掌握的高階技能之一,你學會了嗎?

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