[轉(zhuǎn)載]JVM內(nèi)幕:Java虛擬機(jī)詳解

這篇文章解釋了Java 虛擬機(jī)(JVM)的內(nèi)部架構(gòu)渊抄。下圖顯示了遵守 Java SE 7 規(guī)范的典型的 JVM 核心內(nèi)部組件尖滚。

上圖顯示的組件分兩個章節(jié)解釋脐往。第一章討論針對每個線程創(chuàng)建的組件颂郎,第二章節(jié)討論了線程無關(guān)組件薪铜。

  • 線程
    JVM 系統(tǒng)線程
    每個線程相關(guān)的
    程序計(jì)數(shù)器

    本地棧
    棧限制
    棧幀
    局部變量數(shù)組
    操作數(shù)棧
    動態(tài)鏈接
  • 線程共享

    內(nèi)存管理
    非堆內(nèi)存
    即時編譯
    方法區(qū)
    類文件結(jié)構(gòu)
    類加載器
    更快的類加載
    方法區(qū)在哪里
    類加載器參考
    運(yùn)行時常量池
    異常表
    符號表
    Interned 字符串

線程

這里所說的線程指程序執(zhí)行過程中的一個線程實(shí)體众弓。JVM 允許一個應(yīng)用并發(fā)執(zhí)行多個線程。Hotspot JVM 中的 Java 線程與原生操作系統(tǒng)線程有直接的映射關(guān)系隔箍。當(dāng)線程本地存儲谓娃、緩沖區(qū)分配、同步對象蜒滩、棧滨达、程序計(jì)數(shù)器等準(zhǔn)備好以后,就會創(chuàng)建一個操作系統(tǒng)原生線程俯艰。Java 線程結(jié)束捡遍,原生線程隨之被回收。操作系統(tǒng)負(fù)責(zé)調(diào)度所有線程蟆炊,并把它們分配到任何可用的 CPU 上稽莉。當(dāng)原生線程初始化完畢,就會調(diào)用 Java 線程的 run() 方法涩搓。run() 返回時污秆,被處理未捕獲異常,原生線程將確認(rèn)由于它的結(jié)束是否要終止 JVM 進(jìn)程(比如這個線程是最后一個非守護(hù)線程)昧甘。當(dāng)線程結(jié)束時良拼,會釋放原生線程和 Java 線程的所有資源。

JVM 系統(tǒng)線程

如果使用 jconsole 或者其它調(diào)試器充边,你會看到很多線程在后臺運(yùn)行庸推。這些后臺線程與觸發(fā) public static void main(String[]) 函數(shù)的主線程以及主線程創(chuàng)建的其他線程一起運(yùn)行常侦。Hotspot JVM 后臺運(yùn)行的系統(tǒng)線程主要有下面幾個:

虛擬機(jī)線程(VM thread)

這個線程等待 JVM 到達(dá)安全點(diǎn)操作出現(xiàn)。這些操作必須要在獨(dú)立的線程里執(zhí)行贬媒,因?yàn)楫?dāng)堆修改無法進(jìn)行時聋亡,線程都需要 JVM 位于安全點(diǎn)。這些操作的類型有:stop-the-world 垃圾回收际乘、線程棧 dump坡倔、線程暫停、線程偏向鎖(biased locking)解除脖含。

周期性任務(wù)線程

這線程負(fù)責(zé)定時器事件(也就是中斷)罪塔,用來調(diào)度周期性操作的執(zhí)行。

GC 線程

這些線程支持 JVM 中不同的垃圾回收活動养葵。

編譯器線程

這些線程在運(yùn)行時將字節(jié)碼動態(tài)編譯成本地平臺相關(guān)的機(jī)器碼征堪。

信號分發(fā)線程

這個線程接收發(fā)送到 JVM 的信號并調(diào)用適當(dāng)?shù)?JVM 方法處理。

線程相關(guān)組件

每個運(yùn)行的線程都包含下面這些組件:

程序計(jì)數(shù)器(PC)

PC 指當(dāng)前指令(或操作碼)的地址关拒,本地指令除外佃蚜。如果當(dāng)前方法是 native 方法,那么PC 的值為 undefined着绊。所有的 CPU 都有一個 PC爽锥,典型狀態(tài)下,每執(zhí)行一條指令 PC 都會自增畔柔,因此 PC 存儲了指向下一條要被執(zhí)行的指令地址氯夷。JVM 用 PC 來跟蹤指令執(zhí)行的位置,PC 將實(shí)際上是指向方法區(qū)(Method Area)的一個內(nèi)存地址靶擦。

