本系列文章主要是對(duì)學(xué)習(xí)《深入理解java虛擬機(jī)》的記錄,以加深自己的理解,也方便自己后續(xù)復(fù)習(xí)回顧
前言
之前學(xué)習(xí)java,只是會(huì)用常用的語(yǔ)法、框架,但在開(kāi)發(fā)過(guò)程中,總會(huì)遇到一些奇怪的現(xiàn)象和疑惑的地方。然后覺(jué)得必須深入理解java相關(guān)的實(shí)現(xiàn)。
到現(xiàn)在已經(jīng)前前后后看了《深入理解java虛擬機(jī)》大概有四、五遍。前兩遍基本上第五章以后就不怎么看得下去了,后面幾遍才慢慢得能把整本書(shū)看完,部分重點(diǎn)的章節(jié)看了更多遍。現(xiàn)在就希望把學(xué)習(xí)理解到的jvm相關(guān)的知識(shí)記錄一下,也希望自己在記錄的過(guò)程中,能夠認(rèn)識(shí)理解的更深。
運(yùn)行時(shí)數(shù)據(jù)區(qū)

-
程序計(jì)數(shù)器
線程獨(dú)有的內(nèi)存區(qū)域。
感覺(jué)和CPU里的程序計(jì)數(shù)器的意義一樣。cpu中的程序計(jì)數(shù)器通過(guò)計(jì)數(shù)來(lái)指定cpu要執(zhí)行的指令。
jvm執(zhí)行的是字節(jié)碼,虛擬機(jī)的當(dāng)前線程根據(jù)計(jì)數(shù)來(lái)指定要執(zhí)行的字節(jié)碼。
根據(jù)虛擬機(jī)概念模型,字節(jié)碼解釋器通過(guò)改變計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼。
此內(nèi)存區(qū)域?yàn)槲ㄒ灰粋€(gè)虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError的區(qū)域。
虛擬機(jī)棧
線程獨(dú)有。生命周期和當(dāng)前線程相同。
每個(gè)方法在執(zhí)行時(shí),都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
每個(gè)方法被調(diào)用執(zhí)行完成的過(guò)程,對(duì)應(yīng)著棧幀在虛擬機(jī)棧入棧、出棧的過(guò)程。本地方法棧
和虛擬機(jī)棧類似,只不過(guò)本地方法棧存儲(chǔ)的是當(dāng)前線程調(diào)用的本地方法相關(guān)的信息。堆
所有線程共享。
幾乎所有對(duì)象實(shí)例都在這里分配內(nèi)存。GC主要是回收這里的內(nèi)存。方法區(qū)
所有線程共享。
用于存儲(chǔ)虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等。運(yùn)行時(shí)常量池
屬于方法區(qū)的一部分。
編譯生成的class文件不僅包含對(duì)類相關(guān)的基本信息的描述,還有常量池用于描述編譯期生成的各種字面量和符號(hào)引用。當(dāng)類被加載時(shí),常量池中的信息被存在方法區(qū)里。直接內(nèi)存
并不屬于jvm管理的內(nèi)存區(qū)域。nio分配內(nèi)存就是直接調(diào)用本地方法直接在堆外分配內(nèi)存。
如果直接內(nèi)存申請(qǐng)的大小加上jvm分配的內(nèi)存大于機(jī)器的總內(nèi)存,就會(huì)OOM。
字面量: 字符串,一些數(shù)字類型值和final 修飾的常量等。
符號(hào)引用: 類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符。
類在內(nèi)存的布局
hotspot的實(shí)現(xiàn),對(duì)象在內(nèi)存中分3塊存儲(chǔ):對(duì)象頭(Header),實(shí)例數(shù)據(jù)(Instance data),對(duì)齊填充(padding)。
對(duì)象頭分兩部分?jǐn)?shù)據(jù):一部分為32bit/64bit的Mark Word,用來(lái)存儲(chǔ)對(duì)象的運(yùn)行時(shí)數(shù)據(jù),包括hash code,gc分代年齡,鎖狀態(tài)標(biāo)識(shí),線程持有的鎖,偏向線程id等。另一部分為類型指針,用來(lái)確定當(dāng)前對(duì)象為哪一個(gè)類的實(shí)例。
實(shí)例數(shù)據(jù)部分存儲(chǔ)本對(duì)象及其繼承下來(lái)的相關(guān)父類中的屬性字段的值。
填充部分,對(duì)象內(nèi)存塊的大小必須是8字節(jié)的整數(shù)倍,如果不夠進(jìn)行填充。

