日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > java >内容正文

java

《深入理解Java虚拟机:JVM高级特性与最佳实践》 (第3版)周志明 著

發(fā)布時(shí)間:2023/12/10 java 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《深入理解Java虚拟机:JVM高级特性与最佳实践》 (第3版)周志明 著 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐

  • 第一部分 走近Java
    • 第1章 走近Java
      • 1.2 Java技術(shù)體系
      • 1.4 Java虛擬機(jī)家族
  • 第二部分 自動(dòng)內(nèi)存管理
    • 第2章 Java內(nèi)存區(qū)域與內(nèi)存溢出異常
      • 2.2 運(yùn)行時(shí)數(shù)據(jù)區(qū)域
        • 2.2.1 程序計(jì)數(shù)器
        • 2.2.2 Java虛擬機(jī)棧
        • 2.2.3 本地方法棧
        • 2.2.4 Java堆
        • 2.2.5 方法區(qū)
        • 2.2.6 運(yùn)行時(shí)常量池
        • 2.2.7 直接內(nèi)存
      • 2.3 HotSpot虛擬機(jī)對(duì)象探秘
        • 2.3.1 對(duì)象的創(chuàng)建
        • 2.3.2 對(duì)象的內(nèi)存布局
        • 2.3.3 對(duì)象的訪問(wèn)定位
      • 2.4 實(shí)戰(zhàn):OutOfMemoryError異常
        • 2.4.1 Java堆溢出
        • 2.4.2 虛擬機(jī)棧和本地方法棧溢出
        • 2.4.3 方法區(qū)和運(yùn)行時(shí)常量池溢出
        • 2.4.4 本機(jī)直接內(nèi)存溢出
    • 第3章 垃圾收集器與內(nèi)存分配策略
      • 3.1 概述
      • 3.2 對(duì)象已死?
        • 3.2.1 引用計(jì)數(shù)算法
        • 3.2.2 可達(dá)性分析算法
        • 3.2.3 再談引用
        • 3.2.4 生存還是死亡?
        • 3.2.5 回收方法區(qū)
      • 3.3 垃圾收集算法
        • 3.3.1 分代收集理論
        • 3.3.2 標(biāo)記-清除算法
        • 3.3.3 標(biāo)記-復(fù)制算法
        • 3.3.4 標(biāo)記-整理算法
      • 3.4 HotSpot的算法細(xì)節(jié)實(shí)現(xiàn)
        • 3.4.1 根節(jié)點(diǎn)枚舉
        • 3.4.2 安全點(diǎn)
        • 3.4.3 安全區(qū)域
        • 3.4.4 記憶集與卡表
        • 3.4.5 寫(xiě)屏障
        • 3.4.6 并發(fā)的可達(dá)性分析
      • 3.5 經(jīng)典垃圾收集器
        • 3.5.1 Serial收集器
        • 3.5.2 ParNew收集器
        • 3.5.3 Parallel Scavenge收集器
        • 3.5.4 Serial Old收集器
        • 3.5.5 Parallel Old收集器
        • 3.5.6 CMS收集器
        • 3.5.7 Garbage First收集器
      • 3.6 低延遲垃圾收集器
        • 3.6.1 Shenandoah收集器
        • 3.6.2 ZGC收集器
      • 3.7 選擇合適的垃圾收集器
      • 3.8 實(shí)戰(zhàn):內(nèi)存分配與回收策略
        • 3.8.1 對(duì)象優(yōu)先在Eden分配
        • 3.8.2 大對(duì)象直接進(jìn)入老年代
        • 3.8.3 長(zhǎng)期存活的對(duì)象將進(jìn)入老年代
        • 3.8.4 動(dòng)態(tài)對(duì)象年齡判定
        • 3.8.5 空間分配擔(dān)保
    • 第4章 虛擬機(jī)性能監(jiān)控、故障處理工具
      • 4.2 基礎(chǔ)故障處理工具
        • 4.2.1 jps:虛擬機(jī)進(jìn)程狀況工具
        • 4.2.2 jstat:虛擬機(jī)統(tǒng)計(jì)信息監(jiān)視工具
        • 4.2.3 jinfo:Java配置信息工具
        • 4.2.4 jmap:Java內(nèi)存映像工具
        • 4.2.5 jhat:虛擬機(jī)堆轉(zhuǎn)儲(chǔ)快照分析工具
        • 4.2.6 jstack:Java堆棧跟蹤工具
        • 4.2.7 基礎(chǔ)工具總結(jié)
      • 4.3 可視化故障處理工具
      • 4.4 HotSpot虛擬機(jī)插件及工具
    • 第5章 調(diào)優(yōu)案例分析與實(shí)戰(zhàn)
      • 5.3 實(shí)戰(zhàn):Eclipse運(yùn)行速度調(diào)優(yōu)
  • 第三部分 虛擬機(jī)執(zhí)行子系統(tǒng)
    • 第6章 類(lèi)文件結(jié)構(gòu)
      • 6.2 無(wú)關(guān)性的基石
      • 6.3 Class類(lèi)文件的結(jié)構(gòu)
        • 6.3.1 魔數(shù)與Class文件的版本
        • 6.3.2 常量池
        • 6.3.3 訪問(wèn)標(biāo)志
        • 6.3.4 類(lèi)索引、父類(lèi)索引與接口索引集合
        • 6.3.5 字段表集合
        • 6.3.6 方法表集合
        • 6.3.7 屬性表集合
      • 6.4 字節(jié)碼指令簡(jiǎn)介
        • 6.4.1 字節(jié)碼與數(shù)據(jù)類(lèi)型
        • 6.4.2 加載和存儲(chǔ)指令
        • 6.4.3 運(yùn)算指令
        • 6.4.4 類(lèi)型轉(zhuǎn)換指令
        • 6.4.5 對(duì)象創(chuàng)建與訪問(wèn)指令
        • 6.4.6 操作數(shù)棧管理指令
        • 6.4.7 控制轉(zhuǎn)移指令
        • 6.4.8 方法調(diào)用和返回指令
        • 6.4.9 異常處理指令
        • 6.4.10 同步指令
      • 6.5 公有設(shè)計(jì),私有實(shí)現(xiàn)
    • 第7章 虛擬機(jī)類(lèi)加載機(jī)制
      • 7.1 概述
      • 7.2 類(lèi)加載的時(shí)機(jī)
      • 7.3 類(lèi)加載的過(guò)程
        • 7.3.1 加載
        • 7.3.2 驗(yàn)證
        • 7.3.3 準(zhǔn)備
        • 7.3.4 解析
        • 7.3.5 初始化
      • 7.4 類(lèi)加載器
        • 7.4.1 類(lèi)與類(lèi)加載器
        • 7.4.2 雙親委派模型
        • 7.4.3 破壞雙親委派模型
      • 7.5 Java模塊化系統(tǒng)
    • 第8章 虛擬機(jī)字節(jié)碼執(zhí)行引擎
      • 8.1 概述
      • 8.2 運(yùn)行時(shí)棧幀結(jié)構(gòu)
        • 8.2.1 局部變量表
        • 8.2.2 操作數(shù)棧
        • 8.2.3 動(dòng)態(tài)連接
        • 8.2.4 方法返回地址
        • 8.2.5 附加信息
      • 8.3 方法調(diào)用
        • 8.3.1 解析
        • 8.3.2 分派
      • 8.4 動(dòng)態(tài)類(lèi)型語(yǔ)言支持
        • 8.4.1 動(dòng)態(tài)類(lèi)型語(yǔ)言
      • 8.5 基于棧的字節(jié)碼解釋執(zhí)行引擎
    • 第9章 類(lèi)加載及執(zhí)行子系統(tǒng)的案例與實(shí)戰(zhàn)
  • 第四部分 程序編譯與代碼優(yōu)化
    • 第10章 前端編譯與優(yōu)化
      • 10.1 概述
      • 10.2 Javac編譯器
        • 10.2.1 Javac的源碼與調(diào)試
        • 10.2.2 解析與填充符號(hào)表
        • 10.2.3 注解處理器
        • 10.2.4 語(yǔ)義分析與字節(jié)碼生成
      • 10.3 Java語(yǔ)法糖的味道
        • 10.3.1 泛型
        • 10.3.2 自動(dòng)裝箱、拆箱與遍歷循環(huán)
        • 10.3.3 條件編譯
      • 10.4 實(shí)戰(zhàn):插入式注解處理器
    • 第11章 后端編譯與優(yōu)化
      • 11.1 概述
      • 11.2 即時(shí)編譯器
      • 11.3 提前編譯器
      • 11.4 編譯器優(yōu)化技術(shù)
      • 11.5 實(shí)戰(zhàn):深入理解Graal編譯器
  • 第五部分 高效并發(fā)
    • 第12章 Java內(nèi)存模型與線(xiàn)程
      • 12.2 硬件的效率與一致性
      • 12.3 Java內(nèi)存模型 Java Memory Model(JMM)
        • 12.3.1 主內(nèi)存與工作內(nèi)存
        • 12.3.2 內(nèi)存間交互操作
        • 12.3.3 對(duì)于volatile型變量的特殊規(guī)則
        • 12.3.4 針對(duì)long和double型變量的特殊規(guī)則
        • 12.3.5 原子性、可見(jiàn)性與有序性
        • 12.3.6 先行發(fā)生原則
      • 12.4 Java與線(xiàn)程
        • 12.4.1 線(xiàn)程的實(shí)現(xiàn)
        • 12.4.2 Java線(xiàn)程調(diào)度
        • 12.4.3 狀態(tài)轉(zhuǎn)換
      • 12.5 Java與協(xié)程
    • 第13章 線(xiàn)程安全與鎖優(yōu)化
      • 13.1 概述
      • 13.2 線(xiàn)程安全
        • 13.2.1 Java語(yǔ)言中的線(xiàn)程安全
        • 13.2.2 線(xiàn)程安全的實(shí)現(xiàn)方法
      • 13.3 鎖優(yōu)化
        • 13.3.1 自旋鎖與自適應(yīng)鎖
        • 13.3.2 鎖消除
        • 13.3.3 鎖粗化
        • 13.3.4 輕量級(jí)鎖
        • 13.3.5 偏向鎖
  • 參考鏈接

第一部分 走近Java

第1章 走近Java

1.2 Java技術(shù)體系

  • JDK:把Java程序設(shè)計(jì)語(yǔ)言、Java虛擬機(jī)、Java類(lèi)庫(kù)這三部分統(tǒng)稱(chēng)為JDK(JavaDevelopment Kit),JDK是用于支持Java程序開(kāi)發(fā)的最小環(huán)境。
  • JRE:把Java類(lèi)庫(kù)API中的Java SE API子集和Java虛擬機(jī)這兩部分統(tǒng)稱(chēng)為JRE(Java Runtime Environment),JRE是支持Java程序運(yùn)行的標(biāo)準(zhǔn)環(huán)境。

1.4 Java虛擬機(jī)家族

  • HotSpot: 是Sun/OracleJDK和OpenJDK中默認(rèn)的java虛擬機(jī),也是目前使用范圍最廣的虛擬機(jī)。

第二部分 自動(dòng)內(nèi)存管理

第2章 Java內(nèi)存區(qū)域與內(nèi)存溢出異常

2.2 運(yùn)行時(shí)數(shù)據(jù)區(qū)域

Java虛擬機(jī)在執(zhí)行Java程序的過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存將會(huì)包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域,如圖2-1所示。

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

程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線(xiàn)程所執(zhí)行的字節(jié)碼的行號(hào)指示器

在Java虛擬機(jī)的概念模型里,字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,它是程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線(xiàn)程恢復(fù)等基礎(chǔ)功能都需要依賴(lài)這個(gè)計(jì)數(shù)器來(lái)完成。

由于Java虛擬機(jī)的多線(xiàn)程是通過(guò)線(xiàn)程輪流切換、分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來(lái)說(shuō)是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線(xiàn)程中的指令。因此,為了線(xiàn)程切換后能恢復(fù)到正確的執(zhí)行位置,每條線(xiàn)程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線(xiàn)程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱(chēng)這類(lèi)內(nèi)存區(qū)域?yàn)椤?strong>線(xiàn)程私有”的內(nèi)存。

如果線(xiàn)程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是本地(Native)方法,這個(gè)計(jì)數(shù)器值則應(yīng)為空(Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在《Java虛擬機(jī)規(guī)范》中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域。

2.2.2 Java虛擬機(jī)棧

Java虛擬機(jī)棧(Java Virtual Machine Stack)也是線(xiàn)程私有的,它的生命周期與線(xiàn)程相同。

虛擬機(jī)棧描述的是Java方法執(zhí)行的線(xiàn)程內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候,Java虛擬機(jī)都會(huì)同步創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完畢的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。

局部變量表存放了編譯期可知的各種Java虛擬機(jī)基本數(shù)據(jù)類(lèi)型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference類(lèi)型,它并不等同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔?#xff0c;也可能是指向一個(gè)代表對(duì)象的句柄或者其他與此對(duì)象相關(guān)的位置)和returnAddress類(lèi)型(指向了一條字節(jié)碼指令的地址)。這些數(shù)據(jù)類(lèi)型在局部變量表中的存儲(chǔ)空間以局部變量槽(Slot)來(lái)表示,其中64位長(zhǎng)度的long和double類(lèi)型的數(shù)據(jù)會(huì)占用兩個(gè)變量槽,其余的數(shù)據(jù)類(lèi)型只占用一個(gè)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在棧幀中分配多大的局部變量空間是完全確定的,**在方法運(yùn)行期間不會(huì)改變局部變量表的大小。**請(qǐng)讀者注意,這里說(shuō)的“大小”是指變量槽的數(shù)量,虛擬機(jī)真正使用多大的內(nèi)存空間(譬如按照1個(gè)變量槽占用32個(gè)比特、64個(gè)比特,或者更多)來(lái)實(shí)現(xiàn)一個(gè)變量槽,這是完全由具體的虛擬機(jī)實(shí)現(xiàn)自行決定的事情。

在《Java虛擬機(jī)規(guī)范》中,對(duì)Java虛擬機(jī)棧規(guī)定了兩類(lèi)異常狀況:如果線(xiàn)程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果Java虛擬機(jī)棧容量可以動(dòng)態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存會(huì)拋出OutOfMemoryError異常。

HotSpot虛擬機(jī)的棧容量不可動(dòng)態(tài)擴(kuò)展, 不會(huì)由于虛擬機(jī)無(wú)法擴(kuò)展而出現(xiàn)OutOfMemoryError,但是如果申請(qǐng)時(shí)就失敗,還是會(huì)出現(xiàn)OutOfMemoryError。

2.2.3 本地方法棧

本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的本地(Native)方法服務(wù)

《Java虛擬機(jī)規(guī)范》對(duì)本地方法棧中方法使用的語(yǔ)言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒(méi)有任何強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以根據(jù)需要自由實(shí)現(xiàn)它,甚至有的Java虛擬機(jī)(譬如Hot-Spot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。

與虛擬機(jī)棧一樣,本地方法棧也會(huì)在棧深度溢出或者棧擴(kuò)展失敗時(shí)分別拋出StackOverflowError和OutOfMemoryError異常

2.2.4 Java堆

Java堆是被所有線(xiàn)程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,是虛擬機(jī)所管理的內(nèi)存中最大的一塊。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例

在《Java虛擬機(jī)規(guī)范》中對(duì)Java堆的描述是:“所有的對(duì)象實(shí)例以及數(shù)組都應(yīng)當(dāng)在堆上分配”。

Java堆是垃圾收集器管理的內(nèi)存區(qū)域,因此也被稱(chēng)作GC堆。從回收內(nèi)存的角度看,由于現(xiàn)代垃圾收集器大部分都是基于分代收集理論設(shè)計(jì)的,所以Java堆中經(jīng)常會(huì)出現(xiàn)“新生代”“老年代”“永久代”“Eden空間”“From Survivor空間”“To Survivor空間”等名詞。這些區(qū)域劃分是垃圾收集器的共同特性,而非某個(gè)java虛擬機(jī)具體實(shí)現(xiàn)的固有內(nèi)存分布。

如果從分配內(nèi)存的角度看,所有線(xiàn)程共享的Java堆中可以劃分出多個(gè)線(xiàn)程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB),以提升對(duì)象分配時(shí)的效率。不過(guò)無(wú)論從什么角度,無(wú)論如何劃分,都不會(huì)改變Java堆中存儲(chǔ)內(nèi)容的共性,無(wú)論是哪個(gè)區(qū)域,存儲(chǔ)的都只能是對(duì)象的實(shí)例,將Java堆細(xì)分的目的只是為了更好地回收內(nèi)存,或者更快地分配內(nèi)存。

根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的,但對(duì)于大對(duì)象(典型的如數(shù)組對(duì)象),多數(shù)虛擬機(jī)實(shí)現(xiàn)出于實(shí)現(xiàn)簡(jiǎn)單、存儲(chǔ)高效的考慮,很可能會(huì)要求連續(xù)的內(nèi)存空間。

Java堆既可以被實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過(guò)當(dāng)前主流的Java虛擬機(jī)都是按照可擴(kuò)展來(lái)實(shí)現(xiàn)的(通過(guò)參數(shù)-Xmx和-Xms設(shè)定)。如果在Java堆中沒(méi)有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí),Java虛擬機(jī)將會(huì)拋出OutOfMemoryError異常

2.2.5 方法區(qū)

方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線(xiàn)程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)

雖然《Java虛擬機(jī)規(guī)范》中把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫作“非堆”(Non-Heap),目的是與Java堆區(qū)分開(kāi)來(lái)。

在JDK 8以前,許多Java程序員都習(xí)慣在HotSpot虛擬機(jī)上開(kāi)發(fā)、部署程序,很多人都更愿意把方法區(qū)稱(chēng)呼為“永久代”(Permanent Generation),或?qū)烧呋鞛橐徽劇1举|(zhì)上這兩者并不是等價(jià)的,因?yàn)閮H僅是當(dāng)時(shí)的HotSpot虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)選擇把收集器的分代設(shè)計(jì)擴(kuò)展至方法區(qū),或者說(shuō)使用永久代來(lái)實(shí)現(xiàn)方法區(qū)而已,這樣使得HotSpot的垃圾收集器能夠像管理Java堆一樣管理這部分內(nèi)存,省去專(zhuān)門(mén)為方法區(qū)編寫(xiě)內(nèi)存管理代碼的工作。

當(dāng)年使用永久代來(lái)實(shí)現(xiàn)方法區(qū)的決定并不是一個(gè)好主意,這種設(shè)計(jì)導(dǎo)致了Java應(yīng)用更容易遇到內(nèi)存溢出的問(wèn)題(永久代有-XX:MaxPermSize的上限,即使不設(shè)置也有默認(rèn)大小,而J9和JRockit只要沒(méi)有觸碰到進(jìn)程可用內(nèi)存的上限,例如32位系統(tǒng)中的4GB限制,就不會(huì)出問(wèn)題),而且有極少數(shù)方法(例如String::intern())會(huì)因永久代的原因而導(dǎo)致不同虛擬機(jī)下有不同的表現(xiàn)。

在JDK 6的時(shí)候HotSpot開(kāi)發(fā)團(tuán)隊(duì)就有放棄永久代,逐步改為采用本地內(nèi)存(NativeMemory)來(lái)實(shí)現(xiàn)方法區(qū)的計(jì)劃了。到了JDK 7的HotSpot,已經(jīng)把原本放在永久代的字符串常量池、靜態(tài)變量等移出,而到了JDK 8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Meta-space)來(lái)代替,把JDK 7中永久代還剩余的內(nèi)容(主要是類(lèi)型信息)全部移到元空間中。

根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,如果方法區(qū)無(wú)法滿(mǎn)足新的內(nèi)存分配需求時(shí),將拋出OutOfMemoryError異常。

2.2.6 運(yùn)行時(shí)常量池

運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類(lèi)的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。

運(yùn)行時(shí)常量池相對(duì)于Class文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性,Java語(yǔ)言并不要求常量一定只有編譯期才能產(chǎn)生,也就是說(shuō),并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可以將新的常量放入池中,這種特性被開(kāi)發(fā)人員利用得比較多的便是String類(lèi)的intern()方法。

既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常。

2.2.7 直接內(nèi)存

直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn),所以我們放到這里一起講解。

在JDK 1.4中新加入了NIO(New Input/Output)類(lèi),引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆里面的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來(lái)回復(fù)制數(shù)據(jù)。

2.3 HotSpot虛擬機(jī)對(duì)象探秘

基于實(shí)用優(yōu)先的原則,筆者以最常用的虛擬機(jī)HotSpot和最常用的內(nèi)存區(qū)域Java堆為例,深入探討一下HotSpot虛擬機(jī)在Java堆中對(duì)象分配、布局和訪問(wèn)的全過(guò)程。

2.3.1 對(duì)象的創(chuàng)建

當(dāng)Java虛擬機(jī)遇到一條字節(jié)碼new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類(lèi)的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已被加載、解析和初始化過(guò)。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程。

在類(lèi)加載檢查通過(guò)后,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類(lèi)加載完成后便可完全確定,為對(duì)象分配空間的任務(wù)實(shí)際上便等同于把一塊確定大小的內(nèi)存塊從Java堆中劃分出來(lái)。假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有被使用過(guò)的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間方向挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱(chēng)為“指針碰撞”(Bump The Pointer)。但如果Java堆中的內(nèi)存并不是規(guī)整的,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò)在一起,那就沒(méi)有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱(chēng)為**“空閑列表”(Free List)。選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定**。因此,當(dāng)使用Serial、ParNew等帶壓縮整理過(guò)程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞,既簡(jiǎn)單又高效;而當(dāng)使用CMS這種基于清除(Sweep)算法的收集器時(shí),理論上[插圖]就只能采用較為復(fù)雜的空閑列表來(lái)分配內(nèi)存。

對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使僅僅修改一個(gè)指針?biāo)赶虻奈恢?#xff0c;在并發(fā)情況下也并不是線(xiàn)程安全的,可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒(méi)來(lái)得及修改,對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。解決這個(gè)問(wèn)題有兩種可選方案:一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上虛擬機(jī)是采用CAS配上失敗重試的方式保證更新操作的原子性;另外一種是把內(nèi)存分配的動(dòng)作按照線(xiàn)程劃分在不同的空間之中進(jìn)行,即每個(gè)線(xiàn)程在Java堆中預(yù)先分配一小塊內(nèi)存,稱(chēng)為本地線(xiàn)程分配緩沖(Thread Local Allocation Buffer,TLAB),哪個(gè)線(xiàn)程要分配內(nèi)存,就在哪個(gè)線(xiàn)程的本地緩沖區(qū)中分配,只有本地緩沖區(qū)用完了,分配新的緩存區(qū)時(shí)才需要同步鎖定。虛擬機(jī)是否使用TLAB,可以通過(guò)-XX:+/-UseTLAB參數(shù)來(lái)設(shè)定。

內(nèi)存分配完成之后,虛擬機(jī)必須將分配到的內(nèi)存空間(但不包括對(duì)象頭)都初始化為零值,如果使用了TLAB的話(huà),這一項(xiàng)工作也可以提前至TLAB分配時(shí)順便進(jìn)行。這步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,使程序能訪問(wèn)到這些字段的數(shù)據(jù)類(lèi)型所對(duì)應(yīng)的零值。

接下來(lái),Java虛擬機(jī)還要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例、如何才能找到類(lèi)的元數(shù)據(jù)信息、對(duì)象的哈希碼(實(shí)際上對(duì)象的哈希碼會(huì)延后到真正調(diào)用Object::hashCode()方法時(shí)才計(jì)算)、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)之中。根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式。

在上面工作都完成之后,從虛擬機(jī)的視角來(lái)看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了。但是從Java程序的視角看來(lái),對(duì)象創(chuàng)建才剛剛開(kāi)始——構(gòu)造函數(shù),即Class文件中的()方法還沒(méi)有執(zhí)行,所有的字段都為默認(rèn)的零值,對(duì)象需要的其他資源和狀態(tài)信息也還沒(méi)有按照預(yù)定的意圖構(gòu)造好。一般來(lái)說(shuō)(由字節(jié)碼流中new指令后面是否跟隨invokespecial指令所決定,Java編譯器會(huì)在遇到new關(guān)鍵字的地方同時(shí)生成這兩條字節(jié)碼指令,但如果直接通過(guò)其他方式產(chǎn)生的則不一定如此),new指令之后會(huì)接著執(zhí)行()方法,按照程序員的意愿對(duì)對(duì)象進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全被構(gòu)造出來(lái)。

2.3.2 對(duì)象的內(nèi)存布局

在HotSpot虛擬機(jī)里,對(duì)象在堆內(nèi)存中的存儲(chǔ)布局可以劃分為三個(gè)部分:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)

HotSpot虛擬機(jī)對(duì)象的第一部分對(duì)象頭部分包括兩類(lèi)信息:

第一類(lèi)是用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線(xiàn)程持有的鎖、偏向線(xiàn)程ID、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開(kāi)啟壓縮指針)中分別為32個(gè)比特和64個(gè)比特,官方稱(chēng)它為“Mark Word”。

對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,其實(shí)已經(jīng)超出了32、64位Bitmap結(jié)構(gòu)所能記錄的最大限度,但對(duì)象頭里的信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,考慮到虛擬機(jī)的空間效率,Mark Word被設(shè)計(jì)成一個(gè)有著動(dòng)態(tài)定義的數(shù)據(jù)結(jié)構(gòu),以便在極小的空間內(nèi)存儲(chǔ)盡量多的數(shù)據(jù),根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。例如在32位的HotSpot虛擬機(jī)中,如對(duì)象未被同步鎖鎖定的狀態(tài)下,Mark Word的32個(gè)比特存儲(chǔ)空間中的25個(gè)比特用于存儲(chǔ)對(duì)象哈希碼,4個(gè)比特用于存儲(chǔ)對(duì)象分代年齡,2個(gè)比特用于存儲(chǔ)鎖標(biāo)志位,1個(gè)比特固定為0,在其他狀態(tài)(輕量級(jí)鎖定、重量級(jí)鎖定、GC標(biāo)記、可偏向)下對(duì)象的存儲(chǔ)內(nèi)容如表2-1所示。

第二類(lèi)是類(lèi)型指針,即對(duì)象指向它的類(lèi)型元數(shù)據(jù)的指針,Java虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定該對(duì)象是哪個(gè)類(lèi)的實(shí)例。如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過(guò)普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是如果數(shù)組的長(zhǎng)度是不確定的,將無(wú)法通過(guò)元數(shù)據(jù)中的信息推斷出數(shù)組的大小。

第二部分實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,即我們?cè)诔绦虼a里面所定義的各種類(lèi)型的字段內(nèi)容,無(wú)論是從父類(lèi)繼承下來(lái)的,還是在子類(lèi)中定義的字段都必須記錄起來(lái)。這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)(-XX:FieldsAllocationStyle參數(shù))和字段在Java源碼中定義順序的影響。HotSpot虛擬機(jī)默認(rèn)的分配順序?yàn)閘ongs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),從以上默認(rèn)的分配策略中可以看到,相同寬度的字段總是被分配到一起存放,在滿(mǎn)足這個(gè)前提條件的情況下,在父類(lèi)中定義的變量會(huì)出現(xiàn)在子類(lèi)之前。如果HotSpot虛擬機(jī)的+XX:CompactFields參數(shù)值為true(默認(rèn)就為true),那子類(lèi)之中較窄的變量也允許插入父類(lèi)變量的空隙之中,以節(jié)省出一點(diǎn)點(diǎn)空間。

第三部分是對(duì)齊填充,這并不是必然存在的,也沒(méi)有特別的含義,它僅僅起著占位符的作用。由于HotSpot虛擬機(jī)的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話(huà)說(shuō)就是任何對(duì)象的大小都必須是8字節(jié)的整數(shù)倍。對(duì)象頭部分已經(jīng)被精心設(shè)計(jì)成正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,如果對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊的話(huà),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。

2.3.3 對(duì)象的訪問(wèn)定位

Java程序會(huì)通過(guò)棧上的reference數(shù)據(jù)來(lái)操作堆上的具體對(duì)象。對(duì)象訪問(wèn)方式也是由虛擬機(jī)實(shí)現(xiàn)而定的,主流的訪問(wèn)方式主要有使用句柄和直接指針兩種:

·如果使用句柄訪問(wèn)的話(huà),Java堆中將可能會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)各自具體的地址信息,其結(jié)構(gòu)如圖2-2所示。

如果使用直接指針訪問(wèn)的話(huà),Java堆中對(duì)象的內(nèi)存布局就必須考慮如何放置訪問(wèn)類(lèi)型數(shù)據(jù)的相關(guān)信息,reference中存儲(chǔ)的直接就是對(duì)象地址,如果只是訪問(wèn)對(duì)象本身的話(huà),就不需要多一次間接訪問(wèn)的開(kāi)銷(xiāo),如圖2-3所示。

使用句柄來(lái)訪問(wèn)的最大好處就是reference中存儲(chǔ)的是穩(wěn)定句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要被修改

使用直接指針來(lái)訪問(wèn)最大的好處就是速度更快,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷(xiāo),由于對(duì)象訪問(wèn)在Java中非常頻繁,因此這類(lèi)開(kāi)銷(xiāo)積少成多也是一項(xiàng)極為可觀的執(zhí)行成本,就本書(shū)討論的主要虛擬機(jī)HotSpot而言,它主要使用第二種方式進(jìn)行對(duì)象訪問(wèn)。

2.4 實(shí)戰(zhàn):OutOfMemoryError異常

2.4.1 Java堆溢出

將堆的最小值-Xms參數(shù)與最大值-Xmx參數(shù)設(shè)置為一樣即可避免堆自動(dòng)擴(kuò)展),通過(guò)參數(shù)-XX:+HeapDumpOnOutOf-MemoryError可以讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常的時(shí)候Dump出當(dāng)前的內(nèi)存堆轉(zhuǎn)儲(chǔ)快照以便進(jìn)行事后分析

要解決這個(gè)內(nèi)存區(qū)域的異常,常規(guī)的處理方法是首先通過(guò)內(nèi)存映像分析工具(如EclipseMemory Analyzer)對(duì)Dump出來(lái)的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析。