棧(Stack)

每個線程擁有自己的棧腮考,棧包含每個方法執(zhí)行的棧幀。棧是一個后進(jìn)先出(LIFO)的數(shù)據(jù)結(jié)構(gòu)玄捕,因此當(dāng)前執(zhí)行的方法在棧的頂部踩蔚。每次方法調(diào)用時,一個新的棧幀創(chuàng)建并壓棧到棧頂枚粘。當(dāng)方法正常返回或拋出未捕獲的異常時馅闽,棧幀就會出棧。除了棧幀的壓棧和出棧馍迄,棧不能被直接操作福也。所以可以在堆上分配棧幀,并且不需要連續(xù)內(nèi)存攀圈。

Native棧

并非所有的 JVM 實(shí)現(xiàn)都支持本地(native)方法暴凑,那些提供支持的 JVM 一般都會為每個線程創(chuàng)建本地方法棧。如果 JVM 用 C-linkage 模型實(shí)現(xiàn) JNI(Java Native Invocation)赘来,那么本地棧就是一個 C 的棧现喳。在這種情況下凯傲,本地方法棧的參數(shù)順序、返回值和典型的 C 程序相同嗦篱。本地方法一般來說可以(依賴 JVM 的實(shí)現(xiàn))反過來調(diào)用 JVM 中的 Java 方法冰单。這種 native 方法調(diào)用 Java 會發(fā)生在棧(一般是 Java 棧)上;線程將離開本地方法棧灸促,并在 Java 棧上開辟一個新的棧幀球凰。

棧的限制

棧可以是動態(tài)分配也可以固定大小腿宰。如果線程請求一個超過允許范圍的空間,就會拋出一個StackOverflowError缘厢。如果線程需要一個新的棧幀吃度,但是沒有足夠的內(nèi)存可以分配,就會拋出一個 OutOfMemoryError贴硫。

棧幀(Frame)

每次方法調(diào)用都會新建一個新的棧幀并把它壓棧到棧頂椿每。當(dāng)方法正常返回或者調(diào)用過程中拋出未捕獲的異常時,棧幀將出棧英遭。更多關(guān)于異常處理的細(xì)節(jié)间护,可以參考下面的異常信息表章節(jié)。

每個棧幀包含:

  • 局部變量數(shù)組
  • 返回值
  • 操作數(shù)棧
  • 類當(dāng)前方法的運(yùn)行時常量池引用

局部變量數(shù)組

局部變量數(shù)組包含了方法執(zhí)行過程中的所有變量挖诸,包括 this 引用汁尺、所有方法參數(shù)、其他局部變量多律。對于類方法(也就是靜態(tài)方法)痴突,方法參數(shù)從下標(biāo) 0 開始,對于對象方法狼荞,位置0保留為 this辽装。

有下面這些局部變量:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了 long 和 double 類型以外,所有的變量類型都占用局部變量數(shù)組的一個位置相味。long 和 double 需要占用局部變量數(shù)組兩個連續(xù)的位置拾积,因?yàn)樗鼈兪?64 位雙精度,其它類型都是 32 位單精度丰涉。

操作數(shù)棧

操作數(shù)棧在執(zhí)行字節(jié)碼指令過程中被用到拓巧,這種方式類似于原生 CPU 寄存器。大部分 JVM 字節(jié)碼把時間花費(fèi)在操作數(shù)棧的操作上:入棧一死、出棧玲销、復(fù)制、交換摘符、產(chǎn)生消費(fèi)變量的操作贤斜。因此策吠,局部變量數(shù)組和操作數(shù)棧之間的交換變量指令操作通過字節(jié)碼頻繁執(zhí)行。比如瘩绒,一個簡單的變量初始化語句將產(chǎn)生兩條跟操作數(shù)棧交互的字節(jié)碼猴抹。

int i;

被編譯成下面的字節(jié)碼:

0:    iconst_0    // Push 0 to top of the operand stack
1:    istore_1    // Pop value from top of operand stack and store as local variable 1

更多關(guān)于局部變量數(shù)組、操作數(shù)棧和運(yùn)行時常量池之間交互的詳細(xì)信息锁荔,可以在類文件結(jié)構(gòu)部分找到蟀给。

動態(tài)鏈接