垃圾回收
jvm回收的是哪些區(qū)域的內(nèi)存?
和虛擬機(jī)相關(guān)的虛擬機(jī)棧,程序技術(shù)器,本地方法棧中的內(nèi)存在改線程停止后,所占的內(nèi)存就會(huì)被釋放。
而方法區(qū),堆內(nèi)存占用只有在運(yùn)行時(shí)知道,并且隨著 程序的運(yùn)行 占用內(nèi)存也會(huì)隨著變化。方法區(qū)中有類加載后存儲(chǔ)的類信息描述,這一塊內(nèi)存可以被回收的不多。堆中的對(duì)象是主要可以回收的區(qū)域。
GC需要解決哪些問(wèn)題?
- 有哪些對(duì)象是應(yīng)該被回收的?
- 怎么對(duì)內(nèi)存進(jìn)行回收?
1. 有哪些對(duì)象可以被回收?
只有那些永遠(yuǎn)不會(huì)被引用的對(duì)象才可以被回收。
判斷對(duì)象死亡的方法:
- 引用計(jì)數(shù)
原理:對(duì)象維護(hù) 一個(gè)引用計(jì)數(shù)變量。有被引用則計(jì)數(shù)加1。如果引用計(jì)數(shù)為0代表當(dāng)前對(duì)象可以被回收。
這種方法是多數(shù)人認(rèn)為jvm實(shí)現(xiàn)的方法(起碼我大學(xué)期間是這么認(rèn)為),但是因?yàn)檫@種方法在判定循環(huán)引用(即對(duì)象a引用對(duì)象b,同時(shí)對(duì)象b引用對(duì)象a,除了它們彼此引用再?zèng)]有別的引用關(guān)系)的實(shí)現(xiàn)邏輯上比價(jià)麻煩,所以實(shí)際jvm很少有使用。 - 可達(dá)性分析
原理: 從一系列GC ROOT對(duì)象開(kāi)始向下搜索,搜索經(jīng)過(guò)的對(duì)象添加到對(duì)應(yīng)的引用鏈.如果一個(gè)對(duì)象沒(méi)在任何引用鏈中,則可以被回收。
(如下圖,解決了循環(huán)引用的問(wèn)題)
GC ROOT的選取 - 虛擬機(jī)棧中引用的對(duì)象
- 方法區(qū)中靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 本地方法棧中引用的對(duì)象
2 垃圾收集算法
-
標(biāo)記-清除
先標(biāo)記出哪些對(duì)象可以回收,然后再清除可回收對(duì)象
回收前內(nèi)存狀態(tài):
回收后內(nèi)存狀態(tài):
特點(diǎn)
標(biāo)記,清除效率都不高
回收后很多垃圾碎片 -
復(fù)制
將內(nèi)存分為兩塊,回收時(shí)將存活的對(duì)象完全復(fù)制到另一塊內(nèi)存中,然后將之前的內(nèi)存清空。
回收前內(nèi)存狀態(tài):
回收后內(nèi)存狀態(tài):
特點(diǎn)
實(shí)現(xiàn)效率高
需要更多的額外內(nèi)存 -
標(biāo)記-整理
先標(biāo)記哪些對(duì)象可以回收,然后將可回收對(duì)象移動(dòng)到內(nèi)存的一端
回收前內(nèi)存狀態(tài):
回收后內(nèi)存狀態(tài):
3 分代收集
實(shí)際各種jvm都是用分代收集算法來(lái)進(jìn)行垃圾回收。
jvm把堆內(nèi)存分為新生代(對(duì)象存活率低,每次垃圾回收此區(qū)域大部分對(duì)象都被回收)和老年代(對(duì)象存活率高,每次垃圾回收此區(qū)域很少對(duì)象被回收),jvm根據(jù)對(duì)象特點(diǎn)在相應(yīng)區(qū)域分配和回收對(duì)象內(nèi)存。
新生代內(nèi)存分為:EDEN區(qū)和2個(gè)SURVIVOR區(qū),EDEN/SURVIVOR=8/1,采用復(fù)制算法進(jìn)行垃圾回收。
下面舉實(shí)例來(lái)說(shuō)明內(nèi)存分配與回收的過(guò)程
內(nèi)存配置說(shuō)明:
EDEN:8M, SURVIVOR:1M
老年代:40M