第一步首先應(yīng)確認(rèn)內(nèi)存中導(dǎo)致OOM的對(duì)象是否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。

內(nèi)存泄漏與內(nèi)存溢出的區(qū)別:
[https://blog.csdn.net/tanga842428/article/details/52452369]

如果是內(nèi)存泄漏,可進(jìn)一步通過(guò)工具查看泄漏對(duì)象到GC Roots的引用鏈,找到泄漏對(duì)象是通過(guò)怎樣的引用路徑、與哪些GC Roots相關(guān)聯(lián),才導(dǎo)致垃圾收集器無(wú)法回收它們,根據(jù)泄漏對(duì)象的類(lèi)型信息以及它到GC Roots引用鏈的信息,一般可以比較準(zhǔn)確地定位到這些對(duì)象創(chuàng)建的位置,進(jìn)而找出產(chǎn)生內(nèi)存泄漏的代碼的具體位置。

如果不是內(nèi)存泄漏,換句話(huà)說(shuō)就是內(nèi)存中的對(duì)象確實(shí)都是必須存活的,那就應(yīng)當(dāng)檢查Java虛擬機(jī)的堆參數(shù)(-Xmx與-Xms)設(shè)置,與機(jī)器的內(nèi)存對(duì)比,看看是否還有向上調(diào)整的空間。再?gòu)拇a上檢查是否存在某些對(duì)象生命周期過(guò)長(zhǎng)、持有狀態(tài)時(shí)間過(guò)長(zhǎng)、存儲(chǔ)結(jié)構(gòu)設(shè)計(jì)不合理等情況,盡量減少程序運(yùn)行期的內(nèi)存消耗。

2.4.2 虛擬機(jī)棧和本地方法棧溢出

棧容量只能由-Xss參數(shù)來(lái)設(shè)定。

關(guān)于虛擬機(jī)棧和本地方法棧,在《Java虛擬機(jī)規(guī)范》中描述了兩種異常:

1)如果線(xiàn)程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常。

2)如果虛擬機(jī)的棧內(nèi)存允許動(dòng)態(tài)擴(kuò)展,當(dāng)擴(kuò)展棧容量無(wú)法申請(qǐng)到足夠的內(nèi)存時(shí),將拋出OutOfMemoryError異常。

2.4.3 方法區(qū)和運(yùn)行時(shí)常量池溢出

由于運(yùn)行時(shí)常量池是方法區(qū)的一部分,所以這兩個(gè)區(qū)域的溢出測(cè)試可以放到一起進(jìn)行。

String::intern()是一個(gè)本地方法,它的作用是如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的字符串,則返回代表池中這個(gè)字符串的String對(duì)象的引用;否則,會(huì)將此String對(duì)象包含的字符串添加到常量池中,并且返回此String對(duì)象的引用。

JDK 6或更早之前的HotSpot虛擬機(jī)中,常量池都是分配在永久代中,我們可以通過(guò)-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可間接限制其中常量池的容量。

無(wú)論是在JDK 7中繼續(xù)使用-XX:MaxPermSize參數(shù)或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize參數(shù)把方法區(qū)容量同樣限制在6MB,也都不會(huì)重現(xiàn)JDK 6中的溢出異常,循環(huán)將一直進(jìn)行下去,永不停歇。

在JDK 8以后,永久代便完全退出了歷史舞臺(tái),元空間作為其替代者登場(chǎng)。在默認(rèn)設(shè)置下,前面列舉的那些正常的動(dòng)態(tài)創(chuàng)建新類(lèi)型的測(cè)試用例已經(jīng)很難再迫使虛擬機(jī)產(chǎn)生方法區(qū)的溢出異常了。不過(guò)為了讓使用者有預(yù)防實(shí)際應(yīng)用里出現(xiàn)類(lèi)似于代碼清單2-9那樣的破壞性的操作,HotSpot還是提供了一些參數(shù)作為元空間的防御措施,主要包括:

·-XX:MaxMetaspaceSize:設(shè)置元空間最大值,默認(rèn)是-1,即不限制,或者說(shuō)只受限于本地內(nèi)存大小。

·-XX:MetaspaceSize:指定元空間的初始空間大小,以字節(jié)為單位,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類(lèi)型卸載,同時(shí)收集器會(huì)對(duì)該值進(jìn)行調(diào)整:如果釋放了大量的空間,就適當(dāng)降低該值;如果釋放了很少的空間,那么在不超過(guò)-XX:MaxMetaspaceSize(如果設(shè)置了的話(huà))的情況下,適當(dāng)提高該值。

·-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空間剩余容量的百分比,可減少因?yàn)樵臻g不足導(dǎo)致的垃圾收集的頻率。類(lèi)似的還有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空間剩余容量的百分比。

2.4.4 本機(jī)直接內(nèi)存溢出

直接內(nèi)存(Direct Memory)的容量大小可通過(guò)-XX:MaxDirectMemorySize參數(shù)來(lái)指定,如果不去指定,則默認(rèn)與Java堆最大值(由-Xmx指定)一致。

第3章 垃圾收集器與內(nèi)存分配策略

3.1 概述

程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧3個(gè)區(qū)域線(xiàn)程私有,隨線(xiàn)程生死。棧中的棧幀內(nèi)存可說(shuō)是在編譯時(shí)便確定的。幾個(gè)區(qū)域的內(nèi)存分配與回收都具備有確定性。

Java Heap與方法區(qū)的分配是在運(yùn)行時(shí)不斷變化的,分配與回收是動(dòng)態(tài)的,因而需要垃圾收集器。

3.2 對(duì)象已死?

在堆里面存放著Java世界中幾乎所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事情就是要確定這些對(duì)象之中哪些還“存活”著,哪些已經(jīng)“死去”(“死去”即不可能再被任何途徑使用的對(duì)象)了。

3.2.1 引用計(jì)數(shù)算法

引用計(jì)數(shù)法:在對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加一;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減一;任何時(shí)刻計(jì)數(shù)器為零的對(duì)象就是不可能再被使用的。

引用計(jì)數(shù)算法雖然占用了一些額外的內(nèi)存空間來(lái)進(jìn)行計(jì)數(shù),但它的原理簡(jiǎn)單,判定效率也很高,在大多數(shù)情況下它都是一個(gè)不錯(cuò)的算法。但是單純的引用計(jì)數(shù)就很難解決對(duì)象之間相互循環(huán)引用的問(wèn)題。

3.2.2 可達(dá)性分析算法

可達(dá)性分析算法:基本思路就是通過(guò)一系列稱(chēng)為“GC Roots”的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開(kāi)始,根據(jù)引用關(guān)系向下搜索,搜索過(guò)程所走過(guò)的路徑稱(chēng)為“引用鏈”(Reference Chain),如果某個(gè)對(duì)象到GCRoots間沒(méi)有任何引用鏈相連,或者用圖論的話(huà)來(lái)說(shuō)就是從GC Roots到這個(gè)對(duì)象不可達(dá)時(shí),則證明此對(duì)象是不可能再被使用的。


在Java技術(shù)體系里面,固定可作為GC Roots的對(duì)象包括以下幾種:

·在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象,譬如各個(gè)線(xiàn)程被調(diào)用的方法堆棧中使用到的參數(shù)、局部變量、臨時(shí)變量等。

·在方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象,譬如Java類(lèi)的引用類(lèi)型靜態(tài)變量。

·在方法區(qū)中常量引用的對(duì)象,譬如字符串常量池(String Table)里的引用。

·在本地方法棧中JNI(即通常所說(shuō)的Native方法)引用的對(duì)象。

·Java虛擬機(jī)內(nèi)部的引用,如基本數(shù)據(jù)類(lèi)型對(duì)應(yīng)的Class對(duì)象,一些常駐的異常對(duì)象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統(tǒng)類(lèi)加載器。

·所有被同步鎖(synchronized關(guān)鍵字)持有的對(duì)象。

·反映Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)、本地代碼緩存等。

除了這些固定的GC Roots集合以外,根據(jù)用戶(hù)所選用的垃圾收集器以及當(dāng)前回收的內(nèi)存區(qū)域不同,還可以有其他對(duì)象“臨時(shí)性”地加入,共同構(gòu)成完整GC Roots集合。

3.2.3 再談引用

在JDK 1.2版之后,Java將引用分為**強(qiáng)引用(Strongly Re-ference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(PhantomReference)**4種,這4種引用強(qiáng)度依次逐漸減弱。

·強(qiáng)引用是最傳統(tǒng)的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類(lèi)似“Object obj=new Object()”這種引用關(guān)系。無(wú)論任何情況下,只要強(qiáng)引用關(guān)系還存在,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。

·軟引用是用來(lái)描述一些還有用,但非必須的對(duì)象。只被軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前,會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒(méi)有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。在JDK 1.2版之后提供了SoftReference類(lèi)來(lái)實(shí)現(xiàn)軟引用。

·弱引用也是**用來(lái)描述那些非必須對(duì)象,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生為止。當(dāng)垃圾收集器開(kāi)始工作,無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。**在JDK 1.2版之后提供了WeakReference類(lèi)來(lái)實(shí)現(xiàn)弱引用。

·虛引用也稱(chēng)為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的只是為了能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。在JDK 1.2版之后提供了PhantomReference類(lèi)來(lái)實(shí)現(xiàn)虛引用。

3.2.4 生存還是死亡?

要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過(guò)程
如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒(méi)有與GC Roots相連接的引用鏈,那它將會(huì)被第一次標(biāo)記,隨后進(jìn)行一次篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法。假如對(duì)象沒(méi)有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過(guò),那么虛擬機(jī)將這兩種情況都視為“沒(méi)有必要執(zhí)行”。

如果這個(gè)對(duì)象被判定為確有必要執(zhí)行finalize()方法,那么該對(duì)象將會(huì)被放置在一個(gè)名為F-Queue的隊(duì)列之中,并在稍后由一條由虛擬機(jī)自動(dòng)建立的、低調(diào)度優(yōu)先級(jí)的Finalizer線(xiàn)程去執(zhí)行它們的finalize()方法。

這里所說(shuō)的“執(zhí)行”是指虛擬機(jī)會(huì)觸發(fā)這個(gè)方法開(kāi)始運(yùn)行,但并不承諾一定會(huì)等待它運(yùn)行結(jié)束。這樣做的原因是,如果某個(gè)對(duì)象的finalize()方法執(zhí)行緩慢,或者更極端地發(fā)生了死循環(huán),將很可能導(dǎo)致F-Queue隊(duì)列中的其他對(duì)象永久處于等待,甚至導(dǎo)致整個(gè)內(nèi)存回收子系統(tǒng)的崩潰。finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì)稍后收集器將對(duì)F-Queue中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記,如果對(duì)象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可,譬如把自己(this關(guān)鍵字)賦值給某個(gè)類(lèi)變量或者對(duì)象的成員變量,那在第二次標(biāo)記時(shí)它將被移出“即將回收”的集合;如果對(duì)象這時(shí)候還沒(méi)有逃脫,那基本上它就真的要被回收了。

任何一個(gè)對(duì)象的finalize()方法都只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,如果對(duì)象面臨下一次回收,它的finalize()方法不會(huì)被再次執(zhí)行,因此第二段代碼的自救行動(dòng)失敗了。

3.2.5 回收方法區(qū)

在Java堆中,尤其是在新生代中,對(duì)常規(guī)應(yīng)用進(jìn)行一次垃圾收集通常可以回收70%至99%的內(nèi)存空間,相比之下,方法區(qū)回收囿于苛刻的判定條件,其區(qū)域垃圾收集的回收成果往往遠(yuǎn)低于此。

方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類(lèi)型。

3.3 垃圾收集算法

從如何判定對(duì)象消亡的角度出發(fā),垃圾收集算法可以劃分為“引用計(jì)數(shù)式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類(lèi),這兩類(lèi)也常被稱(chēng)作“直接垃圾收集”和“間接垃圾收集”。

由于引用計(jì)數(shù)式垃圾收集算法在本書(shū)討論到的主流Java虛擬機(jī)中均未涉及,所以我們暫不把它作為正文主要內(nèi)容來(lái)講解,本節(jié)介紹的所有算法均屬于追蹤式垃圾收集的范疇。

3.3.1 分代收集理論

當(dāng)前商業(yè)虛擬機(jī)的垃圾收集器,大多數(shù)都遵循了“分代收集”(Generational Collection)的理論進(jìn)行設(shè)計(jì),分代收集名為理論,實(shí)質(zhì)是一套符合大多數(shù)程序運(yùn)行實(shí)際情況的經(jīng)驗(yàn)法則,它建立在兩個(gè)分代假說(shuō)之上:

1)弱分代假說(shuō)(Weak Generational Hypothesis):絕大多數(shù)對(duì)象都是朝生夕滅的。

2)強(qiáng)分代假說(shuō)(Strong Generational Hypothesis):熬過(guò)越多次垃圾收集過(guò)程的對(duì)象就越難以消亡。

這兩個(gè)分代假說(shuō)共同奠定了多款常用的垃圾收集器的一致的設(shè)計(jì)原則:收集器應(yīng)該將Java堆劃分出不同的區(qū)域,然后將回收對(duì)象依據(jù)其年齡(年齡即對(duì)象熬過(guò)垃圾收集過(guò)程的次數(shù))分配到不同的區(qū)域之中存儲(chǔ)。

在Java堆劃分出不同的區(qū)域之后,**垃圾收集器才可以每次只回收其中某一個(gè)或者某些部分的區(qū)域——因而才有了“Minor GC”“Major GC”“Full GC”這樣的回收類(lèi)型的劃分。**也才能夠針對(duì)不同的區(qū)域安排與里面存儲(chǔ)對(duì)象存亡特征相匹配的垃圾收集算法——因而發(fā)展出了“標(biāo)記-復(fù)制算法”“標(biāo)記-清除算法”“標(biāo)記-整理算法”等針對(duì)性的垃圾收集算法。

把分代收集理論具體放到現(xiàn)在的商用Java虛擬機(jī)里,設(shè)計(jì)者一般至少會(huì)把Java堆劃分為新生代(Young Generation)和老年代(Old Generation)兩個(gè)區(qū)域。

分代收集并非只是簡(jiǎn)單劃分一下內(nèi)存區(qū)域那么容易,它至少存在一個(gè)明顯的困難:對(duì)象不是孤立的,對(duì)象之間會(huì)存在跨代引用。

假如要現(xiàn)在進(jìn)行一次只局限于新生代區(qū)域內(nèi)的收集(Minor GC),但新生代中的對(duì)象是完全有可能被老年代所引用的,為了找出該區(qū)域中的存活對(duì)象,不得不在固定的GC Roots之外,再額外遍歷整個(gè)老年代中所有對(duì)象來(lái)確保可達(dá)性分析結(jié)果的正確性,反過(guò)來(lái)也是一樣[插圖]。遍歷整個(gè)老年代所有對(duì)象的方案雖然理論上可行,但無(wú)疑會(huì)為內(nèi)存回收帶來(lái)很大的性能負(fù)擔(dān)。為了解決這個(gè)問(wèn)題,就需要對(duì)分代收集理論添加第三條經(jīng)驗(yàn)法則:

3)跨代引用假說(shuō)(Intergenerational Reference Hypothesis):跨代引用相對(duì)于同代引用來(lái)說(shuō)僅占極少數(shù)。

這其實(shí)是可根據(jù)前兩條假說(shuō)邏輯推理得出的隱含推論:存在互相引用關(guān)系的兩個(gè)對(duì)象,是應(yīng)該傾向于同時(shí)生存或者同時(shí)消亡的。

依據(jù)這條假說(shuō),我們就不應(yīng)再為了少量的跨代引用去掃描整個(gè)老年代,也不必浪費(fèi)空間專(zhuān)門(mén)記錄每一個(gè)對(duì)象是否存在及存在哪些跨代引用,只需在新生代上建立一個(gè)全局的數(shù)據(jù)結(jié)構(gòu)(該結(jié)構(gòu)被稱(chēng)為“記憶集”,Remembered Set),這個(gè)結(jié)構(gòu)把老年代劃分成若干小塊,標(biāo)識(shí)出老年代的哪一塊內(nèi)存會(huì)存在跨代引用。此后當(dāng)發(fā)生Minor GC時(shí),只有包含了跨代引用的小塊內(nèi)存里的對(duì)象才會(huì)被加入到GC Roots進(jìn)行掃描。雖然這種方法需要在對(duì)象改變引用關(guān)系(如將自己或者某個(gè)屬性賦值)時(shí)維護(hù)記錄數(shù)據(jù)的正確性,會(huì)增加一些運(yùn)行時(shí)的開(kāi)銷(xiāo),但比起收集時(shí)掃描整個(gè)老年代來(lái)說(shuō)仍然是劃算的。

為避免讀者產(chǎn)生混淆,在這里統(tǒng)一定義:

·部分收集(Partial GC):指目標(biāo)不是完整收集整個(gè)Java堆的垃圾收集,其中又分為:

新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集。

老年代收集(Major GC/Old GC):指目標(biāo)只是老年代的垃圾收集。目前只有CMS收集器會(huì)有單獨(dú)收集老年代的行為。另外請(qǐng)注意“Major GC”這個(gè)說(shuō)法現(xiàn)在有點(diǎn)混淆,在不同資料上常有不同所指,讀者需按上下文區(qū)分到底是指老年代的收集還是整堆收集。

■混合收集(Mixed GC):指目標(biāo)是收集整個(gè)新生代以及部分老年代的垃圾收集。目前只有G1收集器會(huì)有這種行為。

·整堆收集(Full GC):收集整個(gè)Java堆和方法區(qū)的垃圾收集。

3.3.2 標(biāo)記-清除算法

該算法分為“標(biāo)記”和“清除”兩個(gè)階段:

首先標(biāo)記出所有需要回收的對(duì)象,標(biāo)記過(guò)程就是對(duì)象是否屬于垃圾的判定過(guò)程;在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,也可以反過(guò)來(lái),標(biāo)記存活的對(duì)象,統(tǒng)一回收所有未被標(biāo)記的對(duì)象。

該算法主要缺點(diǎn)有兩個(gè):

第一個(gè)是執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對(duì)象,而且其中大部分是需要被回收的,這時(shí)必須進(jìn)行大量標(biāo)記和清除的動(dòng)作,導(dǎo)致標(biāo)記和清除兩個(gè)過(guò)程的執(zhí)行效率都隨對(duì)象數(shù)量增長(zhǎng)而降低;

第二個(gè)是內(nèi)存空間的碎片化問(wèn)題,標(biāo)記、清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致當(dāng)以后在程序運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí)無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。

標(biāo)記-清除算法的執(zhí)行過(guò)程如圖3-2所示。

3.3.3 標(biāo)記-復(fù)制算法

標(biāo)記-復(fù)制算法常被簡(jiǎn)稱(chēng)為復(fù)制算法。

為了解決標(biāo)記-清除算法面對(duì)大量可回收對(duì)象時(shí)執(zhí)行效率低的問(wèn)題,1969年Fenichel提出了一種稱(chēng)為“半?yún)^(qū)復(fù)制”(Semispace Copying)的垃圾收集算法,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。如果內(nèi)存中多數(shù)對(duì)象都是存活的,這種算法將會(huì)產(chǎn)生大量的內(nèi)存間復(fù)制的開(kāi)銷(xiāo),但對(duì)于多數(shù)對(duì)象都是可回收的情況,算法需要復(fù)制的就是占少數(shù)的存活對(duì)象,而且每次都是針對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,分配內(nèi)存時(shí)也就不用考慮有空間碎片的復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配即可。

這樣實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效,不過(guò)其缺陷也顯而易見(jiàn),這種復(fù)制回收算法的代價(jià)是將可用內(nèi)存縮小為了原來(lái)的一半,空間浪費(fèi)未免太多了一點(diǎn)。

**現(xiàn)在的商用Java虛擬機(jī)大多都優(yōu)先采用了這種收集算法去回收新生代。**IMB研究新生代中的對(duì)象有98%熬不過(guò)第一輪收集。因此并不需要按照1∶1的比例來(lái)劃分新生代的內(nèi)存空間。

在1989年,Andrew Appel針對(duì)具備“朝生夕滅”特點(diǎn)的對(duì)象,提出了一種更優(yōu)化的半?yún)^(qū)復(fù)制分代策略,現(xiàn)在稱(chēng)為“Appel式回收”。HotSpot虛擬機(jī)的Serial、ParNew等新生代收集器均采用了這種策略來(lái)設(shè)計(jì)新生代的內(nèi)存布局。

Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內(nèi)存只使用Eden和其中一塊Survivor。發(fā)生垃圾搜集時(shí),將Eden和Survivor中仍然存活的對(duì)象一次性復(fù)制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過(guò)的那塊Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(Eden的80%加上一個(gè)Survivor的10%),只有一個(gè)Survivor空間,即10%的新生代是會(huì)被“浪費(fèi)”的。

當(dāng)然,98%的對(duì)象可被回收僅僅是“普通場(chǎng)景”下測(cè)得的數(shù)據(jù),任何人都沒(méi)有辦法百分百保證每次回收都只有不多于10%的對(duì)象存活,因此Appel式回收還有一個(gè)充當(dāng)罕見(jiàn)情況的“逃生門(mén)”的安全設(shè)計(jì),當(dāng)Survivor空間不足以容納一次Minor GC之后存活的對(duì)象時(shí),就需要依賴(lài)其他內(nèi)存區(qū)域(實(shí)際上大多就是老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。如果另外一塊Survivor空間沒(méi)有足夠空間存放上一次新生代收集下來(lái)的存活對(duì)象,這些對(duì)象便將通過(guò)分配擔(dān)保機(jī)制直接進(jìn)入老年代,這對(duì)虛擬機(jī)來(lái)說(shuō)就是安全的。關(guān)于對(duì)新生代進(jìn)行分配擔(dān)保的內(nèi)容,在稍后的3.8.5節(jié)介紹垃圾收集器執(zhí)行規(guī)則時(shí)還會(huì)再進(jìn)行講解。

3.3.4 標(biāo)記-整理算法

標(biāo)記-復(fù)制算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會(huì)降低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。

針對(duì)老年代對(duì)象的存亡特征,1974年Edward Lueders提出了另外一種有針對(duì)性的“標(biāo)記-整理”(Mark-Compact)算法,其中的標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。

標(biāo)記-清除算法與標(biāo)記-整理算法的本質(zhì)差異在于前者是一種非移動(dòng)式的回收算法,而后者是移動(dòng)式的。

是否移動(dòng)回收后的存活對(duì)象是一項(xiàng)優(yōu)缺點(diǎn)并存的風(fēng)險(xiǎn)決策。

“Stop the world”:移動(dòng)存活對(duì)象,必須全程暫停用戶(hù)應(yīng)用程序才能進(jìn)行。

Mark-compact:吞吐量(用戶(hù)程序與收集器的效率總和)更高, 但內(nèi)存回收更復(fù)雜。
Mark-sweep:停頓時(shí)間更短,但是很多碎片空間

3.4 HotSpot的算法細(xì)節(jié)實(shí)現(xiàn)

3.4.1 根節(jié)點(diǎn)枚舉

所有收集器在根節(jié)點(diǎn)枚舉這一步驟時(shí)都是必須暫停用戶(hù)線(xiàn)程的,因此毫無(wú)疑問(wèn)根節(jié)點(diǎn)枚舉與之前提及的整理內(nèi)存碎片一樣會(huì)面臨相似的“Stop The World”的困擾。

由于目前主流Java虛擬機(jī)使用的都是準(zhǔn)確式垃圾收集,所以當(dāng)用戶(hù)線(xiàn)程停頓下來(lái)之后,其實(shí)并不需要一個(gè)不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機(jī)應(yīng)當(dāng)是有辦法直接得到哪些地方存放著對(duì)象引用的。在HotSpot的解決方案里,是使用一組稱(chēng)為OopMap的數(shù)據(jù)結(jié)構(gòu)來(lái)達(dá)到這個(gè)目的。一旦類(lèi)加載動(dòng)作完成的時(shí)候,HotSpot就會(huì)把對(duì)象內(nèi)什么偏移量上是什么類(lèi)型的數(shù)據(jù)計(jì)算出來(lái),在即時(shí)編譯(見(jiàn)第11章)過(guò)程中,也會(huì)在特定的位置記錄下棧里和寄存器里哪些位置是引用。這樣收集器在掃描時(shí)就可以直接得知這些信息了,并不需要真正一個(gè)不漏地從方法區(qū)等GC Roots開(kāi)始查找。

3.4.2 安全點(diǎn)

在OopMap的協(xié)助下,HotSpot可以快速準(zhǔn)確地完成GC Roots枚舉,但一個(gè)很現(xiàn)實(shí)的問(wèn)題隨之而來(lái):可能導(dǎo)致引用關(guān)系變化,或者說(shuō)導(dǎo)致OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對(duì)應(yīng)的OopMap,那將會(huì)需要大量的額外存儲(chǔ)空間,這樣垃圾收集伴隨而來(lái)的空間成本就會(huì)變得無(wú)法忍受的高昂。

實(shí)際上HotSpot也的確沒(méi)有為每條指令都生成OopMap,只是在“特定的位置”記錄了這些信息,這些位置被稱(chēng)為安全點(diǎn)(Safepoint)

有了安全點(diǎn)的設(shè)定,也就決定了用戶(hù)程序執(zhí)行時(shí)并非在代碼指令流的任意位置都能夠停頓下來(lái)開(kāi)始垃圾收集,而是強(qiáng)制要求必須執(zhí)行到達(dá)安全點(diǎn)后才能夠暫停。因此,安全點(diǎn)的選定既不能太少以至于讓收集器等待時(shí)間過(guò)長(zhǎng),也不能太過(guò)頻繁以至于過(guò)分增大運(yùn)行時(shí)的內(nèi)存負(fù)荷。安全點(diǎn)位置的選取基本上是以“是否具有讓程序長(zhǎng)時(shí)間執(zhí)行的特征”為標(biāo)準(zhǔn)進(jìn)行選定的,因?yàn)槊織l指令執(zhí)行的時(shí)間都非常短暫,程序不太可能因?yàn)橹噶盍鏖L(zhǎng)度太長(zhǎng)這樣的原因而長(zhǎng)時(shí)間執(zhí)行,“長(zhǎng)時(shí)間執(zhí)行”的最明顯特征就是指令序列的復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等都屬于指令序列復(fù)用,所以只有具有這些功能的指令才會(huì)產(chǎn)生安全點(diǎn)。

對(duì)于安全點(diǎn),另外一個(gè)需要考慮的問(wèn)題是,如何在垃圾收集發(fā)生時(shí)讓所有線(xiàn)程(這里其實(shí)不包括執(zhí)行JNI調(diào)用的線(xiàn)程)都跑到最近的安全點(diǎn),然后停頓下來(lái)。這里有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動(dòng)式中斷(Voluntary Suspension)

搶先式中斷不需要線(xiàn)程的執(zhí)行代碼主動(dòng)去配合,在垃圾收集發(fā)生時(shí),系統(tǒng)首先把所有用戶(hù)線(xiàn)程全部中斷,如果發(fā)現(xiàn)有用戶(hù)線(xiàn)程中斷的地方不在安全點(diǎn)上,就恢復(fù)這條線(xiàn)程執(zhí)行,讓它一會(huì)再重新中斷,直到跑到安全點(diǎn)上。現(xiàn)在幾乎沒(méi)有虛擬機(jī)實(shí)現(xiàn)采用搶先式中斷來(lái)暫停線(xiàn)程響應(yīng)GC事件。

主動(dòng)式中斷是**當(dāng)垃圾收集需要中斷線(xiàn)程的時(shí)候,不直接對(duì)線(xiàn)程操作,僅僅簡(jiǎn)單地設(shè)置一個(gè)標(biāo)志位,各個(gè)線(xiàn)程執(zhí)行過(guò)程時(shí)會(huì)不停地主動(dòng)去輪詢(xún)這個(gè)標(biāo)志,一旦發(fā)現(xiàn)中斷標(biāo)志為真時(shí)就自己在最近的安全點(diǎn)上主動(dòng)中斷掛起。**輪詢(xún)標(biāo)志的地方和安全點(diǎn)是重合的,另外還要加上所有創(chuàng)建對(duì)象和其他需要在Java堆上分配內(nèi)存的地方,這是為了檢查是否即將要發(fā)生垃圾收集,避免沒(méi)有足夠內(nèi)存分配新對(duì)象。

由于輪詢(xún)操作在代碼中會(huì)頻繁出現(xiàn),這要求它必須足夠高效。HotSpot使用內(nèi)存保護(hù)陷阱的方式,把輪詢(xún)操作精簡(jiǎn)至只有一條匯編指令的程度。

3.4.3 安全區(qū)域

程序“不執(zhí)行”的時(shí)候呢?所謂的程序不執(zhí)行就是沒(méi)有分配處理器時(shí)間,典型的場(chǎng)景便是用戶(hù)線(xiàn)程處于Sleep狀態(tài)或者Blocked狀態(tài),這時(shí)候線(xiàn)程無(wú)法響應(yīng)虛擬機(jī)的中斷請(qǐng)求,不能再走到安全的地方去中斷掛起自己,虛擬機(jī)也顯然不可能持續(xù)等待線(xiàn)程重新被激活分配處理器時(shí)間。對(duì)于這種情況,就必須引入**安全區(qū)域(Safe Region)**來(lái)解決。

**安全區(qū)域是指能夠確保在某一段代碼片段之中,引用關(guān)系不會(huì)發(fā)生變化,因此,在這個(gè)區(qū)域中任意地方開(kāi)始垃圾收集都是安全的。**我們也可以把安全區(qū)域看作被擴(kuò)展拉伸了的安全點(diǎn)。

當(dāng)用戶(hù)線(xiàn)程執(zhí)行到安全區(qū)域里面的代碼時(shí),首先會(huì)標(biāo)識(shí)自己已經(jīng)進(jìn)入了安全區(qū)域,那樣當(dāng)這段時(shí)間里虛擬機(jī)要發(fā)起垃圾收集時(shí)就不必去管這些已聲明自己在安全區(qū)域內(nèi)的線(xiàn)程了。當(dāng)線(xiàn)程要離開(kāi)安全區(qū)域時(shí),它要檢查虛擬機(jī)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者垃圾收集過(guò)程中其他需要暫停用戶(hù)線(xiàn)程的階段),如果完成了,那線(xiàn)程就當(dāng)作沒(méi)事發(fā)生過(guò),繼續(xù)執(zhí)行;否則它就必須一直等待,直到收到可以離開(kāi)安全區(qū)域的信號(hào)為止。