每個棧幀都有一個運(yùn)行時常量池的引用。這個引用指向棧幀當(dāng)前運(yùn)行方法所在類的常量池阳堕。通過這個引用支持動態(tài)鏈接(dynamic linking)跋理。

C/C++ 代碼一般被編譯成對象文件,然后多個對象文件被鏈接到一起產(chǎn)生可執(zhí)行文件或者 dll恬总。在鏈接階段前普,每個對象文件的符號引用被替換成了最終執(zhí)行文件的相對偏移內(nèi)存地址。在 Java中壹堰,鏈接階段是運(yùn)行時動態(tài)完成的拭卿。

當(dāng) Java 類文件編譯時,所有變量和方法的引用都被當(dāng)做符號引用存儲在這個類的常量池中贱纠。符號引用是一個邏輯引用峻厚,實(shí)際上并不指向物理內(nèi)存地址。JVM 可以選擇符號引用解析的時機(jī)谆焊,一種是當(dāng)類文件加載并校驗(yàn)通過后惠桃,這種解析方式被稱為饑餓方式。另外一種是符號引用在第一次使用的時候被解析辖试,這種解析方式稱為惰性方式刽射。無論如何 ,JVM 必須要在第一次使用符號引用時完成解析并拋出可能發(fā)生的解析錯誤剃执。綁定是將對象域誓禁、方法、類的符號引用替換為直接引用的過程肾档。綁定只會發(fā)生一次摹恰。一旦綁定,符號引用會被完全替換怒见。如果一個類的符號引用還沒有被解析俗慈,那么就會載入這個類。每個直接引用都被存儲為相對于存儲結(jié)構(gòu)(與運(yùn)行時變量或方法的位置相關(guān)聯(lián)的)偏移量遣耍。

線程間共享

堆被用來在運(yùn)行時分配類實(shí)例闺阱、數(shù)組。不能在棧上存儲數(shù)組和對象舵变。因?yàn)闂辉O(shè)計(jì)為創(chuàng)建以后無法調(diào)整大小酣溃。棧幀只存儲指向堆中對象或數(shù)組的引用瘦穆。與局部變量數(shù)組(每個棧幀中的)中的原始類型和引用類型不同,對象總是存儲在堆上以便在方法結(jié)束時不會被移除赊豌。對象只能由垃圾回收器移除扛或。

為了支持垃圾回收機(jī)制,堆被分為了下面三個區(qū)域:

  • 新生代
    經(jīng)常被分為 Eden 和 Survivor
  • 老年代
  • 永久代

內(nèi)存管理
對象和數(shù)組永遠(yuǎn)不會顯式回收碘饼,而是由垃圾回收器自動回收熙兔。通常,過程是這樣的:

  1. 新的對象和數(shù)組被創(chuàng)建并放入老年代艾恼。
  2. Minor垃圾回收將發(fā)生在新生代住涉。依舊存活的對象將從 eden 區(qū)移到 survivor 區(qū)。
  3. Major垃圾回收一般會導(dǎo)致應(yīng)用進(jìn)程暫停钠绍,它將在三個區(qū)內(nèi)移動對象舆声。仍然存活的對象將被從新生代移動到老年代。
  4. 每次進(jìn)行老年代回收時也會進(jìn)行永久代回收五慈。它們之中任何一個變滿時,都會進(jìn)行回收主穗。

非堆內(nèi)存

非堆內(nèi)存指的是那些邏輯上屬于 JVM 一部分對象泻拦,但實(shí)際上不在堆上創(chuàng)建。

非堆內(nèi)存包括:

  • 永久代忽媒,包括:
  1. 方法區(qū)
  2. 駐留字符串(interned strings)
  • 代碼緩存(Code Cache):用于編譯和存儲那些被 JIT 編譯器編譯成原生代碼的方法争拐。

即時編譯(JIT)

Java 字節(jié)碼是解釋執(zhí)行的,但是沒有直接在 JVM 宿主執(zhí)行原生代碼快晦雨。為了提高性能架曹,Oracle Hotspot 虛擬機(jī)會找到執(zhí)行最頻繁的字節(jié)碼片段并把它們編譯成原生機(jī)器碼。編譯出的原生機(jī)器碼被存儲在非堆內(nèi)存的代碼緩存中闹瞧。通過這種方法绑雄,Hotspot 虛擬機(jī)將權(quán)衡下面兩種時間消耗:將字節(jié)碼編譯成本地代碼需要的額外時間和解釋執(zhí)行字節(jié)碼消耗更多的時間。

方法區(qū)