a) 新建對(duì)象a,b,c,需要內(nèi)存1m,2m,3m
b) jvm在EDEN區(qū)給a,b,c分配內(nèi)存,運(yùn)行一段時(shí)間后,對(duì)象b,c不可達(dá),處于可回收狀態(tài)
c) 新建對(duì)象d,需要內(nèi)存4m
d) 此時(shí)EDEN區(qū)只剩4m內(nèi)存不足以為對(duì)象d分配內(nèi)存,觸發(fā)minor gc
e) EDEN存活的對(duì)象為a,把對(duì)象## <p id="runningData">運(yùn)行時(shí)數(shù)據(jù)區(qū)</p>a復(fù)制到SURVIVOR_a,把EDEN區(qū)域清空
f) 把對(duì)象d分配到EDEN區(qū),此時(shí)EDEN占用4m,SURVIIVOR_a占1m
g) 新建對(duì)象e,需要對(duì)象5m
接下來(lái)分兩種情況
1 如果PretenureSizeThreshold<5m
對(duì)象e被直接分配到老年代
2 如果PretenureSizeThreshold>5m
a) EDEN區(qū)剩余內(nèi)存不足以分配,觸發(fā)minor gc
b) 把EDEN和SURVIVOR_a區(qū)中的存活對(duì)象復(fù)制到SURVIVOR_b,然后將EDEN和SURVIVOR_a清空
c) SURVIVOR_b不足以存放復(fù)制來(lái)的對(duì)象,直接把對(duì)象d移到老年代
d) 把對(duì)象e分配在EDEN
說(shuō)明
1 如果對(duì)象在SURVIVOR中經(jīng)過(guò)多次(默認(rèn)配置為15次)minor gc,沒(méi)有被回收,該對(duì)象會(huì)被移到老年代。對(duì)象年紀(jì)(經(jīng)歷過(guò)的gc次數(shù))信息在對(duì)象頭中存儲(chǔ)
2 如果老年代中的內(nèi)存不足以分配會(huì)觸發(fā)full gc,如果full gc后內(nèi)存仍不足,會(huì)OOM
3 一般來(lái)說(shuō),minor gc的頻率更高,時(shí)間更短。full gc的頻率更低,花費(fèi)時(shí)間更長(zhǎng)。
類文件結(jié)構(gòu)
現(xiàn)在基于jvm平臺(tái)的語(yǔ)言不僅有java,還有g(shù)roovy,scala和google最近一直在推的kotlin等。
所有這些語(yǔ)言的語(yǔ)法和所用的編譯器可能都不同,但只要它們編譯生成的class文件(字節(jié)碼)符合規(guī)范,就能在虛擬機(jī)上運(yùn)行。
class文件是一組以8位為單位的2進(jìn)制數(shù)據(jù)流。
class文件中有兩種數(shù)據(jù)類型:無(wú)符號(hào)數(shù)和表。class文件的數(shù)據(jù)項(xiàng)如下:

常量項(xiàng)的結(jié)構(gòu)如下:


舉例說(shuō)明
以最簡(jiǎn)單的Hello World代碼為例,分析編譯生成的class文件,來(lái)學(xué)習(xí)class的文件結(jié)構(gòu)。
Hello.java文件如下:
public class Hello{
public static void main(String[] args){
System.out.println("Hello World!");
}
}
Hello.class文件如下:

class 文件分析
- magic code (u4)
文件最頭4個(gè)字節(jié)為magic code:CAFE BABE。
用來(lái)標(biāo)識(shí)此文件為可以被虛擬機(jī)接收的class文件。 - version
接下來(lái)4字節(jié)為版本號(hào):0000(副版本) 0034(主版本)。
代表class版本號(hào)為:52.0,對(duì)應(yīng)jdk1.8。 - 常量池?cái)?shù)量
接下來(lái)2字節(jié)為001d(29)。
代表常量池有28項(xiàng)常量。第0項(xiàng)常量預(yù)留,用來(lái)表達(dá)不指向任何常量的含義。 - 常量解析
接下來(lái)字段為28個(gè)常量的定義。
- 第1個(gè)常量
第一個(gè)字節(jié)為0A,代表為Method_ref info。
根據(jù)上圖常量結(jié)構(gòu),得知method_ref 表中,接下來(lái)兩個(gè)U2分別指向兩個(gè)常量索引0006(const_pool的第6個(gè)常量)和 000F(const_pool的第15個(gè)常量),分別代表指向聲明方法的類描述符和指向名稱及類型的描述符。
** 結(jié)合下面javap 生成的文件 ,我們可以找到#6,#15,然后依次找到最終含義 **
2)第2個(gè)常量
第一個(gè)字節(jié) 為09,代表為Field_ref info。
...............后續(xù)常量解析和上面同理。
用javap 命令可以對(duì)class文件進(jìn)行分析
javap Hello.class
Compiled from "Hello.java"
public class Hello {
public Hello();
public static void main(java.lang.String[]);
}
Classfile /home/fll/code/javaTest/Hello.class
Last modified 2017-5-31; size 416 bytes
MD5 checksum 7c04c33532f23f7d4aca1d0ec468a57f
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Hello
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Hello.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Hello
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public Hello();
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=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 4: 0
line 5: 8
}
SourceFile: "Hello.java"
類加載機(jī)制
虛擬機(jī)把描述類的數(shù)據(jù)從class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),解析和初始化,并最終形成可以被虛擬機(jī)直接使用的java類型
類的生命周期