3.4.4 記憶集與卡表

講解分代收集理論的時(shí)候,提到了為解決對(duì)象跨代引用所帶來(lái)的問(wèn)題,垃圾收集器在新生代中建立了名為記憶集(Remembered Set)的數(shù)據(jù)結(jié)構(gòu),用以避免把整個(gè)老年代加進(jìn)GC Roots掃描范圍。

事實(shí)上并不只是新生代、老年代之間才有跨代引用的問(wèn)題,所有涉及部分區(qū)域收集(Partial GC)行為的垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都會(huì)面臨相同的問(wèn)題,因此我們有必要進(jìn)一步理清記憶集的原理和實(shí)現(xiàn)方式。

**記憶集是一種用于記錄從非收集區(qū)域指向收集區(qū)域的指針集合的抽象數(shù)據(jù)結(jié)構(gòu)。**最簡(jiǎn)單的實(shí)現(xiàn)可以用非收集區(qū)域中所有含跨代引用的對(duì)象數(shù)組來(lái)實(shí)現(xiàn)這個(gè)數(shù)據(jù)結(jié)構(gòu)

這種記錄全部含跨代引用對(duì)象的實(shí)現(xiàn)方案,無(wú)論是空間占用還是維護(hù)成本都相當(dāng)高昂。而在垃圾收集的場(chǎng)景中,收集器只需要通過(guò)記憶集判斷出某一塊非收集區(qū)域是否存在有指向了收集區(qū)域的指針就可以了,并不需要了解這些跨代指針的全部細(xì)節(jié)

那設(shè)計(jì)者在實(shí)現(xiàn)記憶集的時(shí)候,便可以選擇更為粗獷的記錄粒度來(lái)節(jié)省記憶集的存儲(chǔ)和維護(hù)成本,下面列舉了一些可供選擇的記錄精度:

·字長(zhǎng)精度:每個(gè)記錄精確到一個(gè)機(jī)器字長(zhǎng)(就是處理器的尋址位數(shù),如常見(jiàn)的32位或64位,這個(gè)精度決定了機(jī)器訪問(wèn)物理內(nèi)存地址的指針長(zhǎng)度),該字包含跨代指針。

·對(duì)象精度:每個(gè)記錄精確到一個(gè)對(duì)象,該對(duì)象里有字段含有跨代指針。

·卡精度:每個(gè)記錄精確到一塊內(nèi)存區(qū)域,該區(qū)域內(nèi)有對(duì)象含有跨代指針。

其中,**第三種“卡精度”所指的是用一種稱(chēng)為“卡表”(Card Table)的方式去實(shí)現(xiàn)記憶集。**前面定義中提到記憶集其實(shí)是一種“抽象”的數(shù)據(jù)結(jié)構(gòu),抽象的意思是只定義了記憶集的行為意圖,并沒(méi)有定義其行為的具體實(shí)現(xiàn)。卡表就是記憶集的一種具體實(shí)現(xiàn),它定義了記憶集的記錄精度、與堆內(nèi)存的映射關(guān)系等。關(guān)于卡表與記憶集的關(guān)系,讀者不妨按照J(rèn)ava語(yǔ)言中HashMap與Map的關(guān)系來(lái)類(lèi)比理解。

卡表最簡(jiǎn)單的形式可以只是一個(gè)字節(jié)數(shù)組

字節(jié)數(shù)組CARD_TABLE的每一個(gè)元素都對(duì)應(yīng)著其標(biāo)識(shí)的內(nèi)存區(qū)域中一塊特定大小的內(nèi)存塊,這個(gè)內(nèi)存塊被稱(chēng)作“卡頁(yè)”(Card Page)。

一個(gè)卡頁(yè)的內(nèi)存中通常包含不止一個(gè)對(duì)象,只要卡頁(yè)內(nèi)有一個(gè)(或更多)對(duì)象的字段存在著跨代指針,那就將對(duì)應(yīng)卡表的數(shù)組元素的值標(biāo)識(shí)為1,稱(chēng)為這個(gè)元素變臟(Dirty),沒(méi)有則標(biāo)識(shí)為0。在垃圾收集發(fā)生時(shí),只要篩選出卡表中變臟的元素,就能輕易得出哪些卡頁(yè)內(nèi)存塊中包含跨代指針,把它們加入GC Roots中一并掃描。

3.4.5 寫(xiě)屏障

我們已經(jīng)解決了如何使用記憶集來(lái)縮減GC Roots掃描范圍的問(wèn)題,但還沒(méi)有解決卡表元素如何維護(hù)的問(wèn)題,例如它們何時(shí)變臟、誰(shuí)來(lái)把它們變臟等。

卡表元素何時(shí)變臟的答案是很明確的——有其他分代區(qū)域中對(duì)象引用了本區(qū)域?qū)ο髸r(shí),其對(duì)應(yīng)的卡表元素就應(yīng)該變臟,變臟時(shí)間點(diǎn)原則上應(yīng)該發(fā)生在引用類(lèi)型字段賦值的那一刻。

但問(wèn)題是如何變臟,即如何在對(duì)象賦值的那一刻去更新維護(hù)卡表呢?假如是解釋執(zhí)行的字節(jié)碼,那相對(duì)好處理,虛擬機(jī)負(fù)責(zé)每條字節(jié)碼指令的執(zhí)行,有充分的介入空間;但在編譯執(zhí)行的場(chǎng)景中呢?經(jīng)過(guò)即時(shí)編譯后的代碼已經(jīng)是純粹的機(jī)器指令流了,這就必須找到一個(gè)在機(jī)器碼層面的手段,把維護(hù)卡表的動(dòng)作放到每一個(gè)賦值操作之中。

在HotSpot虛擬機(jī)里是通過(guò)寫(xiě)屏障(Write Barrier)技術(shù)維護(hù)卡表狀態(tài)的。先請(qǐng)讀者注意將這里提到的“寫(xiě)屏障”,以及后面在低延遲收集器中會(huì)提到的“讀屏障”與解決并發(fā)亂序執(zhí)行問(wèn)題中的“內(nèi)存屏障”區(qū)分開(kāi)來(lái),避免混淆。

寫(xiě)屏障可以看作在虛擬機(jī)層面對(duì)“引用類(lèi)型字段賦值”這個(gè)動(dòng)作的AOP切面,在引用對(duì)象賦值時(shí)會(huì)產(chǎn)生一個(gè)環(huán)形(Around)通知,供程序執(zhí)行額外的動(dòng)作,也就是說(shuō)賦值的前后都在寫(xiě)屏障的覆蓋范疇內(nèi)。在賦值前的部分的寫(xiě)屏障叫作寫(xiě)前屏障(Pre-Write Barrier),在賦值后的則叫作寫(xiě)后屏障(Post-Write Barrier)。HotSpot虛擬機(jī)的許多收集器中都有使用到寫(xiě)屏障,但直至G1收集器出現(xiàn)之前,其他收集器都只用到了寫(xiě)后屏障。

應(yīng)用寫(xiě)屏障后,虛擬機(jī)就會(huì)為所有賦值操作生成相應(yīng)的指令,一旦收集器在寫(xiě)屏障中增加了更新卡表操作,無(wú)論更新的是不是老年代對(duì)新生代對(duì)象的引用,每次只要對(duì)引用進(jìn)行更新,就會(huì)產(chǎn)生額外的開(kāi)銷(xiāo),不過(guò)這個(gè)開(kāi)銷(xiāo)與MinorGC時(shí)掃描整個(gè)老年代的代價(jià)相比還是低得多的。

除了寫(xiě)屏障的開(kāi)銷(xiāo)外,卡表在高并發(fā)場(chǎng)景下還面臨著“偽共享”(False Sharing)問(wèn)題。偽共享是處理并發(fā)底層細(xì)節(jié)時(shí)一種經(jīng)常需要考慮的問(wèn)題,現(xiàn)代中央處理器的緩存系統(tǒng)中是以緩存行(Cache Line)為單位存儲(chǔ)的,當(dāng)多線(xiàn)程修改互相獨(dú)立的變量時(shí),如果這些變量恰好共享同一個(gè)緩存行,就會(huì)彼此影響(寫(xiě)回、無(wú)效化或者同步)而導(dǎo)致性能降低,這就是偽共享問(wèn)題。

在JDK 7之后,HotSpot虛擬機(jī)增加了一個(gè)新的參數(shù)-XX:+UseCondCardMark,用來(lái)決定是否開(kāi)啟卡表更新的條件判斷。開(kāi)啟會(huì)增加一次額外判斷的開(kāi)銷(xiāo),但能夠避免偽共享問(wèn)題,兩者各有性能損耗,是否打開(kāi)要根據(jù)應(yīng)用實(shí)際運(yùn)行情況來(lái)進(jìn)行測(cè)試權(quán)衡。

3.4.6 并發(fā)的可達(dá)性分析

在3.2節(jié)中曾經(jīng)提到了當(dāng)前主流編程語(yǔ)言的垃圾收集器基本上都是依靠可達(dá)性分析算法來(lái)判定對(duì)象是否存活的,可達(dá)性分析算法理論上要求全過(guò)程都基于一個(gè)能保障一致性的快照中才能夠進(jìn)行分析,這意味著必須全程凍結(jié)用戶(hù)線(xiàn)程的運(yùn)行。在根節(jié)點(diǎn)枚舉(見(jiàn)3.4.1節(jié))這個(gè)步驟中,由于GC Roots相比起整個(gè)Java堆中全部的對(duì)象畢竟還算是極少數(shù),且在各種優(yōu)化技巧(如OopMap)的加持下,它帶來(lái)的停頓已經(jīng)是非常短暫且相對(duì)固定(不隨堆容量而增長(zhǎng))的了。

可從GC Roots再繼續(xù)往下遍歷對(duì)象圖,這一步驟的停頓時(shí)間就必定會(huì)與Java堆容量直接成正比例關(guān)系了:堆越大,存儲(chǔ)的對(duì)象越多,對(duì)象圖結(jié)構(gòu)越復(fù)雜,要標(biāo)記更多對(duì)象而產(chǎn)生的停頓時(shí)間自然就更長(zhǎng),這聽(tīng)起來(lái)是理所當(dāng)然的事情。

要知道包含“標(biāo)記”階段是所有追蹤式垃圾收集算法的共同特征,如果這個(gè)階段會(huì)隨著堆變大而等比例增加停頓時(shí)間,其影響就會(huì)波及幾乎所有的垃圾收集器,同理可知,如果能夠削減這部分停頓時(shí)間的話(huà),那收益也將會(huì)是系統(tǒng)性的。

想解決或者降低用戶(hù)線(xiàn)程的停頓,就要先搞清楚為什么必須在一個(gè)能保障一致性的快照上才能進(jìn)行對(duì)象圖的遍歷?為了能解釋清楚這個(gè)問(wèn)題,我們引入**三色標(biāo)記(Tri-color Marking)**作為工具來(lái)輔助推導(dǎo),把遍歷對(duì)象圖過(guò)程中遇到的對(duì)象,按照“是否訪問(wèn)過(guò)”這個(gè)條件標(biāo)記成以下三種顏色:

·白色:表示對(duì)象尚未被垃圾收集器訪問(wèn)過(guò)。顯然在可達(dá)性分析剛剛開(kāi)始的階段,所有的對(duì)象都是白色的,若在分析結(jié)束的階段,仍然是白色的對(duì)象,即代表不可達(dá)。

·黑色:表示對(duì)象已經(jīng)被垃圾收集器訪問(wèn)過(guò),且這個(gè)對(duì)象的所有引用都已經(jīng)掃描過(guò)。黑色的對(duì)象代表已經(jīng)掃描過(guò),它是安全存活的,如果有其他對(duì)象引用指向了黑色對(duì)象,無(wú)須重新掃描一遍。黑色對(duì)象不可能直接(不經(jīng)過(guò)灰色對(duì)象)指向某個(gè)白色對(duì)象。

·灰色:表示對(duì)象已經(jīng)被垃圾收集器訪問(wèn)過(guò),但這個(gè)對(duì)象上至少存在一個(gè)引用還沒(méi)有被掃描過(guò)。

Wilson于1994年在理論上證明了,當(dāng)且僅當(dāng)以下兩個(gè)條件同時(shí)滿(mǎn)足時(shí),會(huì)產(chǎn)生“對(duì)象消失”的問(wèn)題,即原本應(yīng)該是黑色的對(duì)象被誤標(biāo)為白色:·賦值器插入了一條或多條從黑色對(duì)象到白色對(duì)象的新引用;·賦值器刪除了全部從灰色對(duì)象到該白色對(duì)象的直接或間接引用。

因此,我們要解決并發(fā)掃描時(shí)的對(duì)象消失問(wèn)題,只需破壞這兩個(gè)條件的任意一個(gè)即可。由此分別產(chǎn)生了兩種解決方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)

增量更新要破壞的是第一個(gè)條件,當(dāng)黑色對(duì)象插入新的指向白色對(duì)象的引用關(guān)系時(shí),就將這個(gè)新插入的引用記錄下來(lái),等并發(fā)掃描結(jié)束之后,再將這些記錄過(guò)的引用關(guān)系中的黑色對(duì)象為根,重新掃描一次。這可以簡(jiǎn)化理解為,黑色對(duì)象一旦新插入了指向白色對(duì)象的引用之后,它就變回灰色對(duì)象了。

原始快照要破壞的是第二個(gè)條件,當(dāng)灰色對(duì)象要?jiǎng)h除指向白色對(duì)象的引用關(guān)系時(shí),就將這個(gè)要?jiǎng)h除的引用記錄下來(lái),在并發(fā)掃描結(jié)束之后,再將這些記錄過(guò)的引用關(guān)系中的灰色對(duì)象為根,重新掃描一次。這也可以簡(jiǎn)化理解為,無(wú)論引用關(guān)系刪除與否,都會(huì)按照剛剛開(kāi)始掃描那一刻的對(duì)象圖快照來(lái)進(jìn)行搜索。

以上無(wú)論是對(duì)引用關(guān)系記錄的插入還是刪除,虛擬機(jī)的記錄操作都是通過(guò)寫(xiě)屏障實(shí)現(xiàn)的。在HotSpot虛擬機(jī)中,增量更新和原始快照這兩種解決方案都有實(shí)際應(yīng)用,譬如,CMS是基于增量更新來(lái)做并發(fā)標(biāo)記的,G1、Shenandoah則是用原始快照來(lái)實(shí)現(xiàn)。

3.5 經(jīng)典垃圾收集器

如果說(shuō)收集算法是內(nèi)存回收的方法論,那垃圾收集器就是內(nèi)存回收的實(shí)踐者。

圖3-6展示了七種作用于不同分代的收集器,如果兩個(gè)收集器之間存在連線(xiàn),就說(shuō)明它們可以搭配使用,圖中收集器所處的區(qū)域,則表示它是屬于新生代收集器抑或是老年代收集器。

3.5.1 Serial收集器

Serial收集器是一個(gè)單線(xiàn)程工作的收集器,它進(jìn)行垃圾收集時(shí),必須暫停其他所有工作線(xiàn)程,直到它收集結(jié)束。

“Stop The World”這個(gè)詞語(yǔ)也許聽(tīng)起來(lái)很酷,但這項(xiàng)工作是由虛擬機(jī)在后臺(tái)自動(dòng)發(fā)起和自動(dòng)完成的,在用戶(hù)不可知、不可控的情況下把用戶(hù)的正常工作的線(xiàn)程全部停掉,這對(duì)很多應(yīng)用來(lái)說(shuō)都是不能接受的。圖3-7示意了Serial/Serial Old收集器的運(yùn)行過(guò)程。

迄今為止,它依然是HotSpot虛擬機(jī)運(yùn)行在客戶(hù)端模式下的默認(rèn)新生代收集器, 是所有收集器中額外內(nèi)存消耗最少的

3.5.2 ParNew收集器

ParNew收集器實(shí)質(zhì)上是Serial收集器的多線(xiàn)程并行版本,除了同時(shí)使用多條線(xiàn)程進(jìn)行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對(duì)象分配規(guī)則、回收策略等都與Serial收集器完全一致,在實(shí)現(xiàn)上這兩種收集器也共用了相當(dāng)多的代碼。ParNew收集器的工作過(guò)程如圖3-8所示。

ParNew收集器除了支持多線(xiàn)程并行收集之外,它也是不少運(yùn)行在服務(wù)端模式下的HotSpot虛擬機(jī),尤其是JDK 7之前的遺留系統(tǒng)中首選的新生代收集器,其中有一個(gè)與功能、性能無(wú)關(guān)但其實(shí)很重要的原因是:除了Serial收集器外,目前只有它與能與CMS收集器配合工作。

3.5.3 Parallel Scavenge收集器

Parallel Scavenge收集器的特點(diǎn)是它的關(guān)注點(diǎn)與其他收集器不同,CMS等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶(hù)線(xiàn)程的停頓時(shí)間,而Parallel Scavenge收集器的目標(biāo)則是達(dá)到一個(gè)可控制的吞吐量(Throughput)。

所謂吞吐量就是處理器用于運(yùn)行用戶(hù)代碼的時(shí)間與處理器總消耗時(shí)間的比值,即:

Parallel Scavenge收集器提供了兩個(gè)參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時(shí)間的-XX:MaxGCPauseMillis參數(shù)以及直接設(shè)置吞吐量大小的-XX:GCTimeRatio參數(shù)。

-XX:MaxGCPauseMillis參數(shù)允許的值是一個(gè)大于0的毫秒數(shù),收集器將盡力保證內(nèi)存回收花費(fèi)的時(shí)間不超過(guò)用戶(hù)設(shè)定值。不過(guò)大家不要異想天開(kāi)地認(rèn)為如果把這個(gè)參數(shù)的值設(shè)置得更小一點(diǎn)就能使得系統(tǒng)的垃圾收集速度變得更快,垃圾收集停頓時(shí)間縮短是以犧牲吞吐量和新生代空間為代價(jià)換取的:系統(tǒng)把新生代調(diào)得小一些,收集300MB新生代肯定比收集500MB快,但這也直接導(dǎo)致垃圾收集發(fā)生得更頻繁,原來(lái)10秒收集一次、每次停頓100毫秒,現(xiàn)在變成5秒收集一次、每次停頓70毫秒。停頓時(shí)間的確在下降,但吞吐量也降下來(lái)了。

-XX:GCTimeRatio參數(shù)的值則應(yīng)當(dāng)是一個(gè)大于0小于100的整數(shù),也就是垃圾收集時(shí)間占總時(shí)間的比率,相當(dāng)于吞吐量的倒數(shù)。譬如把此參數(shù)設(shè)置為19,那允許的最大垃圾收集時(shí)間就占總時(shí)間的5%(即1/(1+19)),默認(rèn)值為99,即允許最大1%(即1/(1+99))的垃圾收集時(shí)間。

Parallel Scavenge收集器還有一個(gè)參數(shù)-XX:+UseAdaptiveSizePolicy值得我們關(guān)注。這是一個(gè)開(kāi)關(guān)參數(shù),當(dāng)這個(gè)參數(shù)被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden與Survivor區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對(duì)象大小(-XX:PretenureSizeThreshold)等細(xì)節(jié)參數(shù)了,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或者最大的吞吐量。這種調(diào)節(jié)方式稱(chēng)為垃圾收集的自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics)。

使用Parallel Scavenge收集器配合自適應(yīng)調(diào)節(jié)策略,把內(nèi)存管理的調(diào)優(yōu)任務(wù)交給虛擬機(jī)去完成也許是一個(gè)很不錯(cuò)的選擇。只需要把基本的內(nèi)存數(shù)據(jù)設(shè)置好(如-Xmx設(shè)置最大堆),然后使用-XX:MaxGCPauseMillis參數(shù)(更關(guān)注最大停頓時(shí)間)或-XX:GCTimeRatio(更關(guān)注吞吐量)參數(shù)給虛擬機(jī)設(shè)立一個(gè)優(yōu)化目標(biāo),那具體細(xì)節(jié)參數(shù)的調(diào)節(jié)工作就由虛擬機(jī)完成了。

自適應(yīng)調(diào)節(jié)策略也是Parallel Scavenge收集器區(qū)別于ParNew收集器的一個(gè)重要特性。

3.5.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個(gè)單線(xiàn)程收集器,使用標(biāo)記-整理算法

這個(gè)收集器的主要意義也是供客戶(hù)端模式下的HotSpot虛擬機(jī)使用。

如果在服務(wù)端模式下,它也可能有兩種用途:一種是在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用,另外一種就是作為CMS收集器發(fā)生失敗時(shí)的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時(shí)使用。

Serial Old收集器的工作過(guò)程如圖3-9所示。

3.5.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線(xiàn)程并發(fā)收集,基于標(biāo)記-整理算法實(shí)現(xiàn)。

在注重吞吐量或者處理器資源比較稀缺的場(chǎng)合,可優(yōu)先考慮Parallel Scavenge+Parallel Old這個(gè)組合。

Parallel Old收集器的工作過(guò)程如圖3-10所示。

3.5.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)網(wǎng)站或者基于瀏覽器的B/S系統(tǒng)的服務(wù)端上,這類(lèi)應(yīng)用通常都會(huì)較為關(guān)注服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間盡可能短,以給用戶(hù)帶來(lái)良好的交互體驗(yàn)。CMS收集器就非常符合這類(lèi)應(yīng)用的需求。

從CMS收集器是基于標(biāo)記-清除算法實(shí)現(xiàn)的,整個(gè)過(guò)程分為四個(gè)步驟

1)初始標(biāo)記(CMS initial mark)

該階段僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快。

2)并發(fā)標(biāo)記(CMS concurrent mark)

該階段從GC Roots的直接關(guān)聯(lián)對(duì)象開(kāi)始遍歷整個(gè)對(duì)象圖的過(guò)程,這個(gè)過(guò)程耗時(shí)較長(zhǎng)但是不需要停頓用戶(hù)線(xiàn)程,可以與垃圾收集線(xiàn)程一起并發(fā)運(yùn)行。

3)重新標(biāo)記(CMS remark)

該階段則是為了修正并發(fā)標(biāo)記期間,因用戶(hù)程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄(詳見(jiàn)3.4.6節(jié)中關(guān)于增量更新的講解),這個(gè)階段的停頓時(shí)間通常會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但也遠(yuǎn)比并發(fā)標(biāo)記階段的時(shí)間短。

4)并發(fā)清除(CMS concurrent sweep)

該階段清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對(duì)象,由于不需要移動(dòng)存活對(duì)象,所以該階段也可以與用戶(hù)線(xiàn)程同時(shí)并發(fā)的。

其中,初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要“Stop The World”。

由于在整個(gè)過(guò)程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除階段中,垃圾收集器線(xiàn)程都可以與用戶(hù)線(xiàn)程一起工作,所以從總體上來(lái)說(shuō),CMS收集器的內(nèi)存回收過(guò)程是與用戶(hù)線(xiàn)程一起并發(fā)執(zhí)行的。

通過(guò)圖3-11可以比較清楚地看到CMS收集器的運(yùn)作步驟中并發(fā)和需要停頓的階段。

CMS最主要的優(yōu)點(diǎn)在名字上已經(jīng)體現(xiàn)出來(lái):并發(fā)收集、低停頓

CMS收集器至少有以下三個(gè)明顯的缺點(diǎn):首先,CMS收集器對(duì)處理器資源非常敏感。然后,由于CMS收集器無(wú)法處理“浮動(dòng)垃圾”(Floating Garbage),有可能出現(xiàn)“Con-current ModeFailure”失敗進(jìn)而導(dǎo)致另一次完全“Stop The World”的Full GC的產(chǎn)生。最后一個(gè)缺點(diǎn),在本節(jié)的開(kāi)頭曾提到,CMS是一款基于“標(biāo)記-清除”算法實(shí)現(xiàn)的收集器,如果讀者對(duì)前面這部分介紹還有印象的話(huà),就可能想到這意味著收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生。空間碎片過(guò)多時(shí),將會(huì)給大對(duì)象分配帶來(lái)很大麻煩,往往會(huì)出現(xiàn)老年代還有很多剩余空間,但就是無(wú)法找到足夠大的連續(xù)空間來(lái)分配當(dāng)前對(duì)象,而不得不提前觸發(fā)一次Full GC的情況。為了解決這個(gè)問(wèn)題,CMS收集器提供了一個(gè)-XX:+UseCMS-CompactAtFullCollection開(kāi)關(guān)參數(shù)(默認(rèn)是開(kāi)啟的,此參數(shù)從JDK 9開(kāi)始廢棄),用于在CMS收集器不得不進(jìn)行Full GC時(shí)開(kāi)啟內(nèi)存碎片的合并整理過(guò)程,由于這個(gè)內(nèi)存整理必須移動(dòng)存活對(duì)象,(在Shenandoah和ZGC出現(xiàn)前)是無(wú)法并發(fā)的。這樣空間碎片問(wèn)題是解決了,但停頓時(shí)間又會(huì)變長(zhǎng),因此虛擬機(jī)設(shè)計(jì)者們還提供了另外一個(gè)參數(shù)-XX:CMSFullGCsBefore-Compaction(此參數(shù)從JDK 9開(kāi)始廢棄),這個(gè)參數(shù)的作用是要求CMS收集器在執(zhí)行過(guò)若干次(數(shù)量由參數(shù)值決定)不整理空間的Full GC之后,下一次進(jìn)入Full GC前會(huì)先進(jìn)行碎片整理(默認(rèn)值為0,表示每次進(jìn)入Full GC時(shí)都進(jìn)行碎片整理)。

3.5.7 Garbage First收集器

G1是一款主要面向服務(wù)端應(yīng)用的垃圾收集器。

目標(biāo):在延遲可控的情況下獲得盡可能高的吞吐量
從整體上來(lái)說(shuō)是基于標(biāo)記-清除,但從局部上(兩個(gè)region之間)是基于復(fù)制算法

G1面向堆內(nèi)存任何部分來(lái)組成回收集(CollectionSet,一般簡(jiǎn)稱(chēng)CSet)進(jìn)行回收,衡量標(biāo)準(zhǔn)不再是它屬于哪個(gè)分代,而是哪塊內(nèi)存中存放的垃圾數(shù)量最多,回收收益最大,這就是G1收集器的Mixed GC模式。G1開(kāi)創(chuàng)的基于Region的堆內(nèi)存布局是它能夠?qū)崿F(xiàn)這個(gè)目標(biāo)的關(guān)鍵。

雖然G1也仍是遵循分代收集理論設(shè)計(jì)的,但G1不再堅(jiān)持固定大小以及固定數(shù)量的分代區(qū)域劃分,而是把連續(xù)的Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),每一個(gè)Region都可以根據(jù)需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠?qū)Π缪莶煌巧腞egion采用不同的策略去處理,這樣無(wú)論是新創(chuàng)建的對(duì)象還是已經(jīng)存活了一段時(shí)間、熬過(guò)多次收集的舊對(duì)象都能獲取很好的收集效果。

Region中還有一類(lèi)特殊的Humongous區(qū)域,專(zhuān)門(mén)用來(lái)存儲(chǔ)大對(duì)象。G1認(rèn)為只要大小超過(guò)了一個(gè)Region容量一半的對(duì)象即可判定為大對(duì)象。每個(gè)Region的大小可以通過(guò)參數(shù)-XX:G1HeapRegionSize設(shè)定,取值范圍為1MB~32MB,且應(yīng)為2的N次冪。而對(duì)于那些超過(guò)了整個(gè)Region容量的超級(jí)大對(duì)象,將會(huì)被存放在N個(gè)連續(xù)的Humongous Region之中,G1的大多數(shù)行為都把Humongous Region作為老年代的一部分來(lái)進(jìn)行看待,如圖3-12所示。

雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它們都是一系列區(qū)域(不需要連續(xù))的動(dòng)態(tài)集合。G1收集器之所以能建立可預(yù)測(cè)的停頓時(shí)間模型,是因?yàn)樗鼘egion作為單次回收的最小單元,即每次收集到的內(nèi)存空間都是Region大小的整數(shù)倍,這樣可以有計(jì)劃地避免在整個(gè)Java堆中進(jìn)行全區(qū)域的垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個(gè)Region里面的垃圾堆積的“價(jià)值”大小,價(jià)值即回收所獲得的空間大小以及回收所需時(shí)間的經(jīng)驗(yàn)值,然后在后臺(tái)維護(hù)一個(gè)優(yōu)先級(jí)列表,每次根據(jù)用戶(hù)設(shè)定允許的收集停頓時(shí)間(使用參數(shù)-XX:MaxGCPauseMillis指定,默認(rèn)值是200毫秒),優(yōu)先處理回收價(jià)值收益最大的那些Region,這也就是“Garbage First”名字的由來(lái)。這種使用Region劃分內(nèi)存空間,以及具有優(yōu)先級(jí)的區(qū)域回收方式,保證了G1收集器在有限的時(shí)間內(nèi)獲取盡可能高的收集效率。

G1收集器的運(yùn)作過(guò)程大致可劃分為以下四個(gè)步驟

·初始標(biāo)記(Initial Marking):僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,并且修改TAMS(Top at Mark Start)指針的值,讓下一階段用戶(hù)線(xiàn)程并發(fā)運(yùn)行時(shí),能正確地在可用的Region中分配新對(duì)象。這個(gè)階段需要停頓線(xiàn)程,但耗時(shí)很短,而且是借用進(jìn)行Minor GC的時(shí)候同步完成的,所以G1收集器在這個(gè)階段實(shí)際并沒(méi)有額外的停頓。

·并發(fā)標(biāo)記(Concurrent Marking):從GC Root開(kāi)始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,遞歸掃描整個(gè)堆里的對(duì)象圖,找出要回收的對(duì)象,這階段耗時(shí)較長(zhǎng),但可與用戶(hù)程序并發(fā)執(zhí)行。當(dāng)對(duì)象圖掃描完成以后,還要重新處理SATB(原始快照)記錄下的在并發(fā)時(shí)有引用變動(dòng)的對(duì)象。