方法區(qū)存儲了每個類的信息奥邮,比如:

  • Classloader 引用

  • 運(yùn)行時常量池
    數(shù)值型常量
    字段引用
    方法引用
    屬性

  • 字段數(shù)據(jù) (針對每個字段的信息)
    字段名
    類型
    修飾符
    屬性(Attribute)

  • 方法數(shù)據(jù) (每個方法)
    方法名
    返回值類型
    參數(shù)類型(按順序)
    修飾符
    屬性

  • 方法代碼 (每個方法)
    字節(jié)碼
    操作數(shù)棧大小
    局部變量大小
    局部變量表
    異常表
    每個異常處理器
    開始點(diǎn)
    結(jié)束點(diǎn)
    異常處理代碼的程序計(jì)數(shù)器(PC)偏移量
    被捕獲的異常類對應(yīng)的常量池下標(biāo)

所有線程共享同一個方法區(qū)万牺,因此訪問方法區(qū)數(shù)據(jù)的和動態(tài)鏈接的進(jìn)程必須線程安全。如果兩個線程試圖訪問一個還未加載的類的字段或方法洽腺,必須只加載一次脚粟,而且兩個線程必須等它加載完畢才能繼續(xù)執(zhí)行。

類文件結(jié)構(gòu)

一個編譯后的類文件包含下面的結(jié)構(gòu):

ClassFile {
    u4            magic;
    u2            minor_version;
    u2            major_version;
    u2            constant_pool_count;
    cp_info        contant_pool[constant_pool_count – 1];
    u2            access_flags;
    u2            this_class;
    u2            super_class;
    u2            interfaces_count;
    u2            interfaces[interfaces_count];
    u2            fields_count;
    field_info        fields[fields_count];
    u2            methods_count;
    method_info        methods[methods_count];
    u2            attributes_count;
    attribute_info    attributes[attributes_count];
}
  • magic, minor_version, major_version
    類文件的版本信息和用于編譯這個類的 JDK 版本蘸朋。
  • constant_pool
    類似于符號表核无,盡管它包含更多數(shù)據(jù)。下面有更多的詳細(xì)描述藕坯。
  • access_flags
    提供這個類的描述符列表团南。
  • this_class
    提供這個類全名的常量池(constant_pool)索引噪沙,比如org/jamesdbloom/foo/Bar。
  • super_class
    提供這個類的父類符號引用的常量池索引已慢。
  • interfaces
    指向常量池的索引數(shù)組曲聂,提供那些被實(shí)現(xiàn)的接口的符號引用。
  • fields
    提供每個字段完整描述的常量池索引數(shù)組佑惠。
  • methods
    指向constant_pool的索引數(shù)組朋腋,用于表示每個方法簽名的完整描述。如果這個方法不是抽象方法也不是 native 方法膜楷,那么就會顯示這個函數(shù)的字節(jié)碼旭咽。
  • attributes
    不同值的數(shù)組,表示這個類的附加信息赌厅,包括 RetentionPolicy.CLASS 和RetentionPolicy.RUNTIME 注解穷绵。

可以用 javap 查看編譯后的 java class 文件字節(jié)碼。

如果你編譯下面這個簡單的類:

package org.jvminternals;
public class SimpleClass {
    public void sayHello() {
        System.out.println("Hello");
    }
}

運(yùn)行下面的命令特愿,就可以得到下面的結(jié)果輸出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class

public class org.jvminternals.SimpleClass
  SourceFile: "SimpleClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V
{
  public org.jvminternals.SimpleClass();
    Signature: ()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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      5      0    this   Lorg/jvminternals/SimpleClass;

  public void sayHello();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic      #2    // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc            #3    // String "Hello"
        5: invokevirtual  #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      9      0    this   Lorg/jvminternals/SimpleClass;
}

這個 class 文件展示了三個主要部分:常量池仲墨、構(gòu)造器方法和 sayHello 方法。

常量池

提供了通常由符號表提供的相同信息揍障,詳細(xì)描述見下文目养。

方法