類初始化時(shí)機(jī)
- 遇到new,getstatic,putstatic或invokestatic指令時(shí)。(使用new 實(shí)例化對(duì)象,讀取或設(shè)置類的靜態(tài)字段_** 被final 修飾定義時(shí)賦初值除外**,調(diào)用類的靜態(tài)方法)
- 通過(guò)反射調(diào)用一個(gè)未初始化的類
- 初始化一個(gè)類時(shí),要先初始化其父類
- 虛擬機(jī)啟動(dòng)時(shí),會(huì)初始化Main主類
類加載全過(guò)程
包括加載、驗(yàn)證、準(zhǔn)備、解析和初始化整個(gè)過(guò)程。
- 加載
- 通過(guò)類的全限定名來(lái)加載類的二進(jìn)制字節(jié)流(不限定來(lái)源,可以是jar,war,反射生成,只要結(jié)構(gòu)符合類文件結(jié)構(gòu))
- 將這個(gè)類代表的靜態(tài)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)結(jié)構(gòu)
- 在內(nèi)存中生成代表這個(gè)類的Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的入口。
- 驗(yàn)證
確保加載的字節(jié)流符合當(dāng)前虛擬機(jī)的要求,不會(huì)危及運(yùn)行安全。 - 準(zhǔn)備
為類變量在內(nèi)存方法區(qū)中分配內(nèi)存并設(shè)置初始值。(這里的初始值并不是指程序制定的默認(rèn)值,而是指數(shù)據(jù)類型的零值)
private static int a =1;//此階段后a會(huì)被設(shè)置初值為0,后續(xù)初始化后才會(huì)被賦值為1
- 解析
將常量池內(nèi)的符號(hào)引用替換為直接引用 - 初始化
執(zhí)行類構(gòu)造器<clinit>()方法
<clinit>()由編譯器收集類變量賦值語(yǔ)句和靜態(tài)語(yǔ)句塊,合并而成。收集順序和語(yǔ)句在源代碼文件中的出現(xiàn)順序一致。
靜態(tài)語(yǔ)句塊可以放在變量定義前,但語(yǔ)句內(nèi)對(duì)變量的操作只能有賦值
/**
*可以正確執(zhí)行,輸出i值為2
**/
public class CliTest {
static {
i = 4;
}
private static int i =2;
public static void print(){
System.out.println(i);
}
public static void main(String[] args) {
print();
}
}
/**
*不能正確執(zhí)行,報(bào)非法向前引用
**/
public class CliTest {
static {
i = 4;
i++;
}
private static int i =2;
public static void print(){
System.out.println(i);
}
public static void main(String[] args) {
print();
}
}
虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境下被正確執(zhí)行。
多線程下,一個(gè)線程進(jìn)入執(zhí)行<clinit>()方法,其它線程會(huì)阻塞、等待。(但靜態(tài)語(yǔ)句塊只會(huì)被執(zhí)行一次,即使阻塞解除,其它線程也不會(huì)再執(zhí)行靜態(tài)語(yǔ)句塊)
類加載器
通過(guò)類全限定名加載類二進(jìn)制字節(jié)流的動(dòng)作是放在java虛擬機(jī)外實(shí)現(xiàn)得。我們可以通過(guò)java程序?qū)崿F(xiàn)自己的類加載器。
類的唯一性,由加載這個(gè)類的加載器和類本身確定
雙親委派模型
類加載器種類:
- 啟動(dòng)類加載器(Bootstrap ClassLoader) :屬于java虛擬機(jī)的一部分。負(fù)責(zé)加載存放在<java_home>\lib 或 -Xbootclasspath指定路徑下的符合條件(僅通過(guò)文件名識(shí)別,如rt.jar)的類文件。
** 啟動(dòng)類加載器無(wú)法被程序直接引用。如果自定義類加載器,需要把加載請(qǐng)求委托給啟動(dòng)類加載器,直接用null代替即可 ** - 擴(kuò)展類加載器(Extension ClassLoader):由ExtClassLoader實(shí)現(xiàn)。負(fù)責(zé)加載存放在<java_home>\lib\ext 或 java.ext.dirs指定的目錄下的類文件。開(kāi)發(fā)者可以直接使用。
- 應(yīng)用程序類加載器(Application ClassLoader):由AppClassLoader實(shí)現(xiàn)。用來(lái)加載用戶類路徑上的類文件。如果用戶沒(méi)有自定義類加載器默認(rèn)用的就是這個(gè)類加載器。
雙親委派模型

除了啟動(dòng)類加載器都有自己的父類加載器。當(dāng)一個(gè)類加載器收到類加載請(qǐng)求時(shí),首先自己不會(huì)加載該類,而是把請(qǐng)求委派給自己的父類加載器。父加載器也會(huì)將請(qǐng)求委派給它的父類加載器,直到最終委派到啟動(dòng)類加載器。只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法加載該類時(shí),子類才會(huì)嘗試去加載。
內(nèi)存模型