·最終標(biāo)記(Final Marking):對(duì)用戶(hù)線(xiàn)程做另一個(gè)短暫的暫停,用于處理并發(fā)階段結(jié)束后仍遺留下來(lái)的最后那少量的SATB記錄。

·篩選回收(Live Data Counting and Evacuation):負(fù)責(zé)更新Region的統(tǒng)計(jì)數(shù)據(jù),對(duì)各個(gè)Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶(hù)所期望的停頓時(shí)間來(lái)制定回收計(jì)劃,可以自由選擇任意多個(gè)Region構(gòu)成回收集,然后把決定回收的那一部分Region的存活對(duì)象復(fù)制到空的Region中,再清理掉整個(gè)舊Region的全部空間。這里的操作涉及存活對(duì)象的移動(dòng),是必須暫停用戶(hù)線(xiàn)程,由多條收集器線(xiàn)程并行完成的。


從G1開(kāi)始,最先進(jìn)的垃圾收集器的設(shè)計(jì)導(dǎo)向?yàn)樽非竽軌驊?yīng)付應(yīng)用的內(nèi)存分配速率。

G1 VS CMS
相同:都關(guān)注停頓時(shí)間的控制
G1優(yōu)點(diǎn):可指定最大停頓時(shí)間;分region的內(nèi)存分布;按收益動(dòng)態(tài)確定回收;不會(huì)產(chǎn)生垃圾碎片,有利程序長(zhǎng)時(shí)間運(yùn)行
G1缺點(diǎn):垃圾收集產(chǎn)生的內(nèi)存占用(Footprint)和程序運(yùn)行時(shí)的額外執(zhí)行負(fù)載(Overload)都比CMS高
通常在小內(nèi)存應(yīng)用CMS優(yōu)于G1,而大內(nèi)存應(yīng)用G1更優(yōu)

3.6 低延遲垃圾收集器

衡量垃圾收集器的三項(xiàng)最重要的指標(biāo)是:內(nèi)存占用(Footprint)、吞吐量(Throughput)和延遲(Latency)。

淺色必須掛起用戶(hù)線(xiàn)程,深色用戶(hù)線(xiàn)程和收集器線(xiàn)程并發(fā)工作

3.6.1 Shenandoah收集器

Shenandoah作為第一款不由Oracle(包括以前的Sun)公司的虛擬機(jī)團(tuán)隊(duì)所領(lǐng)導(dǎo)開(kāi)發(fā)的HotSpot垃圾收集器,不可避免地會(huì)受到一些來(lái)自“官方”的排擠。

與G1類(lèi)似,基于region的堆內(nèi)存布局,用于存放大對(duì)象的Humongous Region,默認(rèn)的回收策略是優(yōu)先處理回收價(jià)值最大的region。
不同點(diǎn)三個(gè):G1的回收過(guò)程支持多線(xiàn)程并行,但不能和用戶(hù)線(xiàn)程并發(fā);Shenandoah默認(rèn)不使用分代收集;Shenandoah不用G1的記憶集,而是用connection matrix的全局?jǐn)?shù)據(jù)結(jié)構(gòu)來(lái)記錄跨region的引用關(guān)系。

Shenandoah收集器的工作過(guò)程大致可以劃分為以下九個(gè)階段:

·初始標(biāo)記(Initial Marking):與G1一樣,首先標(biāo)記與GC Roots直接關(guān)聯(lián)的對(duì)象,這個(gè)階段仍是“Stop TheWorld”的,但停頓時(shí)間與堆大小無(wú)關(guān),只與GC Roots的數(shù)量相關(guān)。

·并發(fā)標(biāo)記(Concurrent Marking):與G1一樣,遍歷對(duì)象圖,標(biāo)記出全部可達(dá)的對(duì)象,這個(gè)階段是與用戶(hù)線(xiàn)程一起并發(fā)的,時(shí)間長(zhǎng)短取決于堆中存活對(duì)象的數(shù)量以及對(duì)象圖的結(jié)構(gòu)復(fù)雜程度。

·最終標(biāo)記(Final Marking):與G1一樣,處理剩余的SATB掃描,并在這個(gè)階段統(tǒng)計(jì)出回收價(jià)值最高的Region,將這些Region構(gòu)成一組回收集(Collection Set)。最終標(biāo)記階段也會(huì)有一小段短暫的停頓。

·并發(fā)清理(Concurrent Cleanup):這個(gè)階段用于清理那些整個(gè)區(qū)域內(nèi)連一個(gè)存活對(duì)象都沒(méi)有找到的Region(這類(lèi)Region被稱(chēng)為Immediate Garbage Region)。

·并發(fā)回收(Concurrent Evacuation):并發(fā)回收階段是Shenandoah與之前HotSpot中其他收集器的核心差異。在這個(gè)階段,Shenandoah要把回收集里面的存活對(duì)象先復(fù)制一份到其他未被使用的Region之中。復(fù)制對(duì)象這件事情如果將用戶(hù)線(xiàn)程凍結(jié)起來(lái)再做那是相當(dāng)簡(jiǎn)單的,但如果兩者必須要同時(shí)并發(fā)進(jìn)行的話(huà),就變得復(fù)雜起來(lái)了。其困難點(diǎn)是在移動(dòng)對(duì)象的同時(shí),用戶(hù)線(xiàn)程仍然可能不停對(duì)被移動(dòng)的對(duì)象進(jìn)行讀寫(xiě)訪問(wèn),移動(dòng)對(duì)象是一次性的行為,但移動(dòng)之后整個(gè)內(nèi)存中所有指向該對(duì)象的引用都還是舊對(duì)象的地址,這是很難一瞬間全部改變過(guò)來(lái)的。對(duì)于并發(fā)回收階段遇到的這些困難,Shenandoah將會(huì)通過(guò)讀屏障和被稱(chēng)為“Brooks Pointers”的轉(zhuǎn)發(fā)指針來(lái)解決(講解完Shenandoah整個(gè)工作過(guò)程之后筆者還要再回頭介紹它)。并發(fā)回收階段運(yùn)行的時(shí)間長(zhǎng)短取決于回收集的大小。

·初始引用更新(Initial Update Reference):并發(fā)回收階段復(fù)制對(duì)象結(jié)束后,還需要把堆中所有指向舊對(duì)象的引用修正到復(fù)制后的新地址,這個(gè)操作稱(chēng)為引用更新。引用更新的初始化階段實(shí)際上并未做什么具體的處理,設(shè)立這個(gè)階段只是為了建立一個(gè)線(xiàn)程集合點(diǎn),確保所有并發(fā)回收階段中進(jìn)行的收集器線(xiàn)程都已完成分配給它們的對(duì)象移動(dòng)任務(wù)而已。初始引用更新時(shí)間很短,會(huì)產(chǎn)生一個(gè)非常短暫的停頓。

·并發(fā)引用更新(Concurrent Update Reference):真正開(kāi)始進(jìn)行引用更新操作,這個(gè)階段是與用戶(hù)線(xiàn)程一起并發(fā)的,時(shí)間長(zhǎng)短取決于內(nèi)存中涉及的引用數(shù)量的多少。并發(fā)引用更新與并發(fā)標(biāo)記不同,它不再需要沿著對(duì)象圖來(lái)搜索,只需要按照內(nèi)存物理地址的順序,線(xiàn)性地搜索出引用類(lèi)型,把舊值改為新值即可。

·最終引用更新(Final Update Reference):解決了堆中的引用更新后,還要修正存在于GC Roots中的引用。這個(gè)階段是Shenandoah的最后一次停頓,停頓時(shí)間只與GC Roots的數(shù)量相關(guān)。

·并發(fā)清理(Concurrent Cleanup):經(jīng)過(guò)并發(fā)回收和引用更新之后,整個(gè)回收集中所有的Region已再無(wú)存活對(duì)象,這些Region都變成Immediate Garbage Regions了,最后再調(diào)用一次并發(fā)清理過(guò)程來(lái)回收這些Region的內(nèi)存空間,供以后新對(duì)象分配使用。

以上對(duì)Shenandoah收集器這九個(gè)階段的工作過(guò)程的描述可能拆分得略為瑣碎,讀者只要抓住其中三個(gè)最重要的并發(fā)階段(并發(fā)標(biāo)記、并發(fā)回收、并發(fā)引用更新),就能比較容易理清Shenandoah是如何運(yùn)作的了。

3.6.2 ZGC收集器

·并發(fā)標(biāo)記(Concurrent Mark):與G1、Shenandoah一樣,并發(fā)標(biāo)記是遍歷對(duì)象圖做可達(dá)性分析的階段,前后也要經(jīng)過(guò)類(lèi)似于G1、Shenandoah的初始標(biāo)記、最終標(biāo)記(盡管ZGC中的名字不叫這些)的短暫停頓,而且這些停頓階段所做的事情在目標(biāo)上也是相類(lèi)似的。與G1、Shenandoah不同的是,ZGC的標(biāo)記是在指針上而不是在對(duì)象上進(jìn)行的,標(biāo)記階段會(huì)更新染色指針中的Marked 0、Marked 1標(biāo)志位。

·并發(fā)預(yù)備重分配(Concurrent Prepare for Relocate):這個(gè)階段需要根據(jù)特定的查詢(xún)條件統(tǒng)計(jì)得出本次收集過(guò)程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。重分配集與G1收集器的回收集(Collection Set)還是有區(qū)別的,ZGC劃分Region的目的并非為了像G1那樣做收益優(yōu)先的增量回收。相反,ZGC每次回收都會(huì)掃描所有的Region,用范圍更大的掃描成本換取省去G1中記憶集的維護(hù)成本。因此,ZGC的重分配集只是決定了里面的存活對(duì)象會(huì)被重新復(fù)制到其他的Region中,里面的Region會(huì)被釋放,而并不能說(shuō)回收行為就只是針對(duì)這個(gè)集合里面的Region進(jìn)行,因?yàn)闃?biāo)記過(guò)程是針對(duì)全堆的。此外,在JDK 12的ZGC中開(kāi)始支持的類(lèi)卸載以及弱引用的處理,也是在這個(gè)階段中完成的。

·并發(fā)重分配(Concurrent Relocate):重分配是ZGC執(zhí)行過(guò)程中的核心階段,這個(gè)過(guò)程要把重分配集中的存活對(duì)象復(fù)制到新的Region上,并為重分配集中的每個(gè)Region維護(hù)一個(gè)轉(zhuǎn)發(fā)表(Forward Table),記錄從舊對(duì)象到新對(duì)象的轉(zhuǎn)向關(guān)系。得益于染色指針的支持,ZGC收集器能僅從引用上就明確得知一個(gè)對(duì)象是否處于重分配集之中,如果用戶(hù)線(xiàn)程此時(shí)并發(fā)訪問(wèn)了位于重分配集中的對(duì)象,這次訪問(wèn)將會(huì)被預(yù)置的內(nèi)存屏障所截獲,然后立即根據(jù)Region上的轉(zhuǎn)發(fā)表記錄將訪問(wèn)轉(zhuǎn)發(fā)到新復(fù)制的對(duì)象上,并同時(shí)修正更新該引用的值,使其直接指向新對(duì)象,ZGC將這種行為稱(chēng)為指針的“自愈”(Self-Healing)能力。這樣做的好處是只有第一次訪問(wèn)舊對(duì)象會(huì)陷入轉(zhuǎn)發(fā),也就是只慢一次,對(duì)比Shenandoah的Brooks轉(zhuǎn)發(fā)指針,那是每次對(duì)象訪問(wèn)都必須付出的固定開(kāi)銷(xiāo),簡(jiǎn)單地說(shuō)就是每次都慢,因此ZGC對(duì)用戶(hù)程序的運(yùn)行時(shí)負(fù)載要比Shenandoah來(lái)得更低一些。還有另外一個(gè)直接的好處是由于染色指針的存在,一旦重分配集中某個(gè)Region的存活對(duì)象都復(fù)制完畢后,這個(gè)Region就可以立即釋放用于新對(duì)象的分配(但是轉(zhuǎn)發(fā)表還得留著不能釋放掉),哪怕堆中還有很多指向這個(gè)對(duì)象的未更新指針也沒(méi)有關(guān)系,這些舊指針一旦被使用,它們都是可以自愈的。

·并發(fā)重映射(Concurrent Remap):重映射所做的就是修正整個(gè)堆中指向重分配集中舊對(duì)象的所有引用,這一點(diǎn)從目標(biāo)角度看是與Shenandoah并發(fā)引用更新階段一樣的,但是ZGC的并發(fā)重映射并不是一個(gè)必須要“迫切”去完成的任務(wù),因?yàn)榍懊嬲f(shuō)過(guò),即使是舊引用,它也是可以自愈的,最多只是第一次使用時(shí)多一次轉(zhuǎn)發(fā)和修正操作。重映射清理這些舊引用的主要目的是為了不變慢(還有清理結(jié)束后可以釋放轉(zhuǎn)發(fā)表這樣的附帶收益),所以說(shuō)這并不是很“迫切”。因此,ZGC很巧妙地把并發(fā)重映射階段要做的工作,合并到了下一次垃圾收集循環(huán)中的并發(fā)標(biāo)記階段里去完成,反正它們都是要遍歷所有對(duì)象的,這樣合并就節(jié)省了一次遍歷對(duì)象圖[插圖]的開(kāi)銷(xiāo)。一旦所有指針都被修正之后,原來(lái)記錄新舊對(duì)象關(guān)系的轉(zhuǎn)發(fā)表就可以釋放掉了。

3.7 選擇合適的垃圾收集器



3.8 實(shí)戰(zhàn):內(nèi)存分配與回收策略

Java技術(shù)體系的自動(dòng)內(nèi)存管理,**最根本的目標(biāo)是自動(dòng)化地解決兩個(gè)問(wèn)題:自動(dòng)給對(duì)象分配內(nèi)存以及自動(dòng)回收分配給對(duì)象的內(nèi)存。**內(nèi)存回收,前面已經(jīng)用大量篇幅進(jìn)行闡述。接下來(lái)進(jìn)行對(duì)象內(nèi)存分配講解。

對(duì)象的內(nèi)存分配,從概念上講,應(yīng)該都是在堆上分配(而實(shí)際上也有可能經(jīng)過(guò)即時(shí)編譯后被拆散為標(biāo)量類(lèi)型并間接地在棧上分配)。在經(jīng)典分代的設(shè)計(jì)下,新生對(duì)象通常會(huì)分配在新生代中,少數(shù)情況下(例如對(duì)象大小超過(guò)一定閾值)也可能會(huì)直接分配在老年代。

3.8.1 對(duì)象優(yōu)先在Eden分配

大多數(shù)情況下,對(duì)象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒(méi)有足夠空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC。

HotSpot虛擬機(jī)提供了-XX:+PrintGCDetails這個(gè)收集器日志參數(shù),告訴虛擬機(jī)在發(fā)生垃圾收集行為時(shí)打印內(nèi)存回收日志,并且在進(jìn)程退出的時(shí)候輸出當(dāng)前的內(nèi)存各區(qū)域分配情況。

3.8.2 大對(duì)象直接進(jìn)入老年代

大對(duì)象就是指需要大量連續(xù)內(nèi)存空間的Java對(duì)象,最典型的大對(duì)象便是那種很長(zhǎng)的字符串,或者元素?cái)?shù)量很龐大的數(shù)組。

HotSpot虛擬機(jī)提供了-XX:PretenureSizeThreshold參數(shù),指定大于該設(shè)置值的對(duì)象直接在老年代分配,這樣做的目的就是避免在Eden區(qū)及兩個(gè)Survivor區(qū)之間來(lái)回復(fù)制,產(chǎn)生大量的內(nèi)存復(fù)制操作。

注意 -XX:PretenureSizeThreshold參數(shù)只對(duì)Serial和ParNew兩款新生代收集器有效,HotSpot的其他新生代收集器,如Parallel Scavenge并不支持這個(gè)參數(shù)。如果必須使用此參數(shù)進(jìn)行調(diào)優(yōu),可考慮ParNew加CMS的收集器組合。

3.8.3 長(zhǎng)期存活的對(duì)象將進(jìn)入老年代

對(duì)象通常在Eden區(qū)里誕生,如果經(jīng)過(guò)第一次Minor GC后仍然存活,并且能被Survivor容納的話(huà),該對(duì)象會(huì)被移動(dòng)到Survivor空間中,并且將其對(duì)象年齡設(shè)為1歲。對(duì)象在Survivor區(qū)中每熬過(guò)一次Minor GC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15),就會(huì)被晉升到老年代中。對(duì)象晉升老年代的年齡閾值,可以通過(guò)參數(shù)-XX:MaxTenuringThreshold設(shè)置。

3.8.4 動(dòng)態(tài)對(duì)象年齡判定

為了能更好地適應(yīng)不同程序的內(nèi)存狀況,HotSpot虛擬機(jī)并不是永遠(yuǎn)要求對(duì)象的年齡必須達(dá)到-XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,無(wú)須等到-XX:MaxTenuringThreshold中要求的年齡。

3.8.5 空間分配擔(dān)保

在發(fā)生Minor GC之前,虛擬機(jī)必須先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果這個(gè)條件成立,那這一次Minor GC可以確保是安全的。如果不成立,則虛擬機(jī)會(huì)先查看-XX:HandlePromotionFailure參數(shù)的設(shè)置值是否允許擔(dān)保失敗(Handle PromotionFailure);如果允許,那會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小,如果大于,將嘗試進(jìn)行一次Minor GC,盡管這次Minor GC是有風(fēng)險(xiǎn)的;如果小于,或者-XX:HandlePromotionFailure設(shè)置不允許冒險(xiǎn),那這時(shí)就要改為進(jìn)行一次FullGC。

解釋一下“冒險(xiǎn)”是冒了什么風(fēng)險(xiǎn):前面提到過(guò),新生代使用復(fù)制收集算法,但為了內(nèi)存利用率,只使用其中一個(gè)Survivor空間來(lái)作為輪換備份,因此當(dāng)出現(xiàn)大量對(duì)象在Minor GC后仍然存活的情況——最極端的情況就是內(nèi)存回收后新生代中所有對(duì)象都存活,需要老年代進(jìn)行分配擔(dān)保,把Survivor無(wú)法容納的對(duì)象直接送入老年代。

老年代要進(jìn)行這樣的擔(dān)保,前提是老年代本身還有容納這些對(duì)象的剩余空間,但一共有多少對(duì)象會(huì)在這次回收中活下來(lái)在實(shí)際完成內(nèi)存回收之前是無(wú)法明確知道的,所以只能取之前每一次回收晉升到老年代對(duì)象容量的平均大小作為經(jīng)驗(yàn)值,與老年代的剩余空間進(jìn)行比較,決定是否進(jìn)行Full GC來(lái)讓老年代騰出更多空間。

通常情況下都還是會(huì)將-XX:HandlePromotionFailure開(kāi)關(guān)打開(kāi)。

第4章 虛擬機(jī)性能監(jiān)控、故障處理工具

4.2 基礎(chǔ)故障處理工具

4.2.1 jps:虛擬機(jī)進(jìn)程狀況工具

jps(JVM Process Status Tool):

列出正在運(yùn)行的虛擬機(jī)進(jìn)程,并顯示虛擬機(jī)執(zhí)行主類(lèi)(Main Class,main()函數(shù)所在的類(lèi))名稱(chēng)以及這些進(jìn)程的本地虛擬機(jī)唯一ID(LVMID,Local Virtual Machine Identifier)。

jps還可以通過(guò)RMI協(xié)議查詢(xún)開(kāi)啟了RMI服務(wù)的遠(yuǎn)程虛擬機(jī)進(jìn)程狀態(tài),參數(shù)hostid為RMI注冊(cè)表中注冊(cè)的主機(jī)名。

4.2.2 jstat:虛擬機(jī)統(tǒng)計(jì)信息監(jiān)視工具

jstat(JVM Statistics Monitoring Tool)是用于監(jiān)視虛擬機(jī)各種運(yùn)行狀態(tài)信息的命令行工具。

它可以顯示本地或者遠(yuǎn)程[插圖]虛擬機(jī)進(jìn)程中的類(lèi)加載、內(nèi)存、垃圾收集、即時(shí)編譯等運(yùn)行時(shí)數(shù)據(jù),在沒(méi)有GUI圖形界面、只提供了純文本控制臺(tái)環(huán)境的服務(wù)器上,它將是運(yùn)行期定位虛擬機(jī)性能問(wèn)題的常用工具。

4.2.3 jinfo:Java配置信息工具

jinfo(Configuration Info for Java)的作用是實(shí)時(shí)查看和調(diào)整虛擬機(jī)各項(xiàng)參數(shù)。

4.2.4 jmap:Java內(nèi)存映像工具

jmap(Memory Map for Java)命令用于生成堆轉(zhuǎn)儲(chǔ)快照。

jmap的作用并不僅僅是為了獲取堆轉(zhuǎn)儲(chǔ)快照,它還可以查詢(xún)finalize執(zhí)行隊(duì)列、Java堆和方法區(qū)的詳細(xì)信息,如空間使用率、當(dāng)前用的是哪種收集器等。

4.2.5 jhat:虛擬機(jī)堆轉(zhuǎn)儲(chǔ)快照分析工具

jhat(JVM Heap Analysis Tool)命令與jmap搭配使用,來(lái)分析jmap生成的堆轉(zhuǎn)儲(chǔ)快照。

jhat內(nèi)置了一個(gè)微型的HTTP/Web服務(wù)器,生成堆轉(zhuǎn)儲(chǔ)快照的分析結(jié)果后,可以在瀏覽器中查看。

實(shí)際上,jhat工作中不嘗試用。可以使用VisualVM,Eclipse MemoryAnalyzer、IBM HeapAnalyzer。

4.2.6 jstack:Java堆棧跟蹤工具

jstack(Stack Trace for Java)命令用于生成虛擬機(jī)當(dāng)前時(shí)刻的線(xiàn)程快照(一般稱(chēng)為threaddump或者javacore文件)。

線(xiàn)程快照就是當(dāng)前虛擬機(jī)內(nèi)每一條線(xiàn)程正在執(zhí)行的方法堆棧的集合,生成線(xiàn)程快照的目的通常是定位線(xiàn)程出現(xiàn)長(zhǎng)時(shí)間停頓的原因,如線(xiàn)程間死鎖、死循環(huán)、請(qǐng)求外部資源導(dǎo)致的長(zhǎng)時(shí)間掛起等,都是導(dǎo)致線(xiàn)程長(zhǎng)時(shí)間停頓的常見(jiàn)原因。

4.2.7 基礎(chǔ)工具總結(jié)

下面表4-5~表4-14中羅列了JDK附帶的全部(包括曾經(jīng)存在但已經(jīng)在最新版本中被移除的)工具及其簡(jiǎn)要用途。

·基礎(chǔ)工具:用于支持基本的程序創(chuàng)建和運(yùn)行(見(jiàn)表4-5)

·安全:用于程序簽名、設(shè)置安全測(cè)試等(見(jiàn)表4-6)

·國(guó)際化:用于創(chuàng)建本地語(yǔ)言文件(見(jiàn)表4-7)

·遠(yuǎn)程方法調(diào)用:用于跨Web或網(wǎng)絡(luò)的服務(wù)交互(見(jiàn)表4-8)

·部署工具:用于程序打包、發(fā)布和部署(見(jiàn)表4-10)

·Java Web Start(見(jiàn)表4-11)

·性能監(jiān)控和故障處理:用于監(jiān)控分析Java虛擬機(jī)運(yùn)行信息,排查問(wèn)題(見(jiàn)表4-12

·WebService工具:與CORBA一起在JDK 11中被移除(見(jiàn)表4-13)

·REPL和腳本工具(見(jiàn)表4-14)

4.3 可視化故障處理工具

主要包括JConsole、JHSDB、VisualVM和JMC。

4.3.1 JHSDB:基于服務(wù)性代理的調(diào)試工具

4.3.2 JConsole:Java監(jiān)視與管理控制臺(tái)

4.3.3 VisualVM:多合-故障處理工具

4.3.4 Java Mission Control:可持續(xù)在線(xiàn)的監(jiān)控工具

4.4 HotSpot虛擬機(jī)插件及工具

·Ideal Graph Visualizer:用于可視化展示C2即時(shí)編譯器是如何將字節(jié)碼轉(zhuǎn)化為理想圖,然后轉(zhuǎn)化為機(jī)器碼的。

·Client Compiler Visualizer:用于查看C1即時(shí)編譯器生成高級(jí)中間表示(HIR),轉(zhuǎn)換成低級(jí)中間表示(LIR)和做物理寄存器分配的過(guò)程。

·MakeDeps:幫助處理HotSpot的編譯依賴(lài)的工具。

·Project Creator:幫忙生成Visual Studio的.project文件的工具。

·LogCompilation:將-XX:+LogCompilation輸出的日志整理成更容易閱讀的格式的工具。

·HSDIS:即時(shí)編譯器的反匯編插件。

補(bǔ)充:

·IBM的Support Assistant、Heap Analyzer、Javacore Analyzer、GarbageCollector Analyzer適用于IBM J9/OpenJ9 VM

·HP的HPjmeter、HPjtune適用于HP-UX、SAP、HotSpot VM。

·Eclipse的Memory Analyzer Tool(MAT)適用于HP-UX、SAP、HotSpot VM,安裝IBMDTFJ插件后可支持IBM J9虛擬機(jī)。

第5章 調(diào)優(yōu)案例分析與實(shí)戰(zhàn)

5.3 實(shí)戰(zhàn):Eclipse運(yùn)行速度調(diào)優(yōu)

第三部分 虛擬機(jī)執(zhí)行子系統(tǒng)

第6章 類(lèi)文件結(jié)構(gòu)

6.2 無(wú)關(guān)性的基石

各種不同平臺(tái)的Java虛擬機(jī),以及所有平臺(tái)都統(tǒng)一支持的程序存儲(chǔ)格式——字節(jié)碼(Byte Code)是構(gòu)成平臺(tái)無(wú)關(guān)性的基石。

實(shí)現(xiàn)語(yǔ)言無(wú)關(guān)性的基礎(chǔ)仍然是虛擬機(jī)和字節(jié)碼存儲(chǔ)格式。。Java虛擬機(jī)不與包括Java語(yǔ)言在內(nèi)的任何程序語(yǔ)言綁定,它只與“Class文件”這種特定的二進(jìn)制文件格式所關(guān)聯(lián),Class文件中包含了Java虛擬機(jī)指令集、符號(hào)表以及若干其他輔助信息。

6.3 Class類(lèi)文件的結(jié)構(gòu)

Class文件是一組以8個(gè)字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列在文件之中,中間沒(méi)有添加任何分隔符,這使得整個(gè)Class文件中存儲(chǔ)的內(nèi)容幾乎全部是程序運(yùn)行的必要數(shù)據(jù),沒(méi)有空隙存在。當(dāng)遇到需要占用8個(gè)字節(jié)以上空間的數(shù)據(jù)項(xiàng)時(shí),則會(huì)按照高位在前的方式分割成若干個(gè)8個(gè)字節(jié)進(jìn)行存儲(chǔ)。

根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Class文件格式采用一種類(lèi)似于C語(yǔ)言結(jié)構(gòu)體的偽結(jié)構(gòu)來(lái)存儲(chǔ)數(shù)據(jù),這種偽結(jié)構(gòu)中只有兩種數(shù)據(jù)類(lèi)型:“無(wú)符號(hào)數(shù)”和“表”

·無(wú)符號(hào)數(shù)屬于基本的數(shù)據(jù)類(lèi)型,以u(píng)1、u2、u4、u8來(lái)分別代表1個(gè)字節(jié)、2個(gè)字節(jié)、4個(gè)字節(jié)和8個(gè)字節(jié)的無(wú)符號(hào)數(shù),無(wú)符號(hào)數(shù)可以用來(lái)描述數(shù)字、索引引用、數(shù)量值或者按照UTF-8編碼構(gòu)成字符串值。

·是由多個(gè)無(wú)符號(hào)數(shù)或者其他表作為數(shù)據(jù)項(xiàng)構(gòu)成的復(fù)合數(shù)據(jù)類(lèi)型,所有表的命名都習(xí)慣性地以“_info”結(jié)尾。表用于描述有層次關(guān)系的復(fù)合結(jié)構(gòu)的數(shù)據(jù),整個(gè)Class文件本質(zhì)上也可以視作是一張表

無(wú)論是無(wú)符號(hào)數(shù)還是表,當(dāng)需要描述同一類(lèi)型但數(shù)量不定的多個(gè)數(shù)據(jù)時(shí),經(jīng)常會(huì)使用一個(gè)前置的容量計(jì)數(shù)器加若干個(gè)連續(xù)的數(shù)據(jù)項(xiàng)的形式,這時(shí)候稱(chēng)這一系列連續(xù)的某一類(lèi)型的數(shù)據(jù)為某一類(lèi)型的“集合”。

6.3.1 魔數(shù)與Class文件的版本

每個(gè)Class文件的頭4個(gè)字節(jié)被稱(chēng)為魔數(shù)(Magic Number),它的唯一作用是確定這個(gè)文件是否為一個(gè)能被虛擬機(jī)接受的Class文件。Class文件的魔數(shù)取得很有“浪漫氣息”,值為0xCAFEBABE(咖啡寶貝?)。

緊接著魔數(shù)的4個(gè)字節(jié)存儲(chǔ)的是Class文件的版本號(hào):第5和第6個(gè)字節(jié)是次版本號(hào)(Minor Version),第7和第8個(gè)字節(jié)是主版本號(hào)(Major Version)。Java的版本號(hào)是從45開(kāi)始的,JDK 1.1之后的每個(gè)JDK大版本發(fā)布主版本號(hào)向上加1(JDK 1.0~1.1使用了45.0~45.3的版本號(hào)),高版本的JDK能向下兼容以前版本的Class文件,但不能運(yùn)行以后版本的Class文件。

6.3.2 常量池

由于常量池中常量的數(shù)量是不固定的,所以在常量池的入口需要放置一項(xiàng)u2類(lèi)型的數(shù)據(jù),代表常量池容量計(jì)數(shù)值(constant_pool_count)。與Java中語(yǔ)言習(xí)慣不同,這個(gè)容量計(jì)數(shù)是從1而不是0開(kāi)始的。

如圖6-3所示,常量池容量(偏移地址:0x00000008)為十六進(jìn)制數(shù)0x0016,即十進(jìn)制的22,這就代表常量池中有21項(xiàng)常量,索引值范圍為1~21。在Class文件格式規(guī)范制定之時(shí),設(shè)計(jì)者將第0項(xiàng)常量空出來(lái)是有特殊考慮的,這樣做的目的在于,如果后面某些指向常量池的索引值的數(shù)據(jù)在特定情況下需要表達(dá)“不引用任何一個(gè)常量池項(xiàng)目”的含義,可以把索引值設(shè)置為0來(lái)表示。Class文件結(jié)構(gòu)中只有常量池的容量計(jì)數(shù)是從1開(kāi)始,對(duì)于其他集合類(lèi)型,包括接口索引集合、字段表集合、方法表集合等的容量計(jì)數(shù)都與一般習(xí)慣相同,是從0開(kāi)始。

常量池中主要存放兩大類(lèi)常量:字面量(Literal)和符號(hào)引用(Symbolic References)

字面量比較接近于Java語(yǔ)言層面的常量概念,如文本字符串、被聲明為final的常量值等

符號(hào)引用則屬于編譯原理方面的概念,主要包括下面幾類(lèi)常量:
·被模塊導(dǎo)出或者開(kāi)放的包(Package)
·類(lèi)和接口的全限定名(Fully Qualified Name)
·字段的名稱(chēng)和描述符(Descriptor)·
方法的名稱(chēng)和描述符
·方法句柄和方法類(lèi)型(Method Handle、Method Type、Invoke Dynamic)
·動(dòng)態(tài)調(diào)用點(diǎn)和動(dòng)態(tài)常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

6.3.3 訪問(wèn)標(biāo)志

訪問(wèn)標(biāo)志(access_flags),2個(gè)字節(jié),用于識(shí)別一些類(lèi)或者接口層次的訪問(wèn)信息,包括:這個(gè)Class是類(lèi)還是接口;是否定義為public類(lèi)型;是否定義為abstract類(lèi)型;如果是類(lèi)的話(huà),是否被聲明為final;等等。

6.3.4 類(lèi)索引、父類(lèi)索引與接口索引集合

類(lèi)索引(this_class)和父類(lèi)索引(super_class)都是一個(gè)u2類(lèi)型的數(shù)據(jù),而接口索引集合(interfaces)是一組u2類(lèi)型的數(shù)據(jù)的集合,Class文件中由這三項(xiàng)數(shù)據(jù)來(lái)確定該類(lèi)型的繼承關(guān)系。類(lèi)索引用于確定這個(gè)類(lèi)的全限定名,父類(lèi)索引用于確定這個(gè)類(lèi)的父類(lèi)的全限定名。由于Java語(yǔ)言不允許多重繼承,所以父類(lèi)索引只有一個(gè),除了java.lang.Object之外,所有的Java類(lèi)都有父類(lèi),因此除了java.lang.Object外,所有Java類(lèi)的父類(lèi)索引都不為0。接口索引集合就用來(lái)描述這個(gè)類(lèi)實(shí)現(xiàn)了哪些接口,這些被實(shí)現(xiàn)的接口將按implements關(guān)鍵字(如果這個(gè)Class文件表示的是一個(gè)接口,則應(yīng)當(dāng)是extends關(guān)鍵字)后的接口順序從左到右排列在接口索引集合中。

類(lèi)索引、父類(lèi)索引和接口索引集合都按順序排列在訪問(wèn)標(biāo)志之后,類(lèi)索引和父類(lèi)索引用兩個(gè)u2類(lèi)型的索引值表示,它們各自指向一個(gè)類(lèi)型為CONSTANT_Class_info的類(lèi)描述符常量,通過(guò)CONSTANT_Class_info類(lèi)型的常量中的索引值可以找到定義在CONSTANT_Utf8_info類(lèi)型的常量中的全限定名字符串。

對(duì)于接口索引集合,入口的第一項(xiàng)u2類(lèi)型的數(shù)據(jù)為接口計(jì)數(shù)器(interfaces_count),表示索引表的容量。如果該類(lèi)沒(méi)有實(shí)現(xiàn)任何接口,則該計(jì)數(shù)器值為0,后面接口的索引表不再占用任何字節(jié)。

6.3.5 字段表集合

字段表(field_info)用于描述接口或者類(lèi)中聲明的變量。Java語(yǔ)言中的“字段”(Field)包括類(lèi)級(jí)變量以及實(shí)例級(jí)變量,但不包括在方法內(nèi)部聲明的局部變量。

字段可以包括的修飾符有字段的作用域(public、private、protected修飾符)、是實(shí)例變量還是類(lèi)變量(static修飾符)、可變性(final)、并發(fā)可見(jiàn)性(volatile修飾符,是否強(qiáng)制從主內(nèi)存讀寫(xiě))、可否被序列化(transient修飾符)、字段數(shù)據(jù)類(lèi)型(基本類(lèi)型、對(duì)象、數(shù)組)、字段名稱(chēng)。上述這些信息中,各個(gè)修飾符都是布爾值,要么有某個(gè)修飾符,要么沒(méi)有,很適合使用標(biāo)志位來(lái)表示。而字段叫做什么名字、字段被定義為什么數(shù)據(jù)類(lèi)型,這些都是無(wú)法固定的,只能引用常量池中的常量來(lái)描述。表6-8中列出了字段表的最終格式。

6.3.6 方法表集合

Class文件存儲(chǔ)格式中對(duì)方法的描述與對(duì)字段的描述采用了幾乎完全一致的方式,方法表的結(jié)構(gòu)如同字段表一樣,依次包括訪問(wèn)標(biāo)志(access_flags)、名稱(chēng)索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)幾項(xiàng),如表6-11所示。