每一個方法包含四個區(qū)域,

  1. 簽名和訪問標(biāo)簽
  2. 字節(jié)碼
  3. LineNumberTable:為調(diào)試器提供源碼中的每一行對應(yīng)的字節(jié)碼信息毒嫡。上面的例子中癌蚁,Java 源碼里的第 6 行與 sayHello 函數(shù)字節(jié)碼序號 0 相關(guān),第 7 行與字節(jié)碼序號 8 相關(guān)兜畸。
  4. LocalVariableTable:列出了所有棧幀中的局部變量努释。上面兩個例子中,唯一的局部變量就是 this咬摇。
    這個 class 文件用到下面這些字節(jié)碼操作符:
  • aload0
    這個操作碼是aload格式操作碼中的一個伐蒂。它們用來把對象引用加載到操作碼棧。 表示正在被訪問的局部變量數(shù)組的位置肛鹏,但只能是0饿自、1、2龄坪、3 中的一個昭雌。還有一些其它類似的操作碼用來載入非對象引用的數(shù)據(jù),如iload, lload, float 和 dload健田。其中 i 表示 int烛卧,l 表示 long,f 表示 float,d 表示 double总放。局部變量數(shù)組位置大于 3 的局部變量可以用 iload, lload, float, dload 和 aload 載入呈宇。這些操作碼都只需要一個操作數(shù),即數(shù)組中的位置
  • ldc
    這個操作碼用來將常量從運(yùn)行時常量池壓棧到操作數(shù)棧
  • getstatic
    這個操作碼用來把一個靜態(tài)變量從運(yùn)行時常量池的靜態(tài)變量列表中壓棧到操作數(shù)棧
  • invokespecial, invokevirtual
    這些操作碼屬于一組函數(shù)調(diào)用的操作碼局雄,包括:invokedynamic甥啄、invokeinterface、invokespecial炬搭、invokestatic蜈漓、invokevirtual。在這個 class 文件中宫盔,invokespecial 和 invokevirutal 兩個指令都用到了融虽,兩者的區(qū)別是,invokevirutal 指令調(diào)用一個對象的實(shí)例方法灼芭,invokespecial 指令調(diào)用實(shí)例初始化方法有额、私有方法、父類方法彼绷。
  • return
    這個操作碼屬于ireturn巍佑、lreturn、freturn寄悯、dreturn萤衰、areturn 和 return 操作碼組。每個操作碼返回一種類型的返回值热某,其中 i 表示 int腻菇,l 表示 long胳螟,f 表示 float昔馋,d 表示 double,a 表示 對象引用糖耸。沒有前綴類型字母的 return 表示返回 void

跟任何典型的字節(jié)碼一樣秘遏,操作數(shù)與局部變量、操作數(shù)棧嘉竟、運(yùn)行時常量池的主要交互如下所示邦危。

構(gòu)造器函數(shù)包含兩個指令。首先舍扰,this 變量被壓棧到操作數(shù)棧倦蚪,然后父類的構(gòu)造器函數(shù)被調(diào)用,而這個構(gòu)造器會消費(fèi) this边苹,之后 this 被彈出操作數(shù)棧陵且。

sayHello() 方法更加復(fù)雜,正如之前解釋的那樣个束,因?yàn)樗枰眠\(yùn)行時常量池中的指向符號引用的真實(shí)引用慕购。第一個操作碼 getstatic 從System類中將out靜態(tài)變量壓到操作數(shù)棧聊疲。下一個操作碼 ldc 把字符串 “Hello” 壓棧到操作數(shù)棧。最后 invokevirtual 操作符會調(diào)用 System.out 變量的 println 方法沪悲,從操作數(shù)棧作彈出”Hello” 變量作為 println 的一個參數(shù)获洲,并在當(dāng)前線程開辟一個新棧幀。

類加載器

JVM 啟動時會用 bootstrap 類加載器加載一個初始化類殿如,然后這個類會在public static void main(String[])調(diào)用之前完成鏈接和初始化贡珊。執(zhí)行這個方法會執(zhí)行加載、鏈接握截、初始化需要的額外類和接口飞崖。

加載(Loading)是這樣一個過程,找到代表這個類的 class 文件或根據(jù)特定的名字找到接口類型谨胞,然后讀取到一個字節(jié)數(shù)組中固歪。接著,這些字節(jié)會被解析檢驗(yàn)它們是否代表一個 Class 對象并包含正確的 major胯努、minor 版本信息牢裳。直接父類的類和接口也會被加載進(jìn)來。這些操作一旦完成叶沛,類或者接口對象就從二進(jìn)制表示中創(chuàng)建出來了蒲讯。

鏈接(Linking)是校驗(yàn)類或接口并準(zhǔn)備類型和父類父接口的過程。鏈接過程包含三步:校驗(yàn)(verifying)灰署、準(zhǔn)備(preparing)判帮、部分解析(optionally resolving)。

