
圖片來源
本文將簡單介紹java的字節(jié)碼文件以及初步探索java內(nèi)存模型,我會假定您的電腦上已經(jīng)安裝并配置好JDK,在命令行窗口下輸入
java -version顯示如下信息:
D:\>java -version
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
閱讀java字節(jié)碼文件
首先,我們從一個簡單的用例ClassDemo.java開始,類內(nèi)容如下:
public class ClassDemo {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = 1 + 2;
int d = 3;
System.out.println(c + d);
}
}
在ClassDemo.java文件的路徑下編譯并執(zhí)行該類,注意執(zhí)行java命令運行class文件時不需要后綴,否則報錯。如下:
D:\>javac ClassDemo.java
D:\>java ClassDemo.class
錯誤: 找不到或無法加載主類 ClassDemo.class
D:\>java ClassDemo
6
編譯后的文件ClassDemo.class文件用UE直接打開內(nèi)容如下,文件開頭是一個0xcafebabe```16進制特殊標志(魔數(shù)),文件內(nèi)容我們無法閱讀:

我們需要對class文件進行反編譯,使用javap -v命令將class文件反編譯并輸出到ClassDemo.txt中:
D:\>javap -v ClassDemo.class > ClassDemo.txt
打開ClassDemo.txt就可以看到反編譯后的class文件指令集:
Classfile /D:/ClassDemo.class
Last modified 2018-12-28; size 416 bytes
MD5 checksum 8fc0289dea72a11671d7a74bdb62a225
Compiled from "ClassDemo.java"
public class ClassDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // ClassDemo
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 ClassDemo.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 ClassDemo
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public ClassDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iconst_3
5: istore_3
6: iconst_3
7: istore 4
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12: iload_3
13: iload 4
15: iadd
16: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
19: return
LineNumberTable:
line 3: 0
line 4: 2
line 5: 4
line 6: 6
line 7: 9
line 8: 19
}
SourceFile: "ClassDemo.java"
class文件是交給JVM閱讀并解釋執(zhí)行的,其中包括:java版本、訪問標志、常量池、當前類、超類、接口、字段、方法、屬性。
JVM邏輯內(nèi)存模型
接下來我們先看看JVM中內(nèi)存的主要邏輯劃分,然后結(jié)合class字節(jié)碼文件理解JVM各內(nèi)存區(qū)域存儲的具體內(nèi)容。

線程共享內(nèi)存區(qū)域
所有線程都能訪問這塊內(nèi)存空間,隨虛擬機或者GC而創(chuàng)建和銷毀。
- 方法區(qū)(Method Area,非堆Non-Heap):JVM用來存儲已加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),虛擬機規(guī)范中這是一個邏輯區(qū)域。具體實現(xiàn)根據(jù)不同虛擬機來實現(xiàn),如:oracle的HotSpot在java7中方法區(qū)放在永久代,java8放在元數(shù)據(jù)空間,并且通過GC機制對這個區(qū)域進行管理,回收主要目標是針對常量池的回收和對類型的卸載。
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是:String類的intern()方法。
- 堆內(nèi)存(也稱"GC"堆,Garbage Collected Heap):所有的對象實例以及數(shù)組都要在堆上分配,但是隨著JIT 編譯器的發(fā)展與逃逸分析技術(shù)的逐漸成熟,棧上分配、標量替換優(yōu)化技術(shù)將會導致一些微妙的變化發(fā)生,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。由jvm自動垃圾回收器來管理。
線程私有內(nèi)存區(qū)域
每個線程都會有獨立的內(nèi)存空間,隨線程生命周期而創(chuàng)建和銷毀。
- 虛擬機棧(Java Virtual Machine Stacks):線程棧由多個棧幀(Stack Frame)組成。一個線程會執(zhí)行一個或多個方法,一個方法對應一個棧幀。棧幀內(nèi)容包括:局部變量表、操作棧、動態(tài)鏈接、方法返回地址、附加信息等。每一個方法被調(diào)用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError 異常;如果虛擬機??梢詣討B(tài)擴展,當擴展時無法申請到足夠的內(nèi)存時會拋出OutOfMemoryError 異常。
局部變量包括:基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對象引用reference類型和returnAddress類型。
- 程序計數(shù)器(Program Counter Register):它的作用可以看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復等功能都依賴于此。
每個線程都有一個私有的程序計數(shù)器空間,占用很少的內(nèi)存空間。
執(zhí)行java方法時,計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;如果執(zhí)行Native方法,則計數(shù)器值為空(Undefined)。
CPU同一時間,只會執(zhí)行一條線程中的指令。JVM會在多線程間輪流切換并使用CPU分配的執(zhí)行時間。線程切換后,需要通過程序計數(shù)器來恢復正確的執(zhí)行位置。
此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。 - 本地方法棧(Native Method Stacks);與虛擬機棧作用類似,服務(wù)于Native方法,同時也會拋出:StackOverflowError和OutOfMemoryError異常。
字節(jié)碼文件分析
接下來,我們分析一下反編譯的ClassDemo.txt文件內(nèi)容的具體含義。
字節(jié)碼中的類信息
public class ClassDemo
minor version: 0 //次版本號
major version: 52 //主版本號
flags: ACC_PUBLIC, ACC_SUPER //訪問標志
版本號規(guī)則:
| JDK版本 | 字節(jié)碼中的主版本號 |
|---|---|
| JDK1.2 | 0x002E = 46 |
| JDK1.3 | 0x002F = 47 |
| JDK1.4 | 0x0030 = 48 |
| JDK5 | 0x0031 = 49 |
| JDK6 | 0x0032 = 50 |
| JDK7 | 0x0033 = 51 |
| JDK8 | 0x0034 = 52 |
訪問標志規(guī)則:

字節(jié)碼中的常量池信息
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // ClassDemo
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 ClassDemo.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 ClassDemo
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V

字節(jié)碼中
<init>表示構(gòu)造函數(shù),而()V的解釋如下:
【引用】在JVM規(guī)范中,每個變量/字段都有描述信息,描述信息主要的作用是是描述字段的數(shù)據(jù)類型、方法的參數(shù)信息(包括數(shù)量、類型與順序)與返回值,根據(jù)描述符規(guī)則,基本數(shù)據(jù)類型和代表無返回值的void類型都用一個大寫字符來表示,對象類型則使用字符L加對象的全限定名稱來表示,為了壓縮字節(jié)碼文件的體積,對于基本數(shù)據(jù)類型,JVM都只使用一個大寫字母來表示,如下所示:B -> byte、C -> char、D -> double、F -> float、I -> int、J -> long【由于L被其它數(shù)據(jù)類型給占用了所以用J來表示】、S -> short、Z -> boolean【由于B已經(jīng)被前面的byte類型所占用】、V -> void、 L -> 對象類型,如Ljava/lang/String;
對于數(shù)組類型來說,每一個維度使用一個前置的“[”來表示,如int[]被記錄為[I、String[][]被記錄為[[Ljava/lang/String。
用描述符描述方法時,按照先參數(shù)列表,后返回值的順序來描述。參數(shù)列表按照參數(shù)的嚴格順序放在一組()之內(nèi),如方法:
String getRealNamebyIdAndNickName(int id, String name)的描述符為:(I, Ljava/lang/String) Ljava/lang/String;
字節(jié)碼中的構(gòu)造函數(shù)信息
public ClassDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
由于沒有顯示的申明構(gòu)造函數(shù),此處的默認的無參構(gòu)造函數(shù)。
descrptor:方法入?yún)⒑头祷孛枋?br>
flags:訪問控制。
stack:方法對應棧幀中的操作數(shù)棧的深度
locals:本地變量數(shù)量
args_size:參數(shù)數(shù)量
其中無參構(gòu)造器但是args_size=1是因為無參構(gòu)造器和非靜態(tài)方法調(diào)用會默認傳入this變量參數(shù),其中aload_0即表示的this,stack=1,locals=1同理。
invokespecial:調(diào)用一個初始化方法,私有方法或者父類的方法。
invokestatic:調(diào)用靜態(tài)方法
invokevirtual:調(diào)用實例方法
【引用】LineNumberTable:為調(diào)試器提供源碼中的每一行對應的字節(jié)碼信息。即源碼與指令集的對應關(guān)系。
字節(jié)碼中的方法信息
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iconst_3
5: istore_3
6: iconst_3
7: istore 4
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12: iload_3
13: iload 4
15: iadd
16: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
19: return
LineNumberTable:
line 3: 0
line 4: 2
line 5: 4
line 6: 6
line 7: 9
line 8: 19
內(nèi)存溢出示例
下面是三個內(nèi)存溢出和堆棧溢出的示例,增加對內(nèi)存模型的理解。
- 代碼
import java.util.List;
import java.util.ArrayList;
public class HeapOOMTest {
public static void main(String[] args) {
List<Object> list = new ArrayList<Object>();
while(true) {
list.add(new Object());
}
}
}
- 輸入輸出:
D:\>javac HeapOOMTest.java
D:\>java -Xmx10M -Xms10M HeapOOMTest
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
at HeapOOMTest.main(HeapOOMTest.java:8)
- 代碼
public class StackOverflowTest {
public static void main(String[] args) {
recursion();
}
public static void recursion() {
recursion();
}
}
- 輸入輸出
D:\>javac StackOverflowTest.java
D:\>java -Xmx10M -Xms10M StackOverflowTest
Exception in thread "main" java.lang.StackOverflowError
at StackOverflowTest.recursion(StackOverflowTest.java:8)
at StackOverflowTest.recursion(StackOverflowTest.java:8)
at StackOverflowTest.recursion(StackOverflowTest.java:8)
at StackOverflowTest.recursion(StackOverflowTest.java:8)
at StackOverflowTest.recursion(StackOverflowTest.java:8)
at StackOverflowTest.recursion(StackOverflowTest.java:8)
at StackOverflowTest.recursion(StackOverflowTest.java:8)
at StackOverflowTest.recursion(StackOverflowTest.java:8)
- 代碼
import java.util.List;
import java.util.ArrayList;
public class ConstantPoolTest {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while(true) {
list.add(String.valueOf(i++).intern());
}
}
}
- 輸入輸出
D:\>javac ConstantPoolTest.java
D:\>java -Xmx10M -Xms10M ConstantPoolTest
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Unknown Source)
at java.lang.String.valueOf(Unknown Source)
at ConstantPoolTest.main(ConstantPoolTest.java:10)