6.3.7 屬性表集合

屬性表(attribute_info)在前面的講解之中已經(jīng)出現(xiàn)過(guò)數(shù)次,Class文件、字段表、方法表都可以攜帶自己的屬性表集合,以描述某些場(chǎng)景專(zhuān)有的信息。

  • signature屬性
    可是一個(gè)選的定長(zhǎng)屬性,可出現(xiàn)于類(lèi)、字段表和方法表結(jié)構(gòu)的屬性表中。任何類(lèi)、接口、初始化方法或成員的泛型簽名如果包含了類(lèi)型變量(type variable)或參數(shù)化類(lèi)型(parameterized type),則signature屬性會(huì)為它記錄泛型簽名信息。之所以需要這樣一個(gè)屬性去記錄泛型類(lèi)型,是因?yàn)镴ava語(yǔ)言的泛型采用的是擦除法實(shí)現(xiàn)的偽泛型,字節(jié)碼中的所有泛型信息編譯(類(lèi)型變量和參數(shù)化類(lèi)型)在編譯之后通通被擦除掉。使用擦除法的好處是實(shí)現(xiàn)簡(jiǎn)單,非常容易實(shí)現(xiàn)backport,運(yùn)行期也能節(jié)省一些類(lèi)型所占的內(nèi)存空間。但是壞處是運(yùn)行期就無(wú)法像C#等有真泛型支持的語(yǔ)言那樣,將泛型類(lèi)型和用戶(hù)定義的普通類(lèi)型同等對(duì)待,例如運(yùn)行期間做反射無(wú)法獲得泛型信息。Signature屬性就是為了彌補(bǔ)這個(gè)缺陷而增設(shè)的,現(xiàn)在java的 api能獲取的泛型類(lèi)型,最終數(shù)據(jù)來(lái)源也是這個(gè)屬性。

6.4 字節(jié)碼指令簡(jiǎn)介

Java虛擬機(jī)的指令由一個(gè)字節(jié)長(zhǎng)度的、代表著某種特定操作含義的數(shù)字(稱(chēng)為操作碼,Opcode)以及跟隨其后的零至多個(gè)代表此操作所需的參數(shù)(稱(chēng)為操作數(shù),Operand)構(gòu)成。

由于Java虛擬機(jī)采用面向操作數(shù)棧而不是面向寄存器的架構(gòu)(這兩種架構(gòu)的執(zhí)行過(guò)程、區(qū)別和影響將在第8章中探討),所以大多數(shù)指令都不包含操作數(shù),只有一個(gè)操作碼,指令參數(shù)都存放在操作數(shù)棧中。

,由于限制了Java虛擬機(jī)操作碼的長(zhǎng)度為一個(gè)字節(jié)(即0~255),這意味著指令集的操作碼總數(shù)不能夠超過(guò)256條。又由于Class文件格式放棄了編譯后代碼的操作數(shù)長(zhǎng)度對(duì)齊,這就意味著虛擬機(jī)在處理那些超過(guò)一個(gè)字節(jié)的數(shù)據(jù)時(shí),不得不在運(yùn)行時(shí)從字節(jié)中重建出具體數(shù)據(jù)的結(jié)構(gòu),譬如要將一個(gè)16位長(zhǎng)度的無(wú)符號(hào)整數(shù)使用兩個(gè)無(wú)符號(hào)字節(jié)存儲(chǔ)起來(lái)(假設(shè)將它們命名為byte1和byte2),那它們的值應(yīng)該是這樣的:

這種操作在某種程度上會(huì)導(dǎo)致解釋執(zhí)行字節(jié)碼時(shí)將損失一些性能,但這樣做的優(yōu)勢(shì)也同樣明顯:放棄了操作數(shù)長(zhǎng)度對(duì)齊[插圖],就意味著可以省略掉大量的填充和間隔符號(hào);用一個(gè)字節(jié)來(lái)代表操作碼,也是為了盡可能獲得短小精干的編譯代碼。

6.4.1 字節(jié)碼與數(shù)據(jù)類(lèi)型

在Java虛擬機(jī)的指令集中,大多數(shù)指令都包含其操作所對(duì)應(yīng)的數(shù)據(jù)類(lèi)型信息。

舉個(gè)例子,iload指令用于從局部變量表中加載int型的數(shù)據(jù)到操作數(shù)棧中,而fload指令加載的則是float類(lèi)型的數(shù)據(jù)。這兩條指令的操作在虛擬機(jī)內(nèi)部可能會(huì)是由同一段代碼來(lái)實(shí)現(xiàn)的,但在Class文件中它們必須擁有各自獨(dú)立的操作碼。

6.4.2 加載和存儲(chǔ)指令

加載和存儲(chǔ)指令用于將數(shù)據(jù)在棧幀中的局部變量表和操作數(shù)棧之間來(lái)回傳輸,這類(lèi)指令包括:

·將一個(gè)局部變量加載到操作棧:iload、iload_、lload、lload_、fload、fload_、dload、dload_、aload、aload_

·將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表:istore、istore_、lstore、lstore_、fstore、fstore_、dstore、dstore_、astore、astore_

·將一個(gè)常量加載到操作數(shù)棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_、fconst_、dconst_

·擴(kuò)充局部變量表的訪問(wèn)索引的指令:wide

上面所列舉的指令助記符中,有一部分是以尖括號(hào)結(jié)尾的(例如iload_),這些指令助記符實(shí)際上代表了一組指令(例如iload_,它代表了iload_0、iload_1、iload_2和iload_3這幾條指令)。

6.4.3 運(yùn)算指令

算術(shù)指令用于對(duì)兩個(gè)操作數(shù)棧上的值進(jìn)行某種特定運(yùn)算,并把結(jié)果重新存入到操作棧頂。

大體上運(yùn)算指令可以分為兩種:對(duì)整型數(shù)據(jù)進(jìn)行運(yùn)算的指令與對(duì)浮點(diǎn)型數(shù)據(jù)進(jìn)行運(yùn)算的指令。

無(wú)論是哪種算術(shù)指令,均是使用Java虛擬機(jī)的算術(shù)類(lèi)型來(lái)進(jìn)行計(jì)算的,換句話(huà)說(shuō)是不存在直接支持byte、short、char和boolean類(lèi)型的算術(shù)指令,對(duì)于上述幾種數(shù)據(jù)的運(yùn)算,應(yīng)使用操作int類(lèi)型的指令代替。

所有的算術(shù)指令包括:

·加法指令:iadd、ladd、fadd、dadd

·減法指令:isub、lsub、fsub、dsub

·乘法指令:imul、lmul、fmul、dmul

·除法指令:idiv、ldiv、fdiv、ddiv

·求余指令:irem、lrem、frem、drem

·取反指令:ineg、lneg、fneg、dneg

·位移指令:ishl、ishr、iushr、lshl、lshr、lushr

·按位或指令:ior、lor

·按位與指令:iand、land

·按位異或指令:ixor、lxor

·局部變量自增指令:iinc

·比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

6.4.4 類(lèi)型轉(zhuǎn)換指令

Java虛擬機(jī)直接支持(即轉(zhuǎn)換時(shí)無(wú)須顯式的轉(zhuǎn)換指令)以下數(shù)值類(lèi)型的寬化類(lèi)型轉(zhuǎn)換(Widening NumericConversion,即小范圍類(lèi)型向大范圍類(lèi)型的安全轉(zhuǎn)換)

·int類(lèi)型到long、float或者double類(lèi)型

·long類(lèi)型到float、double類(lèi)型

·float類(lèi)型到double類(lèi)型

與之相對(duì)的,處理窄化類(lèi)型轉(zhuǎn)換(Narrowing Numeric Conversion)時(shí),就必須顯式地使用轉(zhuǎn)換指令來(lái)完成,這些轉(zhuǎn)換指令包括i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化類(lèi)型轉(zhuǎn)換可能會(huì)導(dǎo)致轉(zhuǎn)換結(jié)果產(chǎn)生不同的正負(fù)號(hào)、不同的數(shù)量級(jí)的情況,轉(zhuǎn)換過(guò)程很可能會(huì)導(dǎo)致數(shù)值的精度丟失。

6.4.5 對(duì)象創(chuàng)建與訪問(wèn)指令

Java虛擬機(jī)對(duì)類(lèi)實(shí)例和數(shù)組的創(chuàng)建與操作使用了不同的字節(jié)碼指令。對(duì)象創(chuàng)建后,就可以通過(guò)對(duì)象訪問(wèn)指令獲取對(duì)象實(shí)例或者數(shù)組實(shí)例中的字段或者數(shù)組元素,這些指令包括:

·創(chuàng)建類(lèi)實(shí)例的指令:new

·創(chuàng)建數(shù)組的指令:newarray、anewarray、multianewarray

·訪問(wèn)類(lèi)字段(static字段,或者稱(chēng)為類(lèi)變量)和實(shí)例字段(非static字段,或者稱(chēng)為實(shí)例變量)的指令:getfield、putfield、getstatic、putstatic

·把一個(gè)數(shù)組元素加載到操作數(shù)棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload

·將一個(gè)操作數(shù)棧的值儲(chǔ)存到數(shù)組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore

·取數(shù)組長(zhǎng)度的指令:arraylength

·檢查類(lèi)實(shí)例類(lèi)型的指令:instanceof、checkcast

6.4.6 操作數(shù)棧管理指令

如同操作一個(gè)普通數(shù)據(jù)結(jié)構(gòu)中的堆棧那樣,Java虛擬機(jī)提供了一些用于直接操作操作數(shù)棧的指令,包括:

·將操作數(shù)棧的棧頂一個(gè)或兩個(gè)元素出棧:pop、pop2

·復(fù)制棧頂一個(gè)或兩個(gè)數(shù)值并將復(fù)制值或雙份的復(fù)制值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2

·將棧最頂端的兩個(gè)數(shù)值互換:swap

6.4.7 控制轉(zhuǎn)移指令

控制轉(zhuǎn)移指令可以讓Java虛擬機(jī)有條件或無(wú)條件地從指定位置指令(而不是控制轉(zhuǎn)移指令)的下一條指令繼續(xù)執(zhí)行程序,從概念模型上理解,可以認(rèn)為控制指令就是在有條件或無(wú)條件地修改PC寄存器的值。控制轉(zhuǎn)移指令包括:

·條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne

·復(fù)合條件分支:tableswitch、lookupswitch

·無(wú)條件分支:goto、goto_w、jsr、jsr_w、ret

6.4.8 方法調(diào)用和返回指令

方法調(diào)用(分派、執(zhí)行過(guò)程)將在第8章具體講解,這里僅列舉以下五條指令用于方法調(diào)用:

·invokevirtual指令:用于調(diào)用對(duì)象的實(shí)例方法,根據(jù)對(duì)象的實(shí)際類(lèi)型進(jìn)行分派(虛方法分派),這也是Java語(yǔ)言中最常見(jiàn)的方法分派方式。

·invokeinterface指令:用于調(diào)用接口方法,它會(huì)在運(yùn)行時(shí)搜索一個(gè)實(shí)現(xiàn)了這個(gè)接口方法的對(duì)象,找出適合的方法進(jìn)行調(diào)用。

·invokespecial指令:用于調(diào)用一些需要特殊處理的實(shí)例方法,包括實(shí)例初始化方法、私有方法和父類(lèi)方法。

·invokestatic指令:用于調(diào)用類(lèi)靜態(tài)方法(static方法)。

·invokedynamic指令:用于在運(yùn)行時(shí)動(dòng)態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法。并執(zhí)行該方法。前面四條調(diào)用指令的分派邏輯都固化在Java虛擬機(jī)內(nèi)部,用戶(hù)無(wú)法改變,而invokedynamic指令的分派邏輯是由用戶(hù)所設(shè)定的引導(dǎo)方法決定的。

方法調(diào)用指令與數(shù)據(jù)類(lèi)型無(wú)關(guān),而方法返回指令是根據(jù)返回值的類(lèi)型區(qū)分的,包括ireturn(當(dāng)返回值是boolean、byte、char、short和int類(lèi)型時(shí)使用)、lreturn、freturn、dreturn和areturn,另外還有一條return指令供聲明為void的方法、實(shí)例初始化方法、類(lèi)和接口的類(lèi)初始化方法使用。

6.4.9 異常處理指令

在Java程序中顯式拋出異常的操作(throw語(yǔ)句)都由athrow指令來(lái)實(shí)現(xiàn),除了用throw語(yǔ)句顯式拋出異常的情況之外,《Java虛擬機(jī)規(guī)范》還規(guī)定了許多運(yùn)行時(shí)異常會(huì)在其他Java虛擬機(jī)指令檢測(cè)到異常狀況時(shí)自動(dòng)拋出。

而在Java虛擬機(jī)中,處理異常(catch語(yǔ)句)不是由字節(jié)碼指令來(lái)實(shí)現(xiàn)的,而是采用異常表來(lái)完成

6.4.10 同步指令

Java虛擬機(jī)可以支持**方法級(jí)的同步和方法內(nèi)部一段指令序列的同步,這兩種同步結(jié)構(gòu)都是使用管程(Monitor,更常見(jiàn)的是直接將它稱(chēng)為“鎖”)**來(lái)實(shí)現(xiàn)的。

方法級(jí)的同步是隱式的,無(wú)須通過(guò)字節(jié)碼指令來(lái)控制,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中。虛擬機(jī)可以從方法常量池中的方法表結(jié)構(gòu)中的ACC_SYNCHRONIZED訪問(wèn)標(biāo)志得知一個(gè)方法是否被聲明為同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的ACC_SYNCHRONIZED訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線(xiàn)程就要求先成功持有管程,然后才能執(zhí)行方法,最后當(dāng)方法完成(無(wú)論是正常完成還是非正常完成)時(shí)釋放管程。

同步一段指令集序列通常是由Java語(yǔ)言中的synchronized語(yǔ)句塊來(lái)表示的,Java虛擬機(jī)的指令集中有monitorenter和monitorexit兩條指令來(lái)支持synchronized關(guān)鍵字的語(yǔ)義,正確實(shí)現(xiàn)synchronized關(guān)鍵字需要Javac編譯器與Java虛擬機(jī)兩者共同協(xié)作支持.

6.5 公有設(shè)計(jì),私有實(shí)現(xiàn)

《Java虛擬機(jī)規(guī)范》描繪了Java虛擬機(jī)應(yīng)有的共同程序存儲(chǔ)格式:Class文件格式以及字節(jié)碼指令集。這些內(nèi)容與硬件、操作系統(tǒng)和具體的Java虛擬機(jī)實(shí)現(xiàn)之間是完全獨(dú)立的,虛擬機(jī)實(shí)現(xiàn)者可能更愿意把它們看作程序在各種Java平臺(tái)實(shí)現(xiàn)之間互相安全地交互的手段。理解公有設(shè)計(jì)與私有實(shí)現(xiàn)之間的分界線(xiàn)是非常有必要的,任何一款Java虛擬機(jī)實(shí)現(xiàn)都必須能夠讀取Class文件并精確實(shí)現(xiàn)包含在其中的Java虛擬機(jī)代碼的語(yǔ)義。

虛擬機(jī)實(shí)現(xiàn)的方式主要有以下兩種:

·將輸入的Java虛擬機(jī)代碼在加載時(shí)或執(zhí)行時(shí)翻譯成另一種虛擬機(jī)的指令集;

·將輸入的Java虛擬機(jī)代碼在加載時(shí)或執(zhí)行時(shí)翻譯成宿主機(jī)處理程序的本地指令集(即即時(shí)編譯器代碼生成技術(shù))。

第7章 虛擬機(jī)類(lèi)加載機(jī)制

代碼編譯:本地機(jī)器碼變成字節(jié)碼

7.1 概述

Java虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類(lèi)型,這個(gè)過(guò)程被稱(chēng)作虛擬機(jī)的類(lèi)加載機(jī)制。

Java中,類(lèi)型的加載、連接和初始化過(guò)程都是在程序運(yùn)行期間完成的,這種策略使得提前編譯變得困難,也增加了類(lèi)加載的性能開(kāi)銷(xiāo),但是為java應(yīng)用提供餓了擴(kuò)展性和靈活性。
java的動(dòng)態(tài)擴(kuò)展就是依賴(lài)運(yùn)行期間動(dòng)態(tài)加載和動(dòng)態(tài)連接這個(gè)特點(diǎn)實(shí)現(xiàn)的。

7.2 類(lèi)加載的時(shí)機(jī)

一個(gè)類(lèi)型從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期將會(huì)經(jīng)歷加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個(gè)階段,其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱(chēng)為連接(Linking)。這七個(gè)階段的發(fā)生順序如圖7-1所示。

圖7-1中,加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段的順序是確定的,類(lèi)型的加載過(guò)程必須按照這種順序按部就班地開(kāi)始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定特性(也稱(chēng)為
動(dòng)態(tài)綁定或晚期綁定
)。

關(guān)于在什么情況下需要開(kāi)始類(lèi)加載過(guò)程的第一個(gè)階段“加載”,《Java虛擬機(jī)規(guī)范》中并沒(méi)有進(jìn)行強(qiáng)制約束,這點(diǎn)可以交給虛擬機(jī)的具體實(shí)現(xiàn)來(lái)自由把握。

但是對(duì)于初始化階段,《Java虛擬機(jī)規(guī)范》則是嚴(yán)格規(guī)定了有且只有六種情況必須立即對(duì)類(lèi)進(jìn)行“初始化”(而加載、驗(yàn)證、準(zhǔn)備自然需要在此之前開(kāi)始):

1)遇到new、getstatic、putstatic或invokestatic這四條字節(jié)碼指令時(shí),如果類(lèi)型沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化階段。能夠生成這四條指令的典型Java代碼場(chǎng)景有:

·使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候。

·讀取或設(shè)置一個(gè)類(lèi)型的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候。

·調(diào)用一個(gè)類(lèi)型的靜態(tài)方法的時(shí)候。

2)使用java.lang.reflect包的方法對(duì)類(lèi)型進(jìn)行反射調(diào)用的時(shí)候,如果類(lèi)型沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。

3)當(dāng)初始化類(lèi)的時(shí)候,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類(lèi)的初始化

4)當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)(包含main()方法的那個(gè)類(lèi)),虛擬機(jī)會(huì)先初始化這個(gè)主類(lèi)

5)當(dāng)使用JDK 7新加入的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類(lèi)型的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。**

6)當(dāng)一個(gè)接口中定義了JDK 8新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時(shí),如果有這個(gè)接口的實(shí)現(xiàn)類(lèi)發(fā)生了初始化,那該接口要在其之前被初始化。

對(duì)于這六種會(huì)觸發(fā)類(lèi)型進(jìn)行初始化的場(chǎng)景,《Java虛擬機(jī)規(guī)范》中使用了一個(gè)非常強(qiáng)烈的限定語(yǔ)——“有且只有”,這六種場(chǎng)景中的行為稱(chēng)為對(duì)一個(gè)類(lèi)型進(jìn)行主動(dòng)引用。除此之外,所有引用類(lèi)型的方式都不會(huì)觸發(fā)初始化,稱(chēng)為被動(dòng)引用

-常量會(huì)在編譯階段存入調(diào)用類(lèi)的常量池,不會(huì)觸發(fā)定義類(lèi)的初始化

接口不能使用static{}語(yǔ)句塊,一個(gè)接口在初始化時(shí),并不需要其父接口都實(shí)現(xiàn)初始化,只有真正使用到父接口市,才會(huì)初始化父接口

7.3 類(lèi)加載的過(guò)程

接下來(lái)我們會(huì)詳細(xì)了解Java虛擬機(jī)中類(lèi)加載的全過(guò)程,即加載、驗(yàn)證、準(zhǔn)備、解析和初始化這五個(gè)階段所執(zhí)行的具體動(dòng)作。

7.3.1 加載

“加載”(Loading)階段是整個(gè)“類(lèi)加載”(Class Loading)過(guò)程中的一個(gè)階段.

在加載階段,Java虛擬機(jī)需要完成以下三件事情:

1)通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流。

2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。

3)在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪問(wèn)入口。

相對(duì)于類(lèi)加載過(guò)程的其他階段,非數(shù)組類(lèi)型的加載階段(準(zhǔn)確地說(shuō),是加載階段中獲取類(lèi)的二進(jìn)制字節(jié)流的動(dòng)作)是開(kāi)發(fā)人員可控性最強(qiáng)的階段。加載階段既可以使用Java虛擬機(jī)里內(nèi)置的引導(dǎo)類(lèi)加載器來(lái)完成,也可以由用戶(hù)自定義的類(lèi)加載器去完成,開(kāi)發(fā)人員通過(guò)定義自己的類(lèi)加載器去控制字節(jié)流的獲取方式(重寫(xiě)一個(gè)類(lèi)加載器的findClass()或loadClass()方法),實(shí)現(xiàn)根據(jù)自己的想法來(lái)賦予應(yīng)用程序獲取運(yùn)行代碼的動(dòng)態(tài)性。

對(duì)于數(shù)組類(lèi)而言,數(shù)組類(lèi)本身不通過(guò)類(lèi)加載器創(chuàng)建,它是由Java虛擬機(jī)直接在內(nèi)存中動(dòng)態(tài)構(gòu)造出來(lái)的。但數(shù)組類(lèi)與類(lèi)加載器仍然有很密切的關(guān)系,因?yàn)閿?shù)組類(lèi)的元素類(lèi)型(Element Type,指的是數(shù)組去掉所有維度的類(lèi)型)最終還是要靠類(lèi)加載器來(lái)完成加載,一個(gè)數(shù)組類(lèi)(下面簡(jiǎn)稱(chēng)為C)創(chuàng)建過(guò)程遵循以下規(guī)則:

·如果數(shù)組的組件類(lèi)型不是引用類(lèi)型(例如int[]數(shù)組的組件類(lèi)型為int),Java虛擬機(jī)將會(huì)把數(shù)組C標(biāo)記為與引導(dǎo)類(lèi)加載器關(guān)聯(lián)。

·數(shù)組類(lèi)的可訪問(wèn)性與它的組件類(lèi)型的可訪問(wèn)性一致,如果組件類(lèi)型不是引用類(lèi)型,它的數(shù)組類(lèi)的可訪問(wèn)性將默認(rèn)為public,可被所有的類(lèi)和接口訪問(wèn)到。

加載階段結(jié)束后,Java虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所設(shè)定的格式存儲(chǔ)在方法區(qū)之中了,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式完全由虛擬機(jī)實(shí)現(xiàn)自行定義,《Java虛擬機(jī)規(guī)范》未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)。

類(lèi)型數(shù)據(jù)妥善安置在方法區(qū)之后,會(huì)在Java堆內(nèi)存中實(shí)例化一個(gè)java.lang.Class類(lèi)的對(duì)象,這個(gè)對(duì)象將作為程序訪問(wèn)方法區(qū)中的類(lèi)型數(shù)據(jù)的外部接口。

7.3.2 驗(yàn)證

驗(yàn)證是連接階段的第一步,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運(yùn)行后不會(huì)危害虛擬機(jī)自身的安全。

驗(yàn)證階段大致上會(huì)完成下面四個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。

1.文件格式驗(yàn)證

第一階段要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。一階段可能包括下面這些驗(yàn)證點(diǎn):

·是否以魔數(shù)0xCAFEBABE開(kāi)頭。

·主、次版本號(hào)是否在當(dāng)前Java虛擬機(jī)接受范圍之內(nèi)。

……

該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合描述一個(gè)Java類(lèi)型信息的要求。

2.元數(shù)據(jù)驗(yàn)證

第二階段是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合《Java語(yǔ)言規(guī)范》的要求,這個(gè)階段可能包括的驗(yàn)證點(diǎn)如下:

·這個(gè)類(lèi)是否有父類(lèi)(除了java.lang.Object之外,所有的類(lèi)都應(yīng)當(dāng)有父類(lèi))。

·這個(gè)類(lèi)的父類(lèi)是否繼承了不允許被繼承的類(lèi)(被final修飾的類(lèi))。

……

第二階段的主要目的是對(duì)類(lèi)的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn),保證不存在與《Java語(yǔ)言規(guī)范》定義相悖的元數(shù)據(jù)信息。

3.字節(jié)碼驗(yàn)證