校驗(yàn) 會確認(rèn)類或者接口表示是否結(jié)構(gòu)正確溉箕,以及是否遵循 Java 語言和 JVM 的語義要求晦墙,比如會進(jìn)行下面的檢查:

  1. 格式一致且格式化正確的符號表
  2. final 方法和類沒有被重載
  3. 方法遵循訪問控制關(guān)鍵詞
  4. 方法參數(shù)的數(shù)量、類型正確
  5. 字節(jié)碼沒有不當(dāng)?shù)牟僮鳁?shù)據(jù)
  6. 變量在讀取之前被初始化過
  7. 變量值的類型正確

在驗(yàn)證階段做這些檢查意味著不需要在運(yùn)行階段做這些檢查肴茄。鏈接階段的檢查減慢了類加載的速度晌畅,但是它避免了執(zhí)行這些字節(jié)碼時的多次檢查。

準(zhǔn)備 過程包括為靜態(tài)存儲和 JVM 使用的數(shù)據(jù)結(jié)構(gòu)(比如方法表)分配內(nèi)存空間寡痰。靜態(tài)變量創(chuàng)建并初始化為默認(rèn)值抗楔,但是初始化代碼不在這個階段執(zhí)行,因?yàn)檫@是初始化過程的一部分拦坠。

解析 是可選的階段连躏。它包括通過加載引用的類和接口來檢查這些符號引用是否正確。如果不是發(fā)生在這個階段贞滨,符號引用的解析要等到字節(jié)碼指令使用這個引用的時候才會進(jìn)行入热。

類或者接口初始化 由類或接口初始化方法<clinit>的執(zhí)行組成。

JVM 中有多個類加載器,分飾不同的角色才顿。每個類加載器由它的父加載器加載莫湘。bootstrap 加載器除外,它是所有最頂層的類加載器郑气。

  • Bootstrap 加載器
    一般由本地代碼實(shí)現(xiàn)幅垮,因?yàn)樗?JVM 加載以后的早期階段就被初始化了。bootstrap 加載器負(fù)責(zé)載入基礎(chǔ)的 Java API尾组,比如包含 rt.jar忙芒。它只加載擁有較高信任級別的啟動路徑下找到的類,因此跳過了很多普通類需要做的校驗(yàn)工作讳侨。
  • Extension 加載器
    加載了標(biāo)準(zhǔn) Java 擴(kuò)展 API 中的類呵萨,比如 security 的擴(kuò)展函數(shù)。
  • System 加載器
    是應(yīng)用的默認(rèn)類加載器跨跨,比如從 classpath 中加載應(yīng)用類潮峦。
  • 用戶自定義類加載器
    也可以用來加載應(yīng)用類。使用自定義的類加載器有很多特殊的原因:運(yùn)行時重新加載類或者把加載的類分隔為不同的組勇婴,典型的用法比如 web 服務(wù)器 Tomcat忱嘹。

加速類加載

共享類數(shù)據(jù)(CDS)是Hotspot JVM 5.0 的時候引入的新特性。在 JVM 安裝過程中耕渴,安裝進(jìn)程會加載一系列核心 JVM 類(比如 rt.jar)到一個共享的內(nèi)存映射區(qū)域拘悦。CDS 減少了加載這些類需要的時間,提高了 JVM 啟動的速度橱脸,允許這些類被不同的 JVM 實(shí)例共享础米,同時也減少了內(nèi)存消耗廊散。

方法區(qū)在哪里

The Java Virtual Machine Specification Java SE 7 Edition 中寫得很清楚:“盡管方法區(qū)邏輯上屬于堆的一部分草则,簡單的實(shí)現(xiàn)可以選擇不對它進(jìn)行回收和壓縮【唬”吻商。Oracle JVM 的 jconsle 顯示方法區(qū)和 code cache 區(qū)被當(dāng)做為非堆內(nèi)存掏颊,而 OpenJDK 則顯示 CodeCache 被當(dāng)做 VM 中對象堆(ObjectHeap)的一個獨(dú)立的域糟红。

Classloader 引用

所有的類加載之后都包含一個加載自身的加載器的引用艾帐,反過來每個類加載器都包含它們加載的所有類的引用。

運(yùn)行時常量池