第三階段是整個(gè)驗(yàn)證過(guò)程中最復(fù)雜的一個(gè)階段,主要目的是通過(guò)數(shù)據(jù)流分析和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類(lèi)型校驗(yàn)完畢以后,這階段就要對(duì)類(lèi)的方法體(Class文件中的Code屬性)進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類(lèi)的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為,例如:

·保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類(lèi)型與指令代碼序列都能配合工作,例如不會(huì)出現(xiàn)類(lèi)似于“在操作棧放置了一個(gè)int類(lèi)型的數(shù)據(jù),使用時(shí)卻按long類(lèi)型來(lái)加載入本地變量表中”這樣的情況。

·保證任何跳轉(zhuǎn)指令都不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上。

……

4.符號(hào)引用驗(yàn)證

最后一個(gè)階段的校驗(yàn)行為發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生。

本階段通常需要校驗(yàn)下列內(nèi)容:

·符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類(lèi)。

·在指定類(lèi)中是否存在符合方法的字段描述符及簡(jiǎn)單名稱(chēng)所描述的方法和字段。

……

符號(hào)引用驗(yàn)證的主要目的是確保解析行為能正常執(zhí)行,如果無(wú)法通過(guò)符號(hào)引用驗(yàn)證,Java虛擬機(jī)將會(huì)拋出一個(gè)java.lang.IncompatibleClassChangeError的子類(lèi)異常,典型的如:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

7.3.3 準(zhǔn)備

準(zhǔn)備階段是正式為類(lèi)中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段,從概念上講,這些變量所使用的內(nèi)存都應(yīng)當(dāng)在方法區(qū)中進(jìn)行分配,但必須注意到方法區(qū)本身是一個(gè)邏輯上的區(qū)域,在JDK 7及之前,HotSpot使用永久代來(lái)實(shí)現(xiàn)方法區(qū)時(shí),實(shí)現(xiàn)是完全符合這種邏輯概念的;而在JDK 8及之后,類(lèi)變量則會(huì)隨著Class對(duì)象一起存放在Java堆中,這時(shí)候“類(lèi)變量在方法區(qū)”就完全是一種對(duì)邏輯概念的表述了,關(guān)于這部分內(nèi)容,筆者已在4.3.1節(jié)介紹并且驗(yàn)證過(guò)。

關(guān)于準(zhǔn)備階段,首先這時(shí)候進(jìn)行內(nèi)存分配的僅包括類(lèi)變量,而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中。其次是這里所說(shuō)的初始值“通常情況”下是數(shù)據(jù)類(lèi)型的零值,假設(shè)一個(gè)類(lèi)變量的定義為:

那變量value在準(zhǔn)備階段過(guò)后的初始值為0而不是123,因?yàn)檫@時(shí)尚未開(kāi)始執(zhí)行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類(lèi)構(gòu)造器()方法之中,所以把value賦值為123的動(dòng)作要到類(lèi)的初始化階段才會(huì)被執(zhí)行。

上面提到在“通常情況”下初始值是零值,那言外之意是相對(duì)的會(huì)有某些“特殊情況”:如果類(lèi)字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量值就會(huì)被初始化為ConstantValue屬性所指定的初始值,假設(shè)上面類(lèi)變量value的定義修改為:

編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)Con-stantValue的設(shè)置將value賦值為123。

7.3.4 解析

解析階段是Java虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。

·符號(hào)引用(Symbolic References):符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。

·直接引用(Direct References):直接引用是可以直接指向目標(biāo)的指針、相對(duì)偏移量或者是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局直接相關(guān)的,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在虛擬機(jī)的內(nèi)存中存在。

解析動(dòng)作主要針對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型、方法句柄和調(diào)用點(diǎn)限定符這7類(lèi)符號(hào)引用進(jìn)行,分別對(duì)應(yīng)于常量池的CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info 8種常量類(lèi)型。

7.3.5 初始化

類(lèi)的初始化階段是類(lèi)加載過(guò)程的最后一個(gè)步驟,之前介紹的幾個(gè)類(lèi)加載的動(dòng)作里,除了在加載階段用戶(hù)應(yīng)用程序可以通過(guò)自定義類(lèi)加載器的方式局部參與外,其余動(dòng)作都完全由Java虛擬機(jī)來(lái)主導(dǎo)控制。直到初始化階段,Java虛擬機(jī)才真正開(kāi)始執(zhí)行類(lèi)中編寫(xiě)的Java程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。

進(jìn)行準(zhǔn)備階段時(shí),變量已經(jīng)賦過(guò)一次系統(tǒng)要求的初始零值,而在初始化階段,則會(huì)根據(jù)程序員通過(guò)程序編碼制定的主觀計(jì)劃去初始化類(lèi)變量和其他資源。

類(lèi)的初始化可以從另外一種更直接的形式來(lái)表達(dá):初始化階段就是執(zhí)行類(lèi)構(gòu)造器()方法的過(guò)程。()并不是程序員在Java代碼中直接編寫(xiě)的方法,它是Javac編譯器的自動(dòng)生成物,但我們非常有必要了解這個(gè)方法具體是如何產(chǎn)生的,以及()方法執(zhí)行過(guò)程中各種可能會(huì)影響程序運(yùn)行行為的細(xì)節(jié),這部分比起其他類(lèi)加載過(guò)程更貼近于普通的程序開(kāi)發(fā)人員的實(shí)際工作。

·()方法是由編譯器自動(dòng)收集類(lèi)中的所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊(static{}塊)中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序決定的。靜態(tài)語(yǔ)句塊只能訪問(wèn)在靜態(tài)句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語(yǔ)句塊可以復(fù)制,但不能訪問(wèn)。

·()方法與類(lèi)的構(gòu)造函數(shù)(即在虛擬機(jī)視角中的實(shí)例構(gòu)造器()方法)不同,它不需要顯式地調(diào)用父類(lèi)構(gòu)造器,Java虛擬機(jī)會(huì)保證在子類(lèi)的()方法執(zhí)行前,父類(lèi)的()方法已經(jīng)執(zhí)行完畢。因此在Java虛擬機(jī)中第一個(gè)被執(zhí)行的()方法的類(lèi)型肯定是java.lang.Object。

·由于父類(lèi)的()方法先執(zhí)行,也就意味著父類(lèi)中定義的靜態(tài)語(yǔ)句塊要優(yōu)先于子類(lèi)的變量賦值操作。

·()方法對(duì)于類(lèi)或接口來(lái)說(shuō)并不是必需的,如果一個(gè)類(lèi)中沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)變量的賦值操作,那么編譯器可以不為這個(gè)類(lèi)生成()方法。

·接口中不能使用靜態(tài)語(yǔ)句塊,但仍然有變量初始化的賦值操作,因此接口與類(lèi)一樣都會(huì)生成()方法。但接口與類(lèi)不同的是,執(zhí)行接口的()方法不需要先執(zhí)行父接口的()方法,因?yàn)橹挥挟?dāng)父接口中定義的變量被使用時(shí),父接口才會(huì)被初始化。此外,接口的實(shí)現(xiàn)類(lèi)在初始化時(shí)也一樣不會(huì)執(zhí)行接口的()方法。

·Java虛擬機(jī)必須保證一個(gè)類(lèi)的()方法在多線(xiàn)程環(huán)境中被正確地加鎖同步,如果多個(gè)線(xiàn)程同時(shí)去初始化一個(gè)類(lèi),那么只會(huì)有其中一個(gè)線(xiàn)程去執(zhí)行這個(gè)類(lèi)的()方法,其他線(xiàn)程都需要阻塞等待,直到活動(dòng)線(xiàn)程執(zhí)行完畢()方法。如果在一個(gè)類(lèi)的()方法中有耗時(shí)很長(zhǎng)的操作,那就可能造成多個(gè)進(jìn)程阻塞[插圖],在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的。

7.4 類(lèi)加載器

Java虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)有意把類(lèi)加載階段中的“通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取描述該類(lèi)的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到Java虛擬機(jī)外部去實(shí)現(xiàn),以便讓?xiě)?yīng)用程序自己決定如何去獲取所需的類(lèi)。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼被稱(chēng)為**“類(lèi)加載器”(Class Loader)**。

7.4.1 類(lèi)與類(lèi)加載器

類(lèi)加載器雖然只用于實(shí)現(xiàn)類(lèi)的加載動(dòng)作,但它在Java程序中起到的作用卻遠(yuǎn)超類(lèi)加載階段。對(duì)于任意一個(gè)類(lèi),都必須由加載它的類(lèi)加載器和這個(gè)類(lèi)本身一起共同確立其在Java虛擬機(jī)中的唯一性,每一個(gè)類(lèi)加載器,都擁有一個(gè)獨(dú)立的類(lèi)名稱(chēng)空間。即使兩個(gè)類(lèi)來(lái)源于同一個(gè)class文件,被同一個(gè)java虛擬機(jī)加載,只要加載它們的類(lèi)加載器不同,那這兩個(gè)類(lèi)就必定不相等。

7.4.2 雙親委派模型

站在Java虛擬機(jī)的角度來(lái)看,只存在兩種不同的類(lèi)加載器:

一種是啟動(dòng)類(lèi)加載器(BootstrapClassLoader),這個(gè)類(lèi)加載器使用C++語(yǔ)言實(shí)現(xiàn),是虛擬機(jī)自身的一部分;

另外一種就是其他所有的類(lèi)加載器,這些類(lèi)加載器都由Java語(yǔ)言實(shí)現(xiàn),獨(dú)立存在于虛擬機(jī)外部,并且全都繼承自抽象類(lèi)java.lang.ClassLoader。

站在Java開(kāi)發(fā)人員的角度來(lái)看,類(lèi)加載器就應(yīng)當(dāng)劃分得更細(xì)致一些:

自JDK 1.2以來(lái),Java一直保持著三層類(lèi)加載器、雙親委派的類(lèi)加載架構(gòu),盡管這套架構(gòu)在Java模塊化系統(tǒng)出現(xiàn)后有了一些調(diào)整變動(dòng),但依然未改變其主體結(jié)構(gòu),我們將在7.5節(jié)中專(zhuān)門(mén)討論模塊化系統(tǒng)下的類(lèi)加載器。

對(duì)于這個(gè)時(shí)期的Java應(yīng)用,絕大多數(shù)Java程序都會(huì)使用到以下3個(gè)系統(tǒng)提供的類(lèi)加載器來(lái)進(jìn)行加載:

·啟動(dòng)類(lèi)加載器(Bootstrap Class Loader):該類(lèi)加載器負(fù)責(zé)加載存放在<JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數(shù)所指定的路徑中存放的,而且是Java虛擬機(jī)能夠識(shí)別的類(lèi)庫(kù)加載到虛擬機(jī)的內(nèi)存中。

·擴(kuò)展類(lèi)加載器(Extension Class Loader):這個(gè)類(lèi)加載器是在類(lèi)sun.misc.Launcher$ExtClassLoader中以Java代碼的形式實(shí)現(xiàn)的。它負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中所有的類(lèi)庫(kù)。

·應(yīng)用程序類(lèi)加載器(Application Class Loader):這個(gè)類(lèi)加載器由sun.misc.Launcher$AppClassLoader來(lái)實(shí)現(xiàn)。由于應(yīng)用程序類(lèi)加載器是ClassLoader類(lèi)中的getSystem-ClassLoader()方法的返回值,所以有些場(chǎng)合中也稱(chēng)它為“系統(tǒng)類(lèi)加載器”。它負(fù)責(zé)加載用戶(hù)類(lèi)路徑(ClassPath)上所有的類(lèi)庫(kù),開(kāi)發(fā)者同樣可以直接在代碼中使用這個(gè)類(lèi)加載器。如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器。

圖7-2中展示的各種類(lèi)加載器之間的層次關(guān)系被稱(chēng)為類(lèi)加載器的“雙親委派模型(Parents Delegation Model)”。雙親委派模型要求除了頂層的啟動(dòng)類(lèi)加載器外,其余的類(lèi)加載器都應(yīng)有自己的父類(lèi)加載器。不過(guò)這里類(lèi)加載器之間的父子關(guān)系一般不是以繼承(Inheritance)的關(guān)系來(lái)實(shí)現(xiàn)的,而是通常使用組合(Composition)關(guān)系來(lái)復(fù)用父加載器的代碼

雙親委派模型的工作過(guò)程是:如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求委派給父類(lèi)加載器去完成,每一個(gè)層次的類(lèi)加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到最頂層的啟動(dòng)類(lèi)加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類(lèi))時(shí),子加載器才會(huì)嘗試自己去完成加載。

使用雙親委派模型來(lái)組織類(lèi)加載器之間的關(guān)系,一個(gè)顯而易見(jiàn)的好處就是Java中的類(lèi)隨著它的類(lèi)加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系

雙親委派模型對(duì)于保證Java程序的穩(wěn)定運(yùn)作極為重要,但它的實(shí)現(xiàn)卻異常簡(jiǎn)單,用以實(shí)現(xiàn)雙親委派的代碼只有短短十余行,全部集中在java.lang.ClassLoader的loadClass()方法之中,如代碼清單7-10所示。

這段代碼的邏輯清晰易懂:先檢查請(qǐng)求加載的類(lèi)型是否已經(jīng)被加載過(guò),若沒(méi)有則調(diào)用父加載器的loadClass()方法,若父加載器為空則默認(rèn)使用啟動(dòng)類(lèi)加載器作為父加載器。假如父類(lèi)加載器加載失敗,拋出ClassNotFoundException異常的話(huà),才調(diào)用自己的findClass()方法嘗試進(jìn)行加載。

7.4.3 破壞雙親委派模型

雙親委派模型并不是一個(gè)具有強(qiáng)制性約束的模型,而是Java設(shè)計(jì)者推薦給開(kāi)發(fā)者們的類(lèi)加載器實(shí)現(xiàn)方式。

在Java的世界中大部分的類(lèi)加載器都遵循這個(gè)模型,但也有例外的情況,直到Java模塊化出現(xiàn)為止,雙親委派模型主要出現(xiàn)過(guò)3次較大規(guī)模“被破壞”的情況:

第一次“被破壞”:

由于雙親委派模型在JDK 1.2之后才被引入,但是類(lèi)加載器的概念和抽象類(lèi)java.lang.ClassLoader則在Java的第一個(gè)版本中就已經(jīng)存在,面對(duì)已經(jīng)存在的用戶(hù)自定義類(lèi)加載器的代碼,無(wú)法再以技術(shù)手段避免loadClass()被子類(lèi)覆蓋的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一個(gè)新的protected方法findClass(),并引導(dǎo)用戶(hù)編寫(xiě)的類(lèi)加載邏輯時(shí)盡可能去重寫(xiě)這個(gè)方法,而不是在loadClass()中編寫(xiě)代碼。上節(jié)我們已經(jīng)分析過(guò)loadClass()方法,雙親委派的具體邏輯就實(shí)現(xiàn)在這里面,按照l(shuí)oadClass()方法的邏輯,如果父類(lèi)加載失敗,會(huì)自動(dòng)調(diào)用自己的findClass()方法來(lái)完成加載,這樣既不影響用戶(hù)按照自己的意愿去加載類(lèi),又可以保證新寫(xiě)出來(lái)的類(lèi)加載器是符合雙親委派規(guī)則的。

第二次“被破壞”:

是由這個(gè)模型自身的缺陷導(dǎo)致的,雙親委派很好地解決了各個(gè)類(lèi)加載器協(xié)作時(shí)基礎(chǔ)類(lèi)型的一致性問(wèn)題(越基礎(chǔ)的類(lèi)由越上層的加載器進(jìn)行加載),基礎(chǔ)類(lèi)型之所以被稱(chēng)為“基礎(chǔ)”,是因?yàn)樗鼈兛偸亲鳛楸挥脩?hù)代碼繼承、調(diào)用的API存在,但程序設(shè)計(jì)往往沒(méi)有絕對(duì)不變的完美規(guī)則,如果有基礎(chǔ)類(lèi)型又要調(diào)用回用戶(hù)的代碼,那該怎么辦呢?

為了解決這個(gè)困境,Java的設(shè)計(jì)團(tuán)隊(duì)只好引入了一個(gè)不太優(yōu)雅的設(shè)計(jì):線(xiàn)程上下文類(lèi)加載器(Thread Context ClassLoader)。這個(gè)類(lèi)加載器可以通過(guò)java.lang.Thread類(lèi)的setContext-ClassLoader()方法進(jìn)行設(shè)置,如果創(chuàng)建線(xiàn)程時(shí)還未設(shè)置,它將會(huì)從父線(xiàn)程中繼承一個(gè),如果在應(yīng)用程序的全局范圍內(nèi)都沒(méi)有設(shè)置過(guò)的話(huà),那這個(gè)類(lèi)加載器默認(rèn)就是應(yīng)用程序類(lèi)加載器。

第三次“被破壞”:

是由于用戶(hù)對(duì)程序動(dòng)態(tài)性的追求而導(dǎo)致的,這里所說(shuō)的“動(dòng)態(tài)性”指的是一些非常“熱”門(mén)的名詞:代碼熱替換(Hot Swap)、模塊熱部署(HotDeployment)等。

例如OSGi實(shí)現(xiàn)模塊化熱部署的關(guān)鍵是它自定義的類(lèi)加載器機(jī)制的實(shí)現(xiàn),每一個(gè)程序模塊(OSGi中稱(chēng)為Bundle)都有一個(gè)自己的類(lèi)加載器,當(dāng)需要更換一個(gè)Bundle時(shí),就把Bundle連同類(lèi)加載器一起換掉以實(shí)現(xiàn)代碼的熱替換。在OSGi環(huán)境下,類(lèi)加載器不再雙親委派模型推薦的樹(shù)狀結(jié)構(gòu),而是進(jìn)一步發(fā)展為更加復(fù)雜的網(wǎng)狀結(jié)構(gòu),當(dāng)收到類(lèi)加載請(qǐng)求時(shí),OSGi將按照下面的順序進(jìn)行類(lèi)搜索:

1)將以java.*開(kāi)頭的類(lèi),委派給父類(lèi)加載器加載。

2)否則,將委派列表名單內(nèi)的類(lèi),委派給父類(lèi)加載器加載。

3)否則,將Import列表中的類(lèi),委派給Export這個(gè)類(lèi)的Bundle的類(lèi)加載器加載。

4)否則,查找當(dāng)前Bundle的ClassPath,使用自己的類(lèi)加載器加載。

5)否則,查找類(lèi)是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類(lèi)加載器加載。

6)否則,查找Dynamic Import列表的Bundle,委派給對(duì)應(yīng)Bundle的類(lèi)加載器加載。

7)否則,類(lèi)查找失敗。

7.5 Java模塊化系統(tǒng)

在JDK 9中引入的Java模塊化系統(tǒng)(Java Platform Module System,JPMS)是對(duì)Java技術(shù)的一次重要升級(jí),為了能夠?qū)崿F(xiàn)模塊化的關(guān)鍵目標(biāo)——可配置的封裝隔離機(jī)制,Java虛擬機(jī)對(duì)類(lèi)加載架構(gòu)也做出了相應(yīng)的變動(dòng)調(diào)整,才使模塊化系統(tǒng)得以順利地運(yùn)作。

第8章 虛擬機(jī)字節(jié)碼執(zhí)行引擎

8.1 概述

在《Java虛擬機(jī)規(guī)范》中制定了Java虛擬機(jī)字節(jié)碼執(zhí)行引擎的概念模型,在不同的虛擬機(jī)實(shí)現(xiàn)中,執(zhí)行引擎在執(zhí)行字節(jié)碼的時(shí)候,通常會(huì)有解釋執(zhí)行(通過(guò)解釋器執(zhí)行)和編譯執(zhí)行(通過(guò)即時(shí)編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇,也可能兩者兼?zhèn)涞取?/p>

但從外觀上來(lái)看,所有的Java虛擬機(jī)的執(zhí)行引擎輸入、輸出都是一致的:輸入的是字節(jié)碼二進(jìn)制流,處理過(guò)程是字節(jié)碼解析執(zhí)行的等效過(guò)程,輸出的是執(zhí)行結(jié)果,本章將主要從概念模型的角度來(lái)講解虛擬機(jī)的方法調(diào)用和字節(jié)碼執(zhí)行。

8.2 運(yùn)行時(shí)棧幀結(jié)構(gòu)

Java虛擬機(jī)以方法作為最基本的執(zhí)行單元,**“棧幀”(Stack Frame)**則是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行背后的數(shù)據(jù)結(jié)構(gòu),它也是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的虛擬機(jī)棧(VirtualMachine Stack)的棧元素。

棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等信息。

每一個(gè)方法從調(diào)用開(kāi)始至執(zhí)行結(jié)束的過(guò)程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過(guò)程。

8.2.1 局部變量表

局部變量表(Local Variables Table)是一組變量值的存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在Java程序被編譯為Class文件時(shí),就在方法的Code屬性的max_locals數(shù)據(jù)項(xiàng)中確定了該方法所需分配的局部變量表的最大容量。

局部變量表的容量以變量槽(Variable Slot)為最小單位,《Java虛擬機(jī)規(guī)范》中并沒(méi)有明確指出一個(gè)變量槽應(yīng)占用的內(nèi)存空間大小,只是很有導(dǎo)向性地說(shuō)到每個(gè)變量槽都應(yīng)該能存放一個(gè)boolean、byte、char、short、int、float、reference或returnAddress類(lèi)型的數(shù)據(jù)。

8.2.2 操作數(shù)棧

操作數(shù)棧(Operand Stack)也常被稱(chēng)為操作棧,它是一個(gè)后入先出(Last In First Out,LIFO)棧。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時(shí)候被寫(xiě)入到Code屬性的max_stacks數(shù)據(jù)項(xiàng)之中。

8.2.3 動(dòng)態(tài)連接

每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接(Dynamic Linking)

通過(guò)第6章的講解,我們知道Class文件的常量池中存有大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池里指向方法的符號(hào)引用作為參數(shù)。這些符號(hào)引用一部分會(huì)在類(lèi)加載階段或者第一次使用的時(shí)候就被轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化被稱(chēng)為靜態(tài)解析。另外一部分將在每一次運(yùn)行期間都轉(zhuǎn)化為直接引用,這部分就稱(chēng)為動(dòng)態(tài)連接

8.2.4 方法返回地址

當(dāng)一個(gè)方法開(kāi)始執(zhí)行后,只有兩種方式退出這個(gè)方法:

第一種方式是執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱(chēng)為調(diào)用者或者主調(diào)方法),方法是否有返回值以及返回值的類(lèi)型將根據(jù)遇到何種方法返回指令來(lái)決定,這種退出方法的方式稱(chēng)為**“正常調(diào)用完成”(Normal Method Invocation Completion)**。

另外一種退出方式是在方法執(zhí)行的過(guò)程中遇到了異常,并且這個(gè)異常沒(méi)有在方法體內(nèi)得到妥善處理。無(wú)論是Java虛擬機(jī)內(nèi)部產(chǎn)生的異常,還是代碼中使用athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒(méi)有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法退出,這種退出方法的方式稱(chēng)為**“異常調(diào)用完成(Abrupt Method Invocation Completion)”**。一個(gè)方法使用異常完成出口的方式退出,是不會(huì)給它的上層調(diào)用者提供任何返回值的。

一般來(lái)說(shuō),方法正常退出時(shí),主調(diào)方法的PC計(jì)數(shù)器的值就可以作為返回地址,棧幀中很可能會(huì)保存這個(gè)計(jì)數(shù)器值。而方法異常退出時(shí),返回地址是要通過(guò)異常處理器表來(lái)確定的,棧幀中就一般不會(huì)保存這部分信息。

8.2.5 附加信息

《Java虛擬機(jī)規(guī)范》允許虛擬機(jī)實(shí)現(xiàn)增加一些規(guī)范里沒(méi)有描述的信息到棧幀之中,例如與調(diào)試、性能收集相關(guān)的信息,這部分信息完全取決于具體的虛擬機(jī)實(shí)現(xiàn),這里不再詳述。在討論概念時(shí),一般會(huì)把動(dòng)態(tài)連接、方法返回地址與其他附加信息全部歸為一類(lèi),稱(chēng)為棧幀信息。

8.3 方法調(diào)用

方法調(diào)用并不等同于方法中的代碼被執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個(gè)方法),暫時(shí)還未涉及方法內(nèi)部的具體運(yùn)行過(guò)程。

8.3.1 解析

所有方法調(diào)用的目標(biāo)方法在Class文件里面都是一個(gè)常量池中的符號(hào)引用,在類(lèi)加載的解析階段,會(huì)將其中的一部分符號(hào)引用轉(zhuǎn)化為直接引用,這種解析能夠成立的前提是:方法在程序真正運(yùn)行之前就有一個(gè)可確定的調(diào)用版本,并且這個(gè)方法的調(diào)用版本在運(yùn)行期是不可改變的。換句話(huà)說(shuō),調(diào)用目標(biāo)在程序代碼寫(xiě)好、編譯器進(jìn)行編譯那一刻就已經(jīng)確定下來(lái)。這類(lèi)方法的調(diào)用被稱(chēng)為解析(Resolution)

在Java語(yǔ)言中符合“編譯期可知,運(yùn)行期不可變”這個(gè)要求的方法,主要有靜態(tài)方法和私有方法兩大類(lèi),前者與類(lèi)型直接關(guān)聯(lián),后者在外部不可被訪問(wèn),這兩種方法各自的特點(diǎn)決定了它們都不可能通過(guò)繼承或別的方式重寫(xiě)出其他版本,因此它們都適合在類(lèi)加載階段進(jìn)行解析。

調(diào)用不同類(lèi)型的方法,字節(jié)碼指令集里設(shè)計(jì)了不同的指令。在Java虛擬機(jī)支持以下5條方法調(diào)用字節(jié)碼指令,分別是:

·invokestatic。用于調(diào)用靜態(tài)方法。

·invokespecial。用于調(diào)用實(shí)例構(gòu)造器()方法、私有方法和父類(lèi)中的方法。

·invokevirtual。用于調(diào)用所有的虛方法。

·invokeinterface。用于調(diào)用接口方法,會(huì)在運(yùn)行時(shí)再確定一個(gè)實(shí)現(xiàn)該接口的對(duì)象。

·invokedynamic。先在運(yùn)行時(shí)動(dòng)態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法,然后再執(zhí)行該方法。

前面4條調(diào)用指令,分派邏輯都固化在Java虛擬機(jī)內(nèi)部,而invokedynamic指令的分派邏輯是由用戶(hù)設(shè)定的引導(dǎo)方法來(lái)決定的。

只要能被invokestatic和invokespecial指令調(diào)用的方法,都可以在解析階段中確定唯一的調(diào)用版本,Java語(yǔ)言里符合這個(gè)條件的方法共有靜態(tài)方法、私有方法、實(shí)例構(gòu)造器、父類(lèi)方法4種,再加上被final修飾的方法(盡管它使用invokevirtual指令調(diào)用),這5種方法調(diào)用會(huì)在類(lèi)加載的時(shí)候就可以把符號(hào)引用解析為該方法的直接引用。這些方法統(tǒng)稱(chēng)為“非虛方法”(Non-VirtualMethod),與之相反,其他方法就被稱(chēng)為**“虛方法”(Virtual Method)。Java中的非虛方法除了使用invokestatic、invokespecial調(diào)用的方法之外還有一種,就是被final**修飾的實(shí)例方法。雖然由于歷史設(shè)計(jì)的原因,final方法是使用invokevirtual指令來(lái)調(diào)用的。

解析調(diào)用一定是個(gè)靜態(tài)的過(guò)程,在編譯期間就完全確定,在類(lèi)加載的解析階段就會(huì)把涉及的符號(hào)引用全部轉(zhuǎn)變?yōu)槊鞔_的直接引用,不必延遲到運(yùn)行期再去完成。而另一種主要的方法調(diào)用形式:分派(Dispatch)調(diào)用則要復(fù)雜許多,它可能是靜態(tài)的也可能是動(dòng)態(tài)的,按照分派依據(jù)的宗量數(shù)可分為單分派和多分派。這兩類(lèi)分派方式兩兩組合就構(gòu)成了靜態(tài)單分派、靜態(tài)多分派、動(dòng)態(tài)單分派、動(dòng)態(tài)多分派4種分派組合情況,下面我們來(lái)看看虛擬機(jī)中的方法分派是如何進(jìn)行的。

8.3.2 分派

本節(jié)講解的分派調(diào)用過(guò)程將會(huì)揭示多態(tài)性特征的一些最基本的體現(xiàn),如“重載”和“重寫(xiě)”在Java虛擬機(jī)之中是如何實(shí)現(xiàn)的,這里的實(shí)現(xiàn)當(dāng)然不是語(yǔ)法上該如何寫(xiě),我們關(guān)心的依然是虛擬機(jī)如何確定正確的目標(biāo)方法。

1.靜態(tài)分派

運(yùn)行結(jié)果:

把上面代碼中的“Human”稱(chēng)為變量的**“靜態(tài)類(lèi)型”(Static Type),或者叫“外觀類(lèi)型”(Apparent Type),后面的“Man”則被稱(chēng)為變量的“實(shí)際類(lèi)型”(Actual Type)或者叫“運(yùn)行時(shí)類(lèi)型”(Runtime Type)。靜態(tài)類(lèi)型和實(shí)際類(lèi)型在程序中都可能會(huì)發(fā)生變化,區(qū)別是靜態(tài)類(lèi)型的變化僅僅在使用時(shí)發(fā)生,變量本身的靜態(tài)類(lèi)型不會(huì)被改變,并且最終的靜態(tài)類(lèi)型是在編譯期可知的;而實(shí)際類(lèi)型變化的結(jié)果在運(yùn)行期才可確定,編譯器在編譯程序的時(shí)候并不知道一個(gè)對(duì)象的實(shí)際類(lèi)型是什么。**

**虛擬機(jī)在重載時(shí)是通過(guò)參數(shù)的靜態(tài)類(lèi)型而不是實(shí)際類(lèi)型作為判定依據(jù)的。**由于靜態(tài)類(lèi)型在編譯器可知,所以在編譯階段,編譯器就根據(jù)參數(shù)的靜態(tài)類(lèi)型決定了會(huì)使用哪個(gè)重載版本。

所有依賴(lài)靜態(tài)類(lèi)型來(lái)決定方法執(zhí)行版本的分派動(dòng)作,都稱(chēng)為靜態(tài)分派。靜態(tài)分派發(fā)生在編譯階段,靜態(tài)分派的最典型應(yīng)用表現(xiàn)就是方法重載。

2.動(dòng)態(tài)分派

運(yùn)行結(jié)果:

根據(jù)《Java虛擬機(jī)規(guī)范》,invokevirtual指令的運(yùn)行時(shí)解析過(guò)程大致分為以下幾步:

1)找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類(lèi)型,記作C。

2)如果在類(lèi)型C中找到與常量中的描述符和簡(jiǎn)單名稱(chēng)都相符的方法,則進(jìn)行訪問(wèn)權(quán)限校驗(yàn),如果通過(guò)則返回這個(gè)方法的直接引用,查找過(guò)程結(jié)束;不通過(guò)則返回java.lang.IllegalAccessError異常。

3)否則,按照繼承關(guān)系從下往上依次對(duì)C的各個(gè)父類(lèi)進(jìn)行第二步的搜索和驗(yàn)證過(guò)程。

4)如果始終沒(méi)有找到合適的方法,則拋出java.lang.AbstractMethodError異常。

正是因?yàn)閕nvokevirtual指令執(zhí)行的第一步就是在運(yùn)行期確定接收者的實(shí)際類(lèi)型,所以?xún)纱握{(diào)用中的invokevirtual指令并不是把常量池中方法的符號(hào)引用解析到直接引用上就結(jié)束了,還會(huì)根據(jù)方法接收者的實(shí)際類(lèi)型來(lái)選擇方法版本,這個(gè)過(guò)程就是Java語(yǔ)言中方法重寫(xiě)的本質(zhì)。在運(yùn)行期根據(jù)實(shí)際類(lèi)型確定方法執(zhí)行版本的分派過(guò)程稱(chēng)為動(dòng)態(tài)分派。

既然這種多態(tài)性的根源在于虛方法調(diào)用指令invokevirtual的執(zhí)行邏輯,那自然我們得出的結(jié)論就只會(huì)對(duì)方法有效,對(duì)字段是無(wú)效的,因?yàn)樽侄尾皇褂眠@條指令。事實(shí)上,在Java里面只有虛方法存在,字段永遠(yuǎn)不可能是虛的,換句話(huà)說(shuō),字段永遠(yuǎn)不參與多態(tài),哪個(gè)類(lèi)的方法訪問(wèn)某個(gè)名字的字段時(shí),該名字指的就是這個(gè)類(lèi)能看到的那個(gè)字段。當(dāng)子類(lèi)聲明了與父類(lèi)同名的字段時(shí),雖然在子類(lèi)的內(nèi)存中兩個(gè)字段都會(huì)存在,但是子類(lèi)的字段會(huì)遮蔽父類(lèi)的同名字段。

3.單分派與多分派

方法的接收者與方法的參數(shù)統(tǒng)稱(chēng)為方法的宗量。

根據(jù)分派基于多少種宗量,可以將分派劃分為單分派和多分派兩種。單分派是根據(jù)一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇,多分派則是根據(jù)多于一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇。

總結(jié)一句:如今(直至本書(shū)編寫(xiě)的Java 12和預(yù)覽版的Java 13)的Java語(yǔ)言是一門(mén)靜態(tài)多分派、動(dòng)態(tài)單分派的語(yǔ)言。

4.虛擬機(jī)動(dòng)態(tài)分派的實(shí)現(xiàn)

動(dòng)態(tài)分派是執(zhí)行非常頻繁的動(dòng)作,而且動(dòng)態(tài)分派的方法版本選擇過(guò)程需要運(yùn)行時(shí)在接收者類(lèi)型的方法元數(shù)據(jù)中搜索合適的目標(biāo)方法,因此,Java虛擬機(jī)實(shí)現(xiàn)基于執(zhí)行性能的考慮,真正運(yùn)行時(shí)一般不會(huì)如此頻繁地去反復(fù)搜索類(lèi)型元數(shù)據(jù)。面對(duì)這種情況,一種基礎(chǔ)而且常見(jiàn)的優(yōu)化手段是為類(lèi)型在方法區(qū)中建立一個(gè)虛方法表(Virtual Method Table,也稱(chēng)為vtable,與此對(duì)應(yīng)的,在invokeinterface執(zhí)行時(shí)也會(huì)用到接口方法表——Interface Method Table,簡(jiǎn)稱(chēng)itable),使用虛方法表索引來(lái)代替元數(shù)據(jù)查找以提高性能。虛方法表中存放著各個(gè)方法的實(shí)際入口地址。

8.4 動(dòng)態(tài)類(lèi)型語(yǔ)言支持

JDK 7的發(fā)布的字節(jié)碼首位新成員——invokedynamic指令。這條新增加的指令是JDK 7的項(xiàng)目目標(biāo):實(shí)現(xiàn)動(dòng)態(tài)類(lèi)型語(yǔ)言(Dynamically Typed Language)支持而進(jìn)行的改進(jìn)之一,也是為JDK 8里可以順利實(shí)現(xiàn)Lambda表達(dá)式而做的技術(shù)儲(chǔ)備。

8.4.1 動(dòng)態(tài)類(lèi)型語(yǔ)言

**動(dòng)態(tài)類(lèi)型語(yǔ)言的關(guān)鍵特征是它的類(lèi)型檢查的主體過(guò)程是在運(yùn)行期而不是編譯期進(jìn)行的,**常用的包括:APL、Clojure、Erlang、Groovy、JavaScript、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk、Tcl。

那相對(duì)地,在編譯期就進(jìn)行類(lèi)型檢查過(guò)程的語(yǔ)言,譬如C++和Java等就是最常用的靜態(tài)類(lèi)型語(yǔ)言。

8.5 基于棧的字節(jié)碼解釋執(zhí)行引擎

第9章 類(lèi)加載及執(zhí)行子系統(tǒng)的案例與實(shí)戰(zhàn)

第四部分 程序編譯與代碼優(yōu)化

第10章 前端編譯與優(yōu)化

10.1 概述

在Java技術(shù)下談“編譯期”而沒(méi)有具體上下文語(yǔ)境的話(huà),其實(shí)是一句很含糊的表述,因?yàn)樗赡苁侵敢粋€(gè)前端編譯器(叫“編譯器的前端”更準(zhǔn)確一些)把*.java文件轉(zhuǎn)變成*.class文件的過(guò)程;也可能是指Java虛擬機(jī)的即時(shí)編譯器(常稱(chēng)JIT編譯器,Just In Time Compiler)運(yùn)行期把字節(jié)碼轉(zhuǎn)變成本地機(jī)器碼的過(guò)程;還可能是指使用靜態(tài)的提前編譯器(常稱(chēng)AOT編譯器,Ahead Of Time Compiler)直接把程序編譯成與目標(biāo)機(jī)器指令集相關(guān)的二進(jìn)制代碼的過(guò)程。

下面筆者列舉了這3類(lèi)編譯過(guò)程里一些比較有代表性的編譯器產(chǎn)品:

·前端編譯器:JDK的Javac、Eclipse JDT中的增量式編譯器(ECJ)。

·即時(shí)編譯器:HotSpot虛擬機(jī)的C1、C2編譯器,Graal編譯器。

·提前編譯器:JDK的Jaotc、GNU Compiler for the Java(GCJ)、Excelsior JET。

10.2 Javac編譯器

10.2.1 Javac的源碼與調(diào)試

從Javac代碼的總體結(jié)構(gòu)來(lái)看,編譯過(guò)程大致可以分為1個(gè)準(zhǔn)備過(guò)程和3個(gè)處理過(guò)程,它們分別如下所示。

1)準(zhǔn)備過(guò)程:初始化插入式注解處理器。

2)解析與填充符號(hào)表過(guò)程,包括:

·詞法、語(yǔ)法分析。將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記集合,構(gòu)造出抽象語(yǔ)法樹(shù)。

·填充符號(hào)表。產(chǎn)生符號(hào)地址和符號(hào)信息。

3)插入式注解處理器的注解處理過(guò)程:插入式注解處理器的執(zhí)行階段,本章的實(shí)戰(zhàn)部分會(huì)設(shè)計(jì)一個(gè)插入式注解處理器來(lái)影響Javac的編譯行為。

4)分析與字節(jié)碼生成過(guò)程,包括:

·標(biāo)注檢查。對(duì)語(yǔ)法的靜態(tài)信息進(jìn)行檢查。

·數(shù)據(jù)流及控制流分析。對(duì)程序動(dòng)態(tài)運(yùn)行過(guò)程進(jìn)行檢查。

·解語(yǔ)法糖。將簡(jiǎn)化代碼編寫(xiě)的語(yǔ)法糖還原為原有的形式。

·字節(jié)碼生成。將前面各個(gè)步驟所生成的信息轉(zhuǎn)化成字節(jié)碼。

上述3個(gè)處理過(guò)程里,執(zhí)行插入式注解時(shí)又可能會(huì)產(chǎn)生新的符號(hào),如果有新的符號(hào)產(chǎn)生,就必須轉(zhuǎn)回到之前的解析、填充符號(hào)表的過(guò)程中重新處理這些新符號(hào),從總體來(lái)看,三者之間的關(guān)系與交互順序如圖10-4所示。

Javac編譯動(dòng)作的入口是com.sun.tools.javac.main.JavaCompiler類(lèi),上述3個(gè)過(guò)程的代碼邏輯集中在這個(gè)類(lèi)的compile()和compile2()方法里。

10.2.2 解析與填充符號(hào)表

解析過(guò)程由圖10-5中的parseFiles()方法(圖10-5中的過(guò)程1.1)來(lái)完成,解析過(guò)程包括了經(jīng)典程序編譯原理中的詞法分析和語(yǔ)法分析兩個(gè)步驟。

1.詞法、語(yǔ)法分析

詞法分析是將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記(Token)集合的過(guò)程,單個(gè)字符是程序編寫(xiě)時(shí)的最小元素,但標(biāo)記才是編譯時(shí)的最小元素。關(guān)鍵字、變量名、字面量、運(yùn)算符都可以作為標(biāo)記。

語(yǔ)法分析是根據(jù)標(biāo)記序列構(gòu)造抽象語(yǔ)法樹(shù)的過(guò)程,抽象語(yǔ)法樹(shù)(Abstract Syntax Tree,AST)是一種用來(lái)描述程序代碼語(yǔ)法結(jié)構(gòu)的樹(shù)形表示方式,抽象語(yǔ)法樹(shù)的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語(yǔ)法結(jié)構(gòu)(Syntax Construct),例如包、類(lèi)型、修飾符、運(yùn)算符、接口、返回值甚至連代碼注釋等都可以是一種特定的語(yǔ)法結(jié)構(gòu)。

2.填充符號(hào)表

符號(hào)表(Symbol Table)是由一組符號(hào)地址和符號(hào)信息構(gòu)成的數(shù)據(jù)結(jié)構(gòu),讀者可以把它類(lèi)比想象成哈希表中鍵值對(duì)的存儲(chǔ)形式(實(shí)際上符號(hào)表不一定是哈希表實(shí)現(xiàn),可以是有序符號(hào)表、樹(shù)狀符號(hào)表、棧結(jié)構(gòu)符號(hào)表等各種形式)。符號(hào)表中所登記的信息在編譯的不同階段都要被用到。譬如在語(yǔ)義分析的過(guò)程中,符號(hào)表所登記的內(nèi)容將用于語(yǔ)義檢查(如檢查一個(gè)名字的使用和原先的聲明是否一致)和產(chǎn)生中間代碼,在目標(biāo)代碼生成階段,當(dāng)對(duì)符號(hào)名進(jìn)行地址分配時(shí),符號(hào)表是地址分配的直接依據(jù)。

10.2.3 注解處理器

JDK 5之后,Java語(yǔ)言提供了對(duì)注解(Annotations)的支持,注解在設(shè)計(jì)上原本是與普通的Java代碼一樣,都只會(huì)在程序運(yùn)行期間發(fā)揮作用的。但在JDK 6中又提出并通過(guò)了JSR-269提案,該提案設(shè)計(jì)了一組被稱(chēng)為“插入式注解處理器”的標(biāo)準(zhǔn)API,可以提前至編譯期對(duì)代碼中的特定注解進(jìn)行處理,從而影響到前端編譯器的工作過(guò)程。

可以把插入式注解處理器看作是一組編譯器的插件,當(dāng)這些插件工作時(shí),允許讀取、修改、添加抽象語(yǔ)法樹(shù)中的任意元素。如果這些插件在處理注解期間對(duì)語(yǔ)法樹(shù)進(jìn)行過(guò)修改,編譯器將回到解析及填充符號(hào)表的過(guò)程重新處理,直到所有插入式注解處理器都沒(méi)有再對(duì)語(yǔ)法樹(shù)進(jìn)行修改為止,每一次循環(huán)過(guò)程稱(chēng)為一個(gè)輪次(Round),這也就對(duì)應(yīng)著圖10-4的那個(gè)回環(huán)過(guò)程。

10.2.4 語(yǔ)義分析與字節(jié)碼生成

經(jīng)過(guò)語(yǔ)法分析之后,編譯器獲得了程序代碼的抽象語(yǔ)法樹(shù)表示,抽象語(yǔ)法樹(shù)能夠表示一個(gè)結(jié)構(gòu)正確的源程序,但無(wú)法保證源程序的語(yǔ)義是符合邏輯的。

語(yǔ)義分析的主要任務(wù)則是對(duì)結(jié)構(gòu)上正確的源程序進(jìn)行上下文相關(guān)性質(zhì)的檢查,譬如進(jìn)行類(lèi)型檢查、控制流檢查、數(shù)據(jù)流檢查,等等。

Javac在編譯過(guò)程中,語(yǔ)義分析過(guò)程可分為標(biāo)注檢查和數(shù)據(jù)及控制流分析兩個(gè)步驟,分別由圖10-5的attribute()和flow()方法(分別對(duì)應(yīng)圖10-5中的過(guò)程3.1和過(guò)程3.2)完成。

1.標(biāo)注檢查

標(biāo)注檢查步驟要檢查的內(nèi)容包括諸如變量使用前是否已被聲明、變量與賦值之間的數(shù)據(jù)類(lèi)型是否能夠匹配,等等。

2.數(shù)據(jù)及控制流分析

數(shù)據(jù)流分析和控制流分析是對(duì)程序上下文邏輯更進(jìn)一步的驗(yàn)證,它可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問(wèn)題。編譯時(shí)期的數(shù)據(jù)及控制流分析與類(lèi)加載時(shí)的數(shù)據(jù)及控制流分析的目的基本上可以看作是一致的,但校驗(yàn)范圍會(huì)有所區(qū)別,有一些校驗(yàn)項(xiàng)只有在編譯期或運(yùn)行期才能進(jìn)行。

3.解語(yǔ)法糖

語(yǔ)法糖(Syntactic Sugar),也稱(chēng)糖衣語(yǔ)法,指的是在計(jì)算機(jī)語(yǔ)言中添加的某種語(yǔ)法,這種語(yǔ)法對(duì)語(yǔ)言的編譯結(jié)果和功能并沒(méi)有實(shí)際影響,但是卻能更方便程序員使用該語(yǔ)言。通常來(lái)說(shuō)使用語(yǔ)法糖能夠減少代碼量、增加程序的可讀性,從而減少程序代碼出錯(cuò)的機(jī)會(huì)。

Java中最常見(jiàn)的語(yǔ)法糖包括了泛型、變長(zhǎng)參數(shù)、自動(dòng)裝箱拆箱,等等,Java虛擬機(jī)運(yùn)行時(shí)并不直接支持這些語(yǔ)法,它們?cè)诰幾g階段被還原回原始的基礎(chǔ)語(yǔ)法結(jié)構(gòu),這個(gè)過(guò)程就稱(chēng)為解語(yǔ)法糖。

4.字節(jié)碼生成

字節(jié)碼生成是Javac編譯過(guò)程的最后一個(gè)階段,字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟所生成的信息(語(yǔ)法樹(shù)、符號(hào)表)轉(zhuǎn)化成字節(jié)碼指令寫(xiě)到磁盤(pán)中,編譯器還進(jìn)行了少量的代碼添加和轉(zhuǎn)換工作。

10.3 Java語(yǔ)法糖的味道

10.3.1 泛型

泛型的本質(zhì)是參數(shù)化類(lèi)型(Parameterized Type)或者參數(shù)化多態(tài)(Parametric Poly morphism)的應(yīng)用,即可以將操作的數(shù)據(jù)類(lèi)型指定為方法簽名中的一種特殊參數(shù),這種參數(shù)類(lèi)型能夠在類(lèi)、接口和方法的創(chuàng)建中,分別構(gòu)成泛型類(lèi)、泛型接口和泛型方法。泛型讓程序員能夠針對(duì)泛化的數(shù)據(jù)類(lèi)型編寫(xiě)相同的算法,這極大地增強(qiáng)了編程語(yǔ)言的類(lèi)型系統(tǒng)及抽象能力。

Java選擇的泛型實(shí)現(xiàn)方式叫作“類(lèi)型擦除式泛型”(Type erasure generics),它的唯一優(yōu)勢(shì)是在于實(shí)現(xiàn)這種泛型的影響范圍上:擦除式泛型的實(shí)現(xiàn)幾乎只需要在javac編譯器上作出改進(jìn)即可,不需要改變字節(jié)碼,不需要改動(dòng)java虛擬機(jī),也保證了以前沒(méi)有使用泛型的庫(kù)可以直接運(yùn)行在Java5.0之上。