JVM 維護(hù)了一個按類型區(qū)分的常量池盆偿,一個類似于符號表的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)柒爸。盡管它包含更多數(shù)據(jù)。Java 字節(jié)碼需要數(shù)據(jù)事扭。這個數(shù)據(jù)經(jīng)常因?yàn)樘蟛荒苤苯哟鎯υ谧止?jié)碼中捎稚,取而代之的是存儲在常量池中,字節(jié)碼包含這個常量池的引用。運(yùn)行時常量池被用來上面介紹過的動態(tài)鏈接今野。

常量池中可以存儲多種類型的數(shù)據(jù):

  • 數(shù)字型
  • 字符串型
  • 類引用型
  • 域引用型
  • 方法引用

示例代碼如下:

Object foo = new Object();

寫成字節(jié)碼將是下面這樣:

 0: new #2          // Class java/lang/Object
 1: dup
 2: invokespecial #3    // Method java/ lang/Object "<init>"( ) V

new 操作碼的后面緊跟著操作數(shù) #2 葡公。這個操作數(shù)是常量池的一個索引,表示它指向常量池的第二個實(shí)體条霜。第二個實(shí)體是一個類的引用催什,這個實(shí)體反過來引用了另一個在常量池中包含 UTF8 編碼的字符串類名的實(shí)體(// Class java/lang/Object)。然后宰睡,這個符號引用被用來尋找 java.lang.Object 類蒲凶。new 操作碼創(chuàng)建一個類實(shí)例并初始化變量。新類實(shí)例的引用則被添加到操作數(shù)棧拆内。dup 操作碼創(chuàng)建一個操作數(shù)棧頂元素引用的額外拷貝旋圆。最后用 invokespecial 來調(diào)用第 2 行的實(shí)例初始化方法。操作碼也包含一個指向常量池的引用麸恍。初始化方法把操作數(shù)棧出棧的頂部引用當(dāng)做此方法的一個參數(shù)灵巧。最后這個新對象只有一個引用,這個對象已經(jīng)完成了創(chuàng)建及初始化抹沪。

如果你編譯下面的類:

package org.jvminternals;

public class SimpleClass {
    public void sayHello() {
        System.out.println("Hello");
    }
}

生成的類文件常量池將是這個樣子:

Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V

這個常量池包含了下面的類型:

  • Integer
    4 字節(jié)常量
  • Long
    8 字節(jié)常量
  • Float
    4 字節(jié)常量
  • Double
    8 字節(jié)常量
  • String
    字符串常量指向常量池的另外一個包含真正字節(jié) Utf8 編碼的實(shí)體
  • Utf8
    Utf8 編碼的字符序列字節(jié)流
  • Class
    一個 Class 常量孩等,指向常量池的另一個 Utf8 實(shí)體,這個實(shí)體包含了符合 JVM 內(nèi)部格式的類的全名(動態(tài)鏈接過程需要用到)
  • NameAndType
    冒號(:)分隔的一組值采够,這些值都指向常量池中的其它實(shí)體肄方。第一個值(“:”之前的)指向一個 Utf8 字符串實(shí)體,它是一個方法名或者字段名蹬癌。第二個值指向表示類型的 Utf8 實(shí)體权她。對于字段類型,這個值是類的全名逝薪,對于方法類型隅要,這個值是每個參數(shù)類型類的類全名的列表。
  • Fieldref, Methodref, InterfaceMethodref
    點(diǎn)號(.)分隔的一組值董济,每個值都指向常量池中的其它的實(shí)體步清。第一個值(“.”號之前的)指向類實(shí)體,第二個值指向 NameAndType 實(shí)體虏肾。

異常表

異常表像這樣存儲每個異常處理信息:

  • 起始點(diǎn)(Start point)
  • 結(jié)束點(diǎn)(End point)
  • 異常處理代碼的 PC 偏移量
  • 被捕獲異常的常量池索引

如果一個方法有定義 try-catch 或者 try-finally 異常處理器廓啊,那么就會創(chuàng)建一個異常表。它為每個異常處理器和 finally 代碼塊存儲必要的信息封豪,包括處理器覆蓋的代碼塊區(qū)域和處理異常的類型谴轮。

當(dāng)方法拋出異常時,JVM 會尋找匹配的異常處理器吹埠。如果沒有找到第步,那么方法會立即結(jié)束并彈出當(dāng)前棧幀疮装,這個異常會被重新拋到調(diào)用這個方法的方法中(在新的棧幀中)。如果所有的棧幀都被彈出還沒有找到匹配的異常處理器粘都,那么這個線程就會終止廓推。如果這個異常在最后一個非守護(hù)進(jìn)程拋出(比如這個線程是主線程),那么也有會導(dǎo)致 JVM 進(jìn)程終止翩隧。