泛型擦除帶來(lái)的問(wèn)題:

  • 導(dǎo)致了對(duì)原始類(lèi)型(Primitive types)數(shù)據(jù)的支持成為了麻煩,要強(qiáng)制裝箱拆卸
  • 運(yùn)行時(shí)期無(wú)法取得泛型類(lèi)型信息,會(huì)讓代碼變得啰嗦
  • 可通過(guò)反射繞過(guò)泛型類(lèi)型
  • 泛型更多可參考:
    泛型知識(shí)點(diǎn)

    10.3.2 自動(dòng)裝箱、拆箱與遍歷循環(huán)

    裝箱、拆箱
    1、什么是裝箱?什么是拆箱?

    裝箱:基本類(lèi)型轉(zhuǎn)變?yōu)榘b器類(lèi)型的過(guò)程。
    拆箱:包裝器類(lèi)型轉(zhuǎn)變?yōu)榛绢?lèi)型的過(guò)程。

    2、裝箱和拆箱的執(zhí)行過(guò)程?

    裝箱是通過(guò)調(diào)用包裝器類(lèi)的 valueOf 方法實(shí)現(xiàn)的
    拆箱是通過(guò)調(diào)用包裝器類(lèi)的 xxxValue 方法實(shí)現(xiàn)的,xxx代表對(duì)應(yīng)的基本數(shù)據(jù)類(lèi)型。
    如int裝箱的時(shí)候自動(dòng)調(diào)用Integer的valueOf(int)方法;Integer拆箱的時(shí)候自動(dòng)調(diào)用Integer的intValue方法。

    3、常見(jiàn)問(wèn)題?

    整型的包裝類(lèi) valueOf 方法返回對(duì)象時(shí),在常用的取值范圍內(nèi)(-128<=x<128),會(huì)返回緩存對(duì)象。
    浮點(diǎn)型的包裝類(lèi) valueOf 方法返回新的對(duì)象。
    布爾型的包裝類(lèi) valueOf 方法 Boolean類(lèi)的靜態(tài)常量 TRUE | FALSE。

    裝箱操作會(huì)創(chuàng)建對(duì)象,頻繁的裝箱操作會(huì)消耗許多內(nèi)存,影響性能,所以可以避免裝箱的時(shí)候應(yīng)該盡量避免。

    equals(Object o) 因?yàn)樵璭quals方法中的參數(shù)類(lèi)型是封裝類(lèi)型,所傳入的參數(shù)類(lèi)型(a)是原始數(shù)據(jù)類(lèi)型,所以會(huì)自動(dòng)對(duì)其裝箱,反之,會(huì)對(duì)其進(jìn)行拆箱

    當(dāng)兩種不同類(lèi)型用比較時(shí),包裝器類(lèi)的需要拆箱, 當(dāng)同種類(lèi)型用比較時(shí),會(huì)自動(dòng)拆箱或者裝箱

    包含算術(shù)運(yùn)算會(huì)觸發(fā)自動(dòng)拆箱。

    存在大量自動(dòng)裝箱的過(guò)程,如果裝箱返回的包裝對(duì)象不是從緩存中獲取,會(huì)創(chuàng)建很多新的對(duì)象,比較消耗內(nèi)存。

    10.3.3 條件編譯

    10.4 實(shí)戰(zhàn):插入式注解處理器

    第11章 后端編譯與優(yōu)化

    11.1 概述

    如果我們把字節(jié)碼看作是程序語(yǔ)言的一種中間表示形式(Intermediate Representation,IR)的話(huà),那編譯器無(wú)論在何時(shí)、在何種狀態(tài)下把Class文件轉(zhuǎn)換成與本地基礎(chǔ)設(shè)施(硬件指令集、操作系統(tǒng))相關(guān)的二進(jìn)制機(jī)器碼,它都可以視為整個(gè)編譯過(guò)程的后端。如果讀者閱讀過(guò)本書(shū)的第2版,可能會(huì)發(fā)現(xiàn)本章的標(biāo)題已經(jīng)從“運(yùn)行期編譯與優(yōu)化”悄然改成了“后端編譯與優(yōu)化”,這是因?yàn)樵?012年的Java世界里,雖然提前編譯(Ahead Of Time,AOT)早已有所應(yīng)用,但相對(duì)而言,即時(shí)編譯(Just In Time,JIT)才是占絕對(duì)主流的編譯形式。不過(guò),最近幾年編譯技術(shù)發(fā)展出現(xiàn)了一些微妙的變化,提前編譯不僅逐漸被主流JDK所支持,而且在Java編譯技術(shù)的前沿研究中又重新成了一個(gè)熱門(mén)的話(huà)題,所以再繼續(xù)只提“運(yùn)行期”和“即時(shí)編譯”就顯得不夠全面了,在本章中它們兩者都是主角。

    11.2 即時(shí)編譯器

    11.3 提前編譯器

    11.4 編譯器優(yōu)化技術(shù)

    11.5 實(shí)戰(zhàn):深入理解Graal編譯器

    第五部分 高效并發(fā)

    第12章 Java內(nèi)存模型與線(xiàn)程

    12.2 硬件的效率與一致性

    高速緩存(Cache):讀寫(xiě)速度盡可能接近處理器運(yùn)算速度的高速緩存作為內(nèi)存與處理器之間的緩沖,將運(yùn)算需要使用的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存之中。

    緩存一致性(Cache Coherence):多路處理器系統(tǒng),每個(gè)處理器有自己的高速緩存,而又共享同一主內(nèi)存(Main Memory),這種系統(tǒng)被稱(chēng)為共享內(nèi)存多核系統(tǒng)(Shared Memory Multiprocessors System)。

    12.3 Java內(nèi)存模型 Java Memory Model(JMM)

    12.3.1 主內(nèi)存與工作內(nèi)存

    Java內(nèi)存模型的主要目的是定義程序中各種變量的訪問(wèn)規(guī)則,即關(guān)注在虛擬機(jī)中把變量值存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量值這樣的底層細(xì)節(jié)。

    此處的變量(Variables)與Java編程中所說(shuō)的變量有所區(qū)別,它包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但是不包括局部變量與方法參數(shù),因?yàn)楹笳呤蔷€(xiàn)程私有的,不會(huì)被共享,自然就不會(huì)存在競(jìng)爭(zhēng)問(wèn)題。

    Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中,每條線(xiàn)程還有自己的工作內(nèi)存,線(xiàn)程的工作內(nèi)存中保存了被該線(xiàn)程使用的變量的主內(nèi)存副本,線(xiàn)程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存中的數(shù)據(jù)。不同的線(xiàn)程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線(xiàn)程間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成,線(xiàn)程、主內(nèi)存、工作內(nèi)存三者的交互關(guān)系如圖12-2所示。

    這里所講的主內(nèi)存、工作內(nèi)存與第2章所講的Java內(nèi)存區(qū)域中的Java堆、棧、方法區(qū)等并不是同一個(gè)層次的對(duì)內(nèi)存的劃分,這兩者基本上是沒(méi)有任何關(guān)系的。如果兩者一定要勉強(qiáng)對(duì)應(yīng)起來(lái),那么從變量、主內(nèi)存、工作內(nèi)存的定義來(lái)看,主內(nèi)存主要對(duì)應(yīng)于Java堆中的對(duì)象實(shí)例數(shù)據(jù)部分,而工作內(nèi)存則對(duì)應(yīng)于虛擬機(jī)棧中的部分區(qū)域。從更基礎(chǔ)的層次上說(shuō),主內(nèi)存直接對(duì)應(yīng)于物理硬件的內(nèi)存,而為了獲取更好的運(yùn)行速度,虛擬機(jī)(或者是硬件、操作系統(tǒng)本身的優(yōu)化措施)可能會(huì)讓工作內(nèi)存優(yōu)先存儲(chǔ)于寄存器和高速緩存中,因?yàn)槌绦蜻\(yùn)行時(shí)主要訪問(wèn)的是工作內(nèi)存。

    12.3.2 內(nèi)存間交互操作

    關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存這一類(lèi)的實(shí)現(xiàn)細(xì)節(jié),Java內(nèi)存模型中定義了以下8種操作來(lái)完成。Java虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證下面提及的每一種操作都是原子的、不可再分的(對(duì)于double和long類(lèi)型的變量來(lái)說(shuō),load、store、read和write操作在某些平臺(tái)上允許有例外,這個(gè)問(wèn)題在12.3.4節(jié)會(huì)專(zhuān)門(mén)討論)。

    ·lock(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線(xiàn)程獨(dú)占的狀態(tài)。

    ·unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線(xiàn)程鎖定。

    ·read(讀取):作用于主內(nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€(xiàn)程的工作內(nèi)存中,以便隨后的load動(dòng)作使用。

    ·load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。

    ·use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。

    ·assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。

    ·store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write操作使用。

    ·write(寫(xiě)入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。

    **如果要把一個(gè)變量從主內(nèi)存拷貝到工作內(nèi)存,那就要按順序執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要按順序執(zhí)行store和write操作。**注意,Java內(nèi)存模型只要求上述兩個(gè)操作必須按順序執(zhí)行,但不要求是連續(xù)執(zhí)行。也就是說(shuō)read與load之間、store與write之間是可插入其他指令的,如對(duì)主內(nèi)存中的變量a、b進(jìn)行訪問(wèn)時(shí),一種可能出現(xiàn)的順序是reada、read b、load b、load a。

    除此之外,Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時(shí)必須滿(mǎn)足如下規(guī)則:

    ·不允許read和load、store和write操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者工作內(nèi)存發(fā)起回寫(xiě)了但主內(nèi)存不接受的情況出現(xiàn)。

    ·不允許一個(gè)線(xiàn)程丟棄它最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。

    ·不允許一個(gè)線(xiàn)程無(wú)原因地(沒(méi)有發(fā)生過(guò)任何assign操作)把數(shù)據(jù)從線(xiàn)程的工作內(nèi)存同步回主內(nèi)存中。

    ·一個(gè)新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量,換句話(huà)說(shuō)就是對(duì)一個(gè)變量實(shí)施use、store操作之前,必須先執(zhí)行assign和load操作。

    ·一個(gè)變量在同一個(gè)時(shí)刻只允許一條線(xiàn)程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線(xiàn)程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。

    ·如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,那將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作以初始化變量的值。

    ·如果一個(gè)變量事先沒(méi)有被lock操作鎖定,那就不允許對(duì)它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其他線(xiàn)程鎖定的變量。

    ·對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)。

    這8種內(nèi)存訪問(wèn)操作以及上述規(guī)則限定,再加上稍后會(huì)介紹的專(zhuān)門(mén)針對(duì)volatile的一些特殊規(guī)定,就已經(jīng)能準(zhǔn)確地描述出Java程序中哪些內(nèi)存訪問(wèn)操作在并發(fā)下才是安全的。

    12.3.3 對(duì)于volatile型變量的特殊規(guī)則

    當(dāng)一個(gè)變量被定義成volatile之后,它將具備兩項(xiàng)特性:

    第一項(xiàng)是保證此變量對(duì)所有線(xiàn)程的可見(jiàn)性,可見(jiàn)性是指一條線(xiàn)程修改了這個(gè)變量的值,新值對(duì)于其他線(xiàn)程來(lái)說(shuō)是可以立即得知的;第二個(gè)語(yǔ)義是禁止指令重排序優(yōu)化。

    Volatile不保證原子性,因此在不符合以下兩條規(guī)則的運(yùn)算場(chǎng)景中,仍要通過(guò)加鎖來(lái)包裝原子性:
    · 運(yùn)算結(jié)果并不依賴(lài)變量的當(dāng)前值,或者能確保只有單一的線(xiàn)程修改變量的值。
    · 變量不需要與其他的狀態(tài)變量共同參與不變約束

    內(nèi)存屏障(Memory Barrier/Memory Fence):重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置。

    12.3.4 針對(duì)long和double型變量的特殊規(guī)則

    Java內(nèi)存模型要求lock、unlock、read、load、assign、use、store、write這八種操作都具有原子性,但是對(duì)于64位的數(shù)據(jù)類(lèi)型(long和double),在模型中特別定義了一條寬松的規(guī)定:允許虛擬機(jī)將沒(méi)有被volatile修飾的64位數(shù)據(jù)的讀寫(xiě)操作劃分為兩次32位的操作來(lái)進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)自行選擇是否要保證64位數(shù)據(jù)類(lèi)型的load、store、read和write這四個(gè)操作的原子性,這就是所謂的“l(fā)ong和double的非原子性協(xié)定”(Non-Atomic Treatment of doubleand long Variables)。

    12.3.5 原子性、可見(jiàn)性與有序性

    介紹完Java內(nèi)存模型的相關(guān)操作和規(guī)則后,我們?cè)僬w回顧一下這個(gè)模型的特征。Java內(nèi)存模型是圍繞著在并發(fā)過(guò)程中如何處理原子性、可見(jiàn)性和有序性這三個(gè)特征來(lái)建立的,我們逐個(gè)來(lái)看一下哪些操作實(shí)現(xiàn)了這三個(gè)特性。

    1.原子性(Atomicity)

    一個(gè)或某幾個(gè)操作只能在一個(gè)線(xiàn)程執(zhí)行完之后,另一個(gè)線(xiàn)程才能開(kāi)始執(zhí)行該操作,也就是說(shuō)這些操作是不可分割的,線(xiàn)程不能在這些操作上交替執(zhí)行。

    由Java內(nèi)存模型來(lái)直接保證的原子性變量操作包括read、load、assign、use、store和write這六個(gè),我們大致可以認(rèn)為,基本數(shù)據(jù)類(lèi)型的訪問(wèn)、讀寫(xiě)都是具備原子性的(例外就是long和double的非原子性協(xié)定,讀者只要知道這件事情就可以了,無(wú)須太過(guò)在意這些幾乎不會(huì)發(fā)生的例外情況)。

    如果應(yīng)用場(chǎng)景需要一個(gè)更大范圍的原子性保證(經(jīng)常會(huì)遇到),Java內(nèi)存模型還提供了lock和unlock操作來(lái)滿(mǎn)足這種需求,盡管虛擬機(jī)未把lock和unlock操作直接開(kāi)放給用戶(hù)使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來(lái)隱式地使用這兩個(gè)操作。這兩個(gè)字節(jié)碼指令反映到Java代碼中就是同步塊——synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。

    2.可見(jiàn)性(Visibility)

    **可見(jiàn)性就是指當(dāng)一個(gè)線(xiàn)程修改了共享變量的值時(shí),其他線(xiàn)程能夠立即得知這個(gè)修改。**Java內(nèi)存模型是通過(guò)在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴(lài)主內(nèi)存作為傳遞媒介的方式來(lái)實(shí)現(xiàn)可見(jiàn)性的,無(wú)論是普通變量還是volatile變量都是如此。普通變量與volatile變量的區(qū)別是,volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。因此我們可以說(shuō)volatile保證了多線(xiàn)程操作時(shí)變量的可見(jiàn)性,而普通變量則不能保證這一點(diǎn)。

    除了volatile之外,Java還有兩個(gè)關(guān)鍵字能實(shí)現(xiàn)可見(jiàn)性,它們是synchronized和final

    同步塊的可見(jiàn)性是由“對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)”這條規(guī)則獲得的。而final關(guān)鍵字的可見(jiàn)性是指:被final修飾的字段在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒(méi)有把“this”的引用傳遞出去(this引用逃逸是一件很危險(xiǎn)的事情,其他線(xiàn)程有可能通過(guò)這個(gè)引用訪問(wèn)到“初始化了一半”的對(duì)象),那么在其他線(xiàn)程中就能看見(jiàn)final字段的值。

    3.有序性(Ordering)

    Java語(yǔ)言提供了volatile和synchronized兩個(gè)關(guān)鍵字來(lái)保證線(xiàn)程之間操作的有序性,**volatile關(guān)鍵字本身就包含了禁止指令重排序的語(yǔ)義,而synchronized則是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線(xiàn)程對(duì)其進(jìn)行l(wèi)ock操作”**這條規(guī)則獲得的,這個(gè)規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行地進(jìn)入。

    12.3.6 先行發(fā)生原則

    Java語(yǔ)言中有一個(gè)**“先行發(fā)生”(Happens-Before)的原則。這個(gè)原則非常重要,它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng),線(xiàn)程是否安全的非常有用的手段**。依賴(lài)這個(gè)原則,我們可以通過(guò)幾條簡(jiǎn)單規(guī)則一攬子解決并發(fā)環(huán)境下兩個(gè)操作之間是否可能存在沖突的所有問(wèn)題,而不需要陷入Java內(nèi)存模型苦澀難懂的定義之中。

    ·**程序次序規(guī)則(Program Order Rule):在一個(gè)線(xiàn)程內(nèi),按照控制流順序,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作。注意,這里說(shuō)的是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)等結(jié)構(gòu)。

    ·***管程鎖定規(guī)則(Monitor Lock Rule)***:一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。這里必須強(qiáng)調(diào)的是“同一個(gè)鎖”,而“后面”是指時(shí)間上的先后。

    ·volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫(xiě)操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作,這里的“后面”同樣是指時(shí)間上的先后。

    ·線(xiàn)程啟動(dòng)原則(Threa Start Rule):Thread對(duì)象的start()方法先行發(fā)生于此線(xiàn)程的每一個(gè)動(dòng)作

    ·線(xiàn)程終止規(guī)則(Thread Termination Rule):線(xiàn)程中的所有操作都先行發(fā)生于對(duì)此線(xiàn)程的終止檢測(cè),我們可以通過(guò)Thread::join()方法是否結(jié)束、Thread::isAlive()的返回值等手段檢測(cè)線(xiàn)程是否已經(jīng)終止執(zhí)行。

    ·線(xiàn)程中斷規(guī)則(Thread Interruption Rule):對(duì)線(xiàn)程interrupt()方法的調(diào)用先行發(fā)生于被中斷線(xiàn)程的代碼檢測(cè)到中斷事件的發(fā)生,可以通過(guò)Thread::interrupted()方法檢測(cè)到是否有中斷發(fā)生。

    ·對(duì)象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開(kāi)始

    ·傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論

    12.4 Java與線(xiàn)程

    12.4.1 線(xiàn)程的實(shí)現(xiàn)

    線(xiàn)程是進(jìn)行處理器資源調(diào)度的最基本單位
    進(jìn)程是操作系統(tǒng)資源分配的基本單位,而線(xiàn)程是任務(wù)調(diào)度和執(zhí)行的基本單位

    12.4.2 Java線(xiàn)程調(diào)度

    線(xiàn)程調(diào)度是指系統(tǒng)為線(xiàn)程分配處理器使用權(quán)的過(guò)程。主要有兩種調(diào)度方式:協(xié)同式線(xiàn)程調(diào)度(Cooperative Threads-Scheduling)和搶占式線(xiàn)程調(diào)度(Preemptive Threads-Scheduling)。

    協(xié)同式:線(xiàn)程的執(zhí)行時(shí)間由線(xiàn)程本身控制,線(xiàn)程把自己的工作執(zhí)行完,主動(dòng)通知系統(tǒng)切換到另一個(gè)線(xiàn)程上。實(shí)現(xiàn)簡(jiǎn)單,切換操作對(duì)線(xiàn)程可知;但是如一個(gè)線(xiàn)程有問(wèn)題,會(huì)一直阻塞。
    搶占式:每個(gè)線(xiàn)程由系統(tǒng)來(lái)分配執(zhí)行時(shí)間。Java就是搶占式的。Java線(xiàn)程優(yōu)先級(jí),兩個(gè)處于ready狀態(tài)的線(xiàn)程,優(yōu)先級(jí)越高的越容易被系統(tǒng)選擇。

    12.4.3 狀態(tài)轉(zhuǎn)換

    Java中線(xiàn)程的狀態(tài)分為6種

  • 新建(NEW):創(chuàng)建后尚未啟動(dòng)的線(xiàn)程對(duì)象,還沒(méi)有調(diào)用start()方法。

  • 運(yùn)行(RUNNABLE):Java線(xiàn)程中將**就緒(ready)和運(yùn)行中(running)**兩種狀態(tài)籠統(tǒng)的稱(chēng)為“運(yùn)行”。處于這個(gè)狀態(tài)的線(xiàn)程可能正在執(zhí)行或者正在等待操作系統(tǒng)為它分配執(zhí)行時(shí)間。
    線(xiàn)程對(duì)象創(chuàng)建后,其他線(xiàn)程(比如main線(xiàn)程)調(diào)用了該對(duì)象的start()方法。該狀態(tài)的線(xiàn)程位于可運(yùn)行線(xiàn)程池中,等待被線(xiàn)程調(diào)度選中,獲取CPU的使用權(quán),此時(shí)處于就緒狀態(tài)(ready)。就緒狀態(tài)的線(xiàn)程在獲得CPU時(shí)間片后變?yōu)檫\(yùn)行中狀態(tài)(running)

  • 阻塞(BLOCKED):表示線(xiàn)程阻塞于鎖。

  • 無(wú)限期等待(WAITING):處于這個(gè)狀態(tài)的線(xiàn)程不會(huì)被分配處理器執(zhí)行時(shí)間,進(jìn)入該狀態(tài)的線(xiàn)程需要等待其他線(xiàn)程做出一些特定動(dòng)作(通知或中斷)才能喚醒。會(huì)讓線(xiàn)程陷入這種狀態(tài)的方法:
    ·沒(méi)有設(shè)置Timeout參數(shù)的Object::wait()方法
    ·沒(méi)有設(shè)置Timeout參數(shù)的Thread::join()方法
    ·LockSupport::park()方法

  • 超時(shí)等待(TIMED_WAITING):該狀態(tài)也不會(huì)被分配處理器執(zhí)行時(shí)間,但是不同于WAITING,它可以在指定的時(shí)間后自行返回。以下方法會(huì)讓線(xiàn)程進(jìn)入此狀態(tài):
    ·Thread::sleep()方法
    ·設(shè)置Timeout參數(shù)的Object::wait()方法
    ·設(shè)置Timeout參數(shù)的Thread::join()方法
    ·LockSupport::parkNanos()方法
    ·LockSupport::parkUtil()方法

  • 終止(TERMINATED):表示該線(xiàn)程已經(jīng)執(zhí)行完畢。

  • 12.5 Java與協(xié)程

    第13章 線(xiàn)程安全與鎖優(yōu)化

    13.1 概述

    13.2 線(xiàn)程安全

    13.2.1 Java語(yǔ)言中的線(xiàn)程安全

  • 不可變
    不可變的對(duì)象一定是線(xiàn)程安全的
  • 絕對(duì)線(xiàn)程安全
  • 相對(duì)線(xiàn)程安全
  • 線(xiàn)程兼容
  • 線(xiàn)程對(duì)立
  • 13.2.2 線(xiàn)程安全的實(shí)現(xiàn)方法

    1.互斥同步(Mutual Exclusion & Synchronization)

    互斥同步是最常見(jiàn)也是最主要的并發(fā)正確性保障手段。同步是在多個(gè)線(xiàn)程并發(fā)訪問(wèn)共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一時(shí)刻只被一條(或是一些,當(dāng)使用信號(hào)量的時(shí)候)線(xiàn)程使用。互斥是實(shí)現(xiàn)同步的一種手段,臨界區(qū)(critical section)、互斥量(Mutex)和信號(hào)量(Semaphor)都是常見(jiàn)的互斥實(shí)現(xiàn)方式。

    最基本的互斥同步手段就是synchronized關(guān)鍵字,是一種塊結(jié)構(gòu)(Block Structured)的同步語(yǔ)法。Synchronized關(guān)鍵字經(jīng)過(guò)Javac編譯之后,會(huì)在同步塊的前后分別形成monitorenter和monitorexit兩個(gè)字節(jié)碼指令。這兩個(gè)字節(jié)碼指令都需要一個(gè)reference類(lèi)型的參數(shù)來(lái)指明要鎖定和解鎖的對(duì)象。

    Synchronized修飾的同步塊對(duì)同一條線(xiàn)程來(lái)說(shuō)是可重入的,在持有鎖的線(xiàn)程執(zhí)行完畢并釋放鎖之前,會(huì)無(wú)條件地阻塞后面其他線(xiàn)程的進(jìn)入。

    從執(zhí)行成本的角度看,持有鎖是一個(gè)重量級(jí)(Heavy-weight)的操作。Java的線(xiàn)程是映射到操作系統(tǒng)的原生內(nèi)核線(xiàn)程之上,如果要阻塞或喚醒一條線(xiàn)程,則需要操作系統(tǒng)來(lái)幫忙完成,不可避免需要用戶(hù)態(tài)到核心態(tài)的轉(zhuǎn)換。

    自JDK5起,Java類(lèi)庫(kù)中新提供了java.util.concurrent(J.U.C包),其中的java.util.concurrent.locks.Lock接口便成了Java的另一種全新的互斥同步手段。基于Lock接口,用戶(hù)能夠以非塊(Non-Block Structured)來(lái)實(shí)現(xiàn)互斥同步。

    **重入鎖(ReentrantLock)**是Lock接口最常見(jiàn)的一種實(shí)現(xiàn),它也是可重入的。比Synchronized多了三個(gè)高級(jí)功能:
    ·等待可中斷:當(dāng)持有鎖的線(xiàn)程長(zhǎng)期不釋放鎖的時(shí)候,正在等待的線(xiàn)程可選擇放棄等待,改為處理其他事情。

    ·公平鎖:多個(gè)線(xiàn)程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來(lái)依次獲得鎖;而非公平鎖在鎖釋放時(shí),任何一個(gè)等待鎖的線(xiàn)程都有機(jī)會(huì)獲得鎖。Synchronized中的鎖是非公平鎖,ReentrantLock在默認(rèn)情況下也是非公平的,但可改為公平鎖。

    ·鎖綁定多個(gè)條件:一個(gè)ReentrantLock對(duì)象可以同時(shí)綁定多個(gè)Condition對(duì)象。在Synchronized中,鎖對(duì)象的wait()跟它的notify()或者notifyAll()方法配合可以實(shí)現(xiàn)一個(gè)隱含的條件,如要和多個(gè)條件關(guān)聯(lián),需要額外添加一個(gè)鎖。而ReentrantLock只需多次調(diào)用newCondition()方法。

    JDK6針對(duì)synchronized優(yōu)化后,synchronized和reentrantlock的性能差不多。兩個(gè)都可滿(mǎn)足時(shí)優(yōu)先使用synchronized:
    ·synchronized是在java語(yǔ)法層面的同步,足夠清晰簡(jiǎn)單
    ·reentrantlock需要在finally塊中釋放鎖,否則一旦受同步保護(hù)的代碼塊拋出異常,則可能永遠(yuǎn)不會(huì)釋放持有的鎖

    2.非阻塞同步
    互斥同步面臨的主要問(wèn)題是進(jìn)行線(xiàn)程阻塞和喚醒鎖帶來(lái)的性能開(kāi)銷(xiāo),因此這種同步也被稱(chēng)為阻塞同步(Blocking Synchronization)
    基于沖突檢測(cè)的樂(lè)觀并發(fā)策略:不管有沒(méi)有沖突,先進(jìn)行操作,如果沒(méi)有其他線(xiàn)程爭(zhēng)用共享數(shù)據(jù),那操作就直接成功了;如果共享的數(shù)據(jù)的確被爭(zhēng)用,產(chǎn)生了沖突,那再進(jìn)行其他的補(bǔ)償措施。被稱(chēng)為非阻塞同步(Non-Blocking Synchronization),使用這種措施的代碼也常被稱(chēng)為無(wú)鎖(Lock-Free)編程。

    **Compare-and-Swap(CAS)比較并交換指令,需要有三個(gè)操作數(shù),分別是內(nèi)存位置(在Java中指變量的內(nèi)存地址,用V表示),舊的預(yù)期值(用A表示)和準(zhǔn)備設(shè)置的新值(用B表示)。CAS指令執(zhí)行時(shí),當(dāng)且僅當(dāng)V符合A時(shí),處理器才會(huì)用B更新V的值,否則就不執(zhí)行更新。但是,不管是否更新了V的值,都會(huì)返回V的舊值。**上述處理過(guò)程是一個(gè)原子操作,執(zhí)行期間不會(huì)被其他線(xiàn)程中斷。

    JDK5之后,Java類(lèi)庫(kù)中才開(kāi)始使用CAS操作,該操作由sun.misc.Unsafe類(lèi)里面的compareAndSwapInt()和compareAndSwapLong()等幾個(gè)方法包裝提供。

    CAS操作的“ABA問(wèn)題”:如果一個(gè)變量V初次讀取的時(shí)候是A值,在這段期間它的值曾被改成B,后又改回為A,那CAS操作就會(huì)誤認(rèn)為它從來(lái)沒(méi)有改變過(guò)。J.U.C包為了解決這個(gè)問(wèn)題,提供了一個(gè)帶有標(biāo)記的原子引用類(lèi)AtomicStampedReference,它可以通過(guò)控制變量值的版本來(lái)保證CAS的正確性。

    3.無(wú)同步方案
    可重入代碼(Reentrant Code)/純代碼(Pure Code):指可以在代碼執(zhí)行的任何時(shí)刻中斷它,轉(zhuǎn)而去執(zhí)行另一段代碼(包括遞歸調(diào)用它本身),而在控制權(quán)返回后,原來(lái)的程序不會(huì)出現(xiàn)任何錯(cuò)誤,也不會(huì)對(duì)結(jié)果有所影響。在特指多線(xiàn)程的上下文語(yǔ)境里,可認(rèn)為可重入代碼是線(xiàn)程安全代碼的一個(gè)真子集。

    線(xiàn)程本地存儲(chǔ)(Thread Local Storage):如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享,那就看看這些共享數(shù)據(jù)的代碼是否能保證在同一個(gè)線(xiàn)程中執(zhí)行。如果能保證,我們就可以把共享數(shù)據(jù)的可見(jiàn)范圍控制在同一個(gè)線(xiàn)程之內(nèi),這樣,無(wú)需同步也能保證線(xiàn)程之間不出現(xiàn)數(shù)據(jù)爭(zhēng)用的問(wèn)題。

    在Java之中,如果一個(gè)變量要被多線(xiàn)程訪問(wèn),可以使用volatile關(guān)鍵字將它聲明為易變的。可用java.lang.ThreadLocal類(lèi)實(shí)現(xiàn)線(xiàn)程本地存儲(chǔ)的功能。每一個(gè)線(xiàn)程的Thread對(duì)象中都有個(gè)ThreadLocalMap對(duì)象,這個(gè)對(duì)象存儲(chǔ)了一組以ThreadLocal.threadLocalHashCode為健,以本地線(xiàn)程變量為值的K-V值對(duì),ThreadLocal對(duì)象就是當(dāng)前線(xiàn)程的ThreadLocalMap的訪問(wèn)入口,每一個(gè)ThreadLocal對(duì)象都包含了一個(gè)獨(dú)一無(wú)二的threadLocalHashCode值,使用這個(gè)值就可以在線(xiàn)程K-V值對(duì)中找回對(duì)應(yīng)的本地線(xiàn)程變量。

    13.3 鎖優(yōu)化

    從JDK5到JDK6HotSpot虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)花費(fèi)了大量的資源實(shí)現(xiàn)了各種鎖優(yōu)化技術(shù),如**適應(yīng)性自旋(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖膨脹(Lock Coarsening)、輕量級(jí)鎖(LightEight Locking)、偏向鎖(Biased Locking)**等,這些技術(shù)都是胃了在線(xiàn)程之間更高效地共享數(shù)據(jù)及解決競(jìng)爭(zhēng)問(wèn)題,從而提供程序的執(zhí)行效率。

    13.3.1 自旋鎖與自適應(yīng)鎖

    在Java中鎖起到的作用是互斥同步,而互斥同步對(duì)性的影響最大的是阻塞,阻塞是通過(guò)掛起線(xiàn)程和恢復(fù)線(xiàn)程來(lái)實(shí)現(xiàn)的,這個(gè)操作是很昂貴的,消耗的服務(wù)器資源比較大。針對(duì)于此虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)發(fā)明了自旋鎖,因?yàn)?strong>在共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短一段時(shí)間,為了這段時(shí)間去掛起和恢復(fù)線(xiàn)程很不值得。所以在一個(gè)線(xiàn)程獲得鎖的同時(shí)另一個(gè)線(xiàn)程可以先“稍等一會(huì)兒”,但并不放棄處理器執(zhí)行時(shí)間,為了讓線(xiàn)程等待,只須讓線(xiàn)程執(zhí)行一個(gè)忙循環(huán)(自旋),這就是自旋鎖。

    那么這個(gè)自旋鎖的自旋時(shí)間多久比較合適呢?

    如自旋時(shí)間太短那就起不到自旋的作用了,太長(zhǎng)又會(huì)占用過(guò)多的處理器資源。所以在JDK1.4.2中引入自旋鎖的時(shí)候,就提供了自旋次數(shù)為10默認(rèn)值以及可以自行配置的參數(shù)-XXPreBlockSpin。

    在JDK1.6中對(duì)自旋鎖進(jìn)行了優(yōu)化,引入了自適應(yīng)自旋它可以根據(jù)前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定的。如果上一次獲得了鎖,那么下一次就會(huì)被認(rèn)為也會(huì)獲得鎖,進(jìn)而自旋時(shí)間會(huì)加長(zhǎng);如果這個(gè)鎖很少被成功獲得,那么有可能就直接省略掉自旋鎖,避免處理器資源浪費(fèi)

    13.3.2 鎖消除

    鎖消除是指:虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼要求同步,但是對(duì)被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。

    鎖消除是虛擬機(jī)自行判斷的,開(kāi)發(fā)人員,在編寫(xiě)代碼的時(shí)候并不用刻意的去規(guī)避這些問(wèn)題,因?yàn)橛行┩酱胧┒际荍ava本身自己實(shí)現(xiàn)的。

    例如如下代碼:

    public String concatString(String str1,String str2,String str3){return str1 + str2 + str3; }

    因?yàn)镾tring是被final修飾的類(lèi),所以每次變動(dòng)都是會(huì)產(chǎn)生新的String對(duì)象來(lái)進(jìn)行的,因此在編譯時(shí)會(huì)對(duì)String連接做自動(dòng)優(yōu)化。在JDK5之前會(huì)轉(zhuǎn)成StringBuffer對(duì)象進(jìn)行append()操作,在JDK5以后會(huì)轉(zhuǎn)為StringBuilder對(duì)象進(jìn)行append()操作。
    這樣JDK5之前編譯器就會(huì)把代碼變成如下形式:

    public String concatString(String str1,String str2,String str3){StringBuffer sb = new StringBuffer();sb.append(str1);sb.append(str2);sb.append(str3);return sb.toString(); }

    因?yàn)镾tringBuffer::append()方法就涉及到同步塊,鎖的就是sb對(duì)象。所以發(fā)現(xiàn)sb的動(dòng)態(tài)作用域在concatString()方法內(nèi)部,其他線(xiàn)程又無(wú)法訪問(wèn)到它,因此這里的鎖就可以被安全的消除。

    13.3.3 鎖粗化

    我們?cè)诰帉?xiě)代碼的時(shí)候,一般會(huì)遵循一個(gè)原則,就是盡量將同步塊的作用范圍限制的最小,只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣同步操作數(shù)量會(huì)變得更少,即使存鎖競(jìng)爭(zhēng),等待鎖的線(xiàn)程也能盡可能快地拿到鎖。
    但是實(shí)際情況,在一系列連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作時(shí)出現(xiàn)在循環(huán)體之中的,那即使沒(méi)有線(xiàn)程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。

    上面的代碼中concatString()方法就是頻繁的堆sb對(duì)象進(jìn)行加鎖,虛擬機(jī)會(huì)探測(cè)到這種情況,將鎖的范圍擴(kuò)展到整個(gè)系列操作的外部。就是在第一個(gè)append()操作之前到最后一個(gè)append()操作之后,只需要加一次鎖就可以了。

    總結(jié)一下鎖粗化:虛擬機(jī)探測(cè)到有一系列零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)加鎖的同步范圍擴(kuò)展(粗化)到整個(gè)系列的操作外部。

    13.3.4 輕量級(jí)鎖

    輕量級(jí)鎖是相對(duì)于操作系統(tǒng)互斥量來(lái)實(shí)現(xiàn)的“重量級(jí)”鎖而言的,但是輕量級(jí)鎖并不用來(lái)替代重量級(jí)鎖的,它是指在沒(méi)有多線(xiàn)程競(jìng)爭(zhēng)的前提下,減少重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。

    要理解輕量級(jí)鎖,必須要對(duì)虛擬機(jī)對(duì)象的內(nèi)存布局(尤其是對(duì)象頭部分)。

    HotSpot虛擬機(jī)的對(duì)象頭分為兩部分:
    第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)
    ,如哈希碼(HashCode)、GC分代年齡(Generational GC Age)等。這部分?jǐn)?shù)據(jù)的長(zhǎng)度咋32位和64位的虛擬機(jī)中分別會(huì)占用32個(gè)或64個(gè)比特,官方稱(chēng)它為“Mark Word”,它是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵
    第二部分是用于存儲(chǔ)指向方法區(qū)對(duì)象類(lèi)型數(shù)據(jù)的指針,如果是數(shù)組對(duì)象,還會(huì)有一個(gè)額外的部分用戶(hù)存儲(chǔ)數(shù)組長(zhǎng)度。

    由于對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,Mark Word被設(shè)計(jì)成一個(gè)非固定的動(dòng)態(tài)數(shù)據(jù)結(jié)構(gòu),以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息。
    Mark Word會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。下面是對(duì)象的狀態(tài)對(duì)應(yīng)的對(duì)象頭的存儲(chǔ)內(nèi)容表

    輕量級(jí)鎖工作過(guò)程
    輕量級(jí)鎖加鎖
    在代碼即將進(jìn)入同步塊的時(shí)候,如果此同步對(duì)象沒(méi)有被鎖定(標(biāo)志位“01”狀態(tài)),虛擬機(jī)首先將在當(dāng)前線(xiàn)程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝。
    然后,虛擬機(jī)將使用CAS操作嘗試把對(duì)象的Mark Word 更新為執(zhí)行Lock Record 的指針。
    如果這個(gè)更新操作成功了,即代表線(xiàn)程擁有了這個(gè)對(duì)象的鎖,并且對(duì)象Mark Word的鎖標(biāo)志位(Mark Word的最后兩個(gè)比特)將轉(zhuǎn)變?yōu)椤?0”,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)。
    如果這個(gè)更新操作失敗了,那就意味著至少存在一條線(xiàn)程與當(dāng)前線(xiàn)程競(jìng)爭(zhēng)獲取該對(duì)象的鎖。虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線(xiàn)程的棧楨,如果是,說(shuō)明當(dāng)前線(xiàn)程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那直接進(jìn)入同步塊繼續(xù)執(zhí)行就可以了,否則就說(shuō)嘛這個(gè)鎖對(duì)象已經(jīng)被其他線(xiàn)程搶占了。如果出現(xiàn)兩條意思的線(xiàn)程爭(zhēng)用同一個(gè)鎖的情況,輕量級(jí)鎖就會(huì)膨脹為重量級(jí)鎖。鎖標(biāo)記的狀態(tài)值變?yōu)椤?0”,此時(shí)Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線(xiàn)程必須進(jìn)入阻塞狀態(tài)。

    上面說(shuō)了輕量級(jí)鎖的加鎖過(guò)程了,它的解鎖過(guò)程也同樣是通過(guò)CAS操作來(lái)進(jìn)行的。

    如果對(duì)象的Mark Word 仍然指向線(xiàn)程的鎖記錄,那就用CAS操作把對(duì)象當(dāng)前的Mark Wrod和線(xiàn)程中復(fù)制的Displaced Mark Word替換回來(lái)。
    假如能夠替換,那整個(gè)同步過(guò)程就順利完成了;
    如果替換失敗,則說(shuō)明有其他線(xiàn)程嘗試過(guò)濾獲取該鎖,就要在釋放鎖的同時(shí),喚醒被掛起的線(xiàn)程。

    輕量級(jí)鎖總結(jié):
    輕量級(jí)鎖能提升性能的依據(jù)是:**“對(duì)于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的”。
    如果沒(méi)有競(jìng)爭(zhēng),輕量級(jí)鎖便通過(guò)CAS操作成功避免了使用互斥量的開(kāi)銷(xiāo);但如果確實(shí)存在鎖競(jìng)爭(zhēng),除了互斥量的本身開(kāi)銷(xiāo)外,還額外發(fā)生了CAS操作的開(kāi)銷(xiāo)。**因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖反而會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。

    13.3.5 偏向鎖

    偏向鎖的意義:
    偏向鎖的目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行性能。
    如果說(shuō)輕量級(jí)鎖是在無(wú)競(jìng)爭(zhēng)的情況下使用CAS操作消除同步使用的互斥量,那偏向鎖就是在無(wú)競(jìng)爭(zhēng)的情況下把整個(gè)同步都消除掉,連CAS操作都不去做了。

    偏向鎖的定義:
    這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線(xiàn)程,如果在接下來(lái)的執(zhí)行過(guò)程中,該鎖一直沒(méi)有被其他線(xiàn)程獲取,則持有偏向鎖的線(xiàn)程將用于不需要在進(jìn)行同步。

    偏向鎖加鎖過(guò)程
    當(dāng)虛擬機(jī)啟動(dòng)了偏向鎖,那么當(dāng)鎖對(duì)象第一次被線(xiàn)程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象頭中的標(biāo)志位設(shè)置為“01”、把偏向模式設(shè)置為“1”,表示進(jìn)入偏向模式。
    同時(shí)使用CAS操作把獲取到這個(gè)鎖的線(xiàn)程ID記錄在對(duì)象的Mark Word之中。
    如果CAS操作成功,持有偏向鎖的線(xiàn)程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊是,虛擬機(jī)都可以不再進(jìn)行任何同步操作。

    偏向鎖解鎖過(guò)程
    當(dāng)出現(xiàn)另外一個(gè)線(xiàn)程區(qū)嘗試獲取這個(gè)鎖的情況,偏向模式就馬上宣告結(jié)束。根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài)決定是否撤銷(xiāo)偏向(偏向模式設(shè)置為“0”),撤銷(xiāo)后標(biāo)志位恢復(fù)到未鎖定(標(biāo)志位“01”)或輕量級(jí)鎖定(標(biāo)志位為“00”)的狀態(tài),后續(xù)的同步操作就按照上面介紹的輕量級(jí)鎖那樣去執(zhí)行。

    參考鏈接

  • 深入了解java虛擬機(jī)
  • 泛型相關(guān)
  • 裝箱、拆箱
  • 鎖優(yōu)化
  • 總結(jié)

    以上是生活随笔為你收集整理的《深入理解Java虚拟机:JVM高级特性与最佳实践》 (第3版)周志明 著的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。