Finally 異常處理器匹配所有的異常類型受啥,且不管什么異常拋出 finally 代碼塊都會執(zhí)行。在這種情況下鸽心,當(dāng)沒有異常拋出時滚局,finally 代碼塊還是會在方法最后執(zhí)行。這種靠在代碼 return 之前跳轉(zhuǎn)到 finally 代碼塊來實(shí)現(xiàn)顽频。

符號表

除了按類型來分的運(yùn)行時常量池藤肢,Hotspot JVM 在永久代還包含一個符號表。這個符號表是一個哈希表糯景,保存了符號指針到符號的映射關(guān)系(也就是 Hashtable<Symbol*, Symbol>)嘁圈,它擁有指向所有符號(包括在每個類運(yùn)行時常量池中的符號)的指針。

引用計(jì)數(shù)被用來控制一個符號從符號表從移除的過程蟀淮。比如當(dāng)一個類被卸載時最住,它擁有的在常量池中所有符號的引用計(jì)數(shù)將減少。當(dāng)符號表中的符號引用計(jì)數(shù)為 0 時怠惶,符號表會認(rèn)為這個符號不再被引用涨缚,將從符號表中卸載。符號表和后面介紹的字符串表都被保存在一個規(guī)范化的結(jié)構(gòu)中策治,以便提高效率并保證每個實(shí)例只出現(xiàn)一次脓魏。

字符串表

Java 語言規(guī)范要求相同的(即包含相同序列的 Unicode 指針序列)字符串字面量必須指向相同的 String 實(shí)例。除此之外通惫,在一個字符串實(shí)例上調(diào)用 String.intern() 方法的返回引用必須與字符串是字面量時的一樣茂翔。因此,下面的代碼返回 true:

("j" + "v" + "m").intern() == "jvm"

Hotspot JVM 中 interned 字符串保存在字符串表中履腋。字符串表是一個哈希表珊燎,保存著對象指針到符號的映射關(guān)系(也就是Hashtable<oop, Symbol>),它被保存到永久代中遵湖。符號表和字符串表的實(shí)體都以規(guī)范的格式保存悔政,保證每個實(shí)體都只出現(xiàn)一次。

當(dāng)類加載時奄侠,字符串字面量被編譯器自動 intern 并加入到符號表卓箫。除此之外载矿,String 類的實(shí)例可以調(diào)用 String.intern() 顯式地 intern垄潮。當(dāng)調(diào)用 String.intern() 方法時烹卒,如果符號表已經(jīng)包含了這個字符串,那么就會返回符號表里的這個引用弯洗,如果不是旅急,那么這個字符串就被加入到字符串表中同時返回這個引用。

原文鏈接: jamesdbloom 翻譯: ImportNew.com - 挖坑的張師傅
譯文鏈接: http://www.importnew.com/17770.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牡整,一起剝皮案震驚了整個濱河市藐吮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逃贝,老刑警劉巖谣辞,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異沐扳,居然都是意外死亡泥从,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門沪摄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來躯嫉,“玉大人,你說我怎么就攤上這事杨拐∑聿停” “怎么了?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵哄陶,是天一觀的道長帆阳。 經(jīng)常有香客問我,道長屋吨,這世上最難降的妖魔是什么舱痘? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮离赫,結(jié)果婚禮上芭逝,老公的妹妹穿的比我還像新娘。我一直安慰自己渊胸,他們只是感情好旬盯,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著翎猛,像睡著了一般胖翰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上切厘,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天萨咳,我揣著相機(jī)與錄音,去河邊找鬼疫稿。 笑死培他,一個胖子當(dāng)著我的面吹牛鹃两,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舀凛,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼俊扳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了猛遍?” 一聲冷哼從身側(cè)響起馋记,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎懊烤,沒想到半個月后梯醒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡腌紧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年冤馏,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寄啼。...
    茶點(diǎn)故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡逮光,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出墩划,到底是詐尸還是另有隱情涕刚,我是刑警寧澤,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布乙帮,位于F島的核電站杜漠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏察净。R本人自食惡果不足惜驾茴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望氢卡。 院中可真熱鬧锈至,春花似錦、人聲如沸译秦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽筑悴。三九已至们拙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阁吝,已是汗流浹背砚婆。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留突勇,地道東北人装盯。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓坷虑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親验夯。 傳聞我的和親對象是個殘疾皇子猖吴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評論 2 349