JVM內(nèi)存模型及JIT運行優(yōu)化

JVM內(nèi)存模型定義

  • JVM不僅承擔(dān)了Java字節(jié)碼的分析(JIT)和執(zhí)行(Runtime),同時也內(nèi)置了自動內(nèi)存分配管理機制
  • 內(nèi)存模型圖解


    image
  • 堆是jvm內(nèi)存中最大的一塊內(nèi)存空間,該空間被所有線程共享,幾乎所有的對象和數(shù)組都被分配到了堆內(nèi)存中: 堆被劃分為新生代和老年代,新生代劃分為Eden和Survivor區(qū),Suvivor是由From Survivor和To Survivor組成
    • java6中,永久代在非堆內(nèi)存去
    • java7中,永久代的靜態(tài)變量和運行時常量池被合并到 了堆中
    • java8中,永久代被元空間取代了,元空間存儲靜態(tài)變量和運行時常量池跟java7永久代一樣兒,都移到了堆中中


      image
程序計數(shù)器
  • 是一塊很小的內(nèi)存空間,主要用來記錄各個線程執(zhí)行的字節(jié)碼地址 例如:分支,循環(huán),跳轉(zhuǎn),異常,線程恢復(fù)都能依賴于計數(shù)器
  • 注意: 每個線程有一個單獨的程序計數(shù)器來記錄下一條運行的指令
方法區(qū)
  • 在HotSpot虛擬機使用永久代來實現(xiàn)方法區(qū),在其他虛擬機中不是這樣的,只是在HotSpot虛擬機中,設(shè)計人員使用了永久代實現(xiàn)了JVM規(guī)范的方法區(qū)
  • 方法區(qū)主要用來存放已被虛擬機加載的類相關(guān)信息 : 類信息,運行時常量池,字符串常量池(class、運行時常量池、字段、方法、代碼、JIT代碼等)
    • 類信息包括了類的版本,字段,方法,接口和父類等信息
    • JVM執(zhí)行類加載步驟:加載,連接(驗證,準(zhǔn)備,解析三個階段),初始化,在加載類的時候,JVM會先加載class文件,在class文件中除了有類的版本,字段,方法和接口等描述信息外,還有一項信息是常量池,用于存放編譯期間生成的各種字面量和符合引用
      • 字面量包括字符串(String a = "hello"),基本類型的常量(final修飾的變量)
      • 符號引用包括類和方法的全限定名(如String為Java/lang/String),字段的名稱和描述符以及方法的名稱和描述符
    • 當(dāng)類加載到內(nèi)存中后,JVM就會將class文件常量池中的內(nèi)容存放到運行時常量池中,在解析階段,JVM會把符號引用替換為直接引用(對象的索引值)
      • 比如:類中的一個字符串常量在class文件中時,存放在class文件常量池中的
    • 在JVM加載完類后,JVM會將這個字符串常量放到運行時常量池中,并在解析階段,指定改字符串對象的索引值
    • 運行時常量池是全局共享的,多個類中共用一個運行時常量池,class文件中常量池多個相同的字符串在運行時常量池中只會存在一份
    • 方法區(qū)與堆空間類似,也是一個共享內(nèi)存區(qū),所以方法區(qū)是線程共享的,如果有兩個線程試圖都訪問方法區(qū)中的一個類信息,而這個類還沒有裝入JVM中,那么此時就只允許一個線程去加載它,另一個線程必須等待
    • 永久代:包括靜態(tài)變量和運行時常量池,永久代的類等數(shù)據(jù)
      • Java7中將永久代的靜態(tài)變量和運行時常量池轉(zhuǎn)移到了堆中,其余部分則存儲在JVM的非堆內(nèi)存中(當(dāng)依然在JVM內(nèi)存中)
      • Java8中將方法區(qū)中實現(xiàn)的永久代去掉,使用元空間替代,并且元空間的存儲位置為本地內(nèi)存(不在JVM內(nèi)存中,而是直接存在內(nèi)存中的),之前永久代的類的元數(shù)據(jù)存儲在了元空間,而永久代的靜態(tài)變量以及運行時常量池跟Java7一樣轉(zhuǎn)移到了堆中
      • 元空間:存儲的是類的元數(shù)據(jù)信息:關(guān)于數(shù)據(jù)的數(shù)據(jù)或者叫做用來描述數(shù)據(jù)的數(shù)據(jù):就是最小的數(shù)據(jù)單元,元數(shù)據(jù)可以為數(shù)據(jù)說明其元素或?qū)傩?名稱,大小,數(shù)據(jù)類型等),其結(jié)構(gòu)(長度,字段,數(shù)據(jù)列),或其相關(guān)數(shù)據(jù)(位于何處,如何聯(lián)系,擁有者等)
      • 為何使用元數(shù)據(jù)區(qū)替代永久代
        1. 字符串存在永久代中,容易出現(xiàn)性能問題和內(nèi)存溢出
        2. 類及方法的信息等都比較難確定其大小,因此對于永久代的大小指定比較困難(默認8M),大小容易出現(xiàn)永久代溢出,太大則容易導(dǎo)致老年代溢出
        3. 永久代會為GC帶來不必要的復(fù)雜度,并且回收效率偏低
        4. 最重要的是Oracle想將HotSpot與JRockit(沒有永久代概念)虛擬機合二為一
虛擬機棧
  • Java虛擬機棧是線程私有的內(nèi)存空間,它跟Java線程一起被創(chuàng)建,當(dāng)創(chuàng)建一個線程時,會在虛擬機棧中申請一個棧幀,用來保存方法的局部變量,操作數(shù)棧,動態(tài)鏈接方法和返回地址等信息,并參與方法的調(diào)用和返回
  • 每個方法的調(diào)用都是一個入棧操作,方法的返回則是棧幀的出棧操作
    • 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(bloolean,byte,char,short,int ,float ,long,double: 64位的long和double類型數(shù)據(jù)占用2個局部變量空間Slot,其余都占1個),對象引用refrencele:不是對象本身,可能是一個指向?qū)ο笃鹗嘉恢玫囊弥羔槪部赡苁侵赶蛞粋€代表對象的句柄, returnAddress類型,指向一條字節(jié)碼指令的地址
    • 每8個bit組成1byte字節(jié),cpu每次只能訪問1個字節(jié),而不能單獨訪問具體的1個小格子,1byte就是內(nèi)存的最小的IO單元,所以在對象中有個對其填充,對象不滿8bit,需要補齊
    • 直接尋址技術(shù): 是計算機軟硬件的標(biāo)準(zhǔn)技術(shù)之一,cpu只要知道要訪問的數(shù)據(jù)的內(nèi)存地址,就能直接到內(nèi)存的對應(yīng)位置去訪問數(shù)據(jù)
    • 計算32位操作系統(tǒng)的最大內(nèi)存: 一個內(nèi)存地址占用1byte字節(jié),2^32 /1024/1024 = 4096M = 4G內(nèi)存
    • 計算機操作系統(tǒng)會給內(nèi)存每1個字節(jié)分配1個內(nèi)存地址,CPU只需要知道某個數(shù)據(jù)類型地址,就可以直接去讀取內(nèi)存位置上提取數(shù)據(jù)了
    • 64位操作系統(tǒng)支持 4G * 4G = 17億多GB內(nèi)存,實際上16G已經(jīng)不錯了
    • 對于32位操作系統(tǒng)一個對象指針(內(nèi)存地址為 32位bit值,所以1個指針就需要4byte字節(jié)),對于一個64位操作系統(tǒng)來說,一個內(nèi)存地址是64位的二進制數(shù),占用8個字節(jié)存放1個地址指針
  • 局部變量表所需的內(nèi)存空間在編譯期間完成分配,方法需要分配的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小
本地方法棧
  • 同Java虛擬機棧功能類似,Java虛擬機棧用來管理java函數(shù)調(diào)用的,本地方法棧用來管理本地方法的調(diào)用,是由C語言實現(xiàn)的
擴展: Java類中數(shù)據(jù)存儲位置
  1. 全局變量: 全部存放在靜態(tài)存儲區(qū),在程序開始執(zhí)行時給全局變量分配存儲區(qū),程序完畢就釋放,在執(zhí)行過程中占據(jù)固定的存儲單元:在類加載過程中的準(zhǔn)備階段進行初始化為默認值,在<clinit>初始化中賦值成程序給與的指定值,所以全局變量可以不用指定值,但是局部變量必須初始化指定值方可使用,他們在棧幀中分配不參與類初始化中
  2. 局部變量: 棧幀中分配,一個棧幀需要多大內(nèi)存在編譯器就確定完成,程序運行階段不會更改(棧幀中包括: 局部變量表, 操作數(shù)棧, 動態(tài)鏈接,方法返回地址及附加信息)局部變量存儲在局部變量表中(相當(dāng)于數(shù)組,可以通過正整數(shù)索引引用,通常索引0位this類),在編譯階段確定其大小long,double在32位中占用兩個Slot,Refrence對象指針占用一個Slot(32位4個字節(jié),64位8個字節(jié),只存在局部變量表中,如果在堆中跟實際類型一致)

JIT運行時編譯(優(yōu)化Java)

類編譯加載執(zhí)行過程
  • Java編譯到運行過程


    image
  1. 類編譯
    • 將.java文件編譯成.class文件(使用javac命令生成),編譯后的字節(jié)碼文件主要包括常量池和方法表集合這兩個部分
    • 常量池主要記錄的是類文件中出現(xiàn)的字面量以及符號引用
      • 字面常量包括字符串常量,聲明為final的屬性以及一些基本類型的屬性
      • 符號引用包括類和接口的全限定名,類引用,方法引用以及成員變量引用(如String str = "abc",其中str就是成員變量引用,通過javap -verbose -p可查看類常量表中的全限定名等)
  2. 類加載
    • 當(dāng)一個類被創(chuàng)建實例或者被其他對象引用時,虛擬機在沒有加載過該類情況下,會通過類加載器將字節(jié)碼文件加載到內(nèi)存中
    • 不同的實現(xiàn)類有不同的類加載器加載,JDK中本地方法類一般由根加載器加載,JDK中內(nèi)部實現(xiàn)的擴展類一般由擴展加載器實現(xiàn)加載,程序中的類文件則由系統(tǒng)加載器實現(xiàn)加載
    • 在類加載后,class類文件中的常量池信息以及其他數(shù)據(jù)會被保存到JVM內(nèi)存的方法區(qū)中
    • 主要包括以下步驟:
      1. 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流
      2. 將這個字節(jié)流代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
      3. 在內(nèi)存中生成一個代表這類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
    • 應(yīng)用:
      1. 從zip包中讀取class,最終成為日后Jar包,ear,war格式基礎(chǔ)
      2. 從網(wǎng)路中獲取,熱更新等
      3. 運行時計算生成,動態(tài)代理,將特定接口生成如$Proxy代理類的二進制字節(jié)流
  3. 類鏈接: 驗證,準(zhǔn)備,解析
    • 驗證: 驗證類符合Java規(guī)范和JVM規(guī)范,在保證符合規(guī)范的前提下,避免危害虛擬機安全,因此驗證階段是否嚴(yán)謹直接決定了JVM是否能夠承受惡意代碼攻擊
      • 四個階段驗證:
      1. 文件格式驗證: 驗證class文件字節(jié)流是否符合class文件格式,并能夠被當(dāng)前版本的JVM處理(魔數(shù),主版本,次版本,常量池(檢查常量的tag是否有不被支持的(acc_synchronized等))等校驗)
        • 目的是為了保證輸入的字節(jié)流能正確的解析并存儲于方法區(qū)之中,只有通過這個階段,下面三個才能進行,不會在直接操作字節(jié)流
      2. 元數(shù)據(jù)驗證:主要對字節(jié)碼描述信息進行語義分析保證其描述信息符合java語言規(guī)范要求
        • 這類是否有父類
        • 這個類的父類是否不允許繼承(被final修飾)
        • 如果這個類不是抽象類,是否實現(xiàn)了其父類或接口之中要求必須實現(xiàn)的所有方法
        • 類中的字段,方法是否同父類產(chǎn)生沖突(覆蓋了父類的final方法,或者出現(xiàn)不符合的方法重載,方法參數(shù)一致,只有返回值不同這種方法簽名是一致的,注意同橋接方法中JVM的方法簽名區(qū)分:泛型中子類重寫父類的get屬性具體參考https://www.cnblogs.com/jixp/articles/10264034.html)
      3. 字節(jié)碼驗證: 通過數(shù)據(jù)流和控制流分析,確定程序語義是合法,符合邏輯的,當(dāng)2中對元數(shù)據(jù)信息中的數(shù)據(jù)類型進行校驗分析后,這個階段保證被校驗的類的方法在運行時不會做出危害JVM安全事件
        • 保證操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列配合工作,比如iconst_1,其后卻 lload_0導(dǎo)致數(shù)據(jù)類型不一致
        • 保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
        • 保證方法體的類型轉(zhuǎn)化是有效的,如子類賦值給父類安全,父類不能賦值給子類等
      4. 符號引用驗證: JVM將符號引用轉(zhuǎn)化為直接引用階段,主要是對類自身以外信息進行校驗,確保解析動作能夠完成
        1. 符號引用中通過字符串描述的全限定名是否能夠找到對應(yīng)的類
        2. 在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段(類中是否有方法名等)
        3. 符號引用中的類,字段,方法訪問性(public,protected,private,default)是否能夠被當(dāng)前類所訪問
        4. 符號引用驗證為了確保解析動作能夠正常執(zhí)行,如果不能通過,則會拋出IllegalAccessError,NoSuchFieldError,NoSuchMethodError等異常;
      • 如果我們確定代碼沒有問題,可以使用 -Xverfity:none來關(guān)閉大部分驗證從而節(jié)省時間
    • 準(zhǔn)備: 為類的靜態(tài)變量分配內(nèi)存,初始化為系統(tǒng)的初始值,對于final static修飾的常量,直接賦值為用戶定義值,對于static修飾變量會賦值為默認初始值
      private final static int value = 123 //賦值為123
      private static int lala = 123 //賦值為0
      
      • 注意: 內(nèi)存分配的僅包括類變量,不包括實例變量(<init>方法時,同對象的實例化隨著對象一起分配在堆中的)
      • 當(dāng)被final修飾類變量時,比如上方代碼解析字段屬性表為:編譯時javac會為value生成ConstantValue屬性的
          #17 = Utf8               value //value值有ConstantValue屬性在準(zhǔn)備階段初始化為指定值123,
          //而下方的lala則被初始化為默認值0
          #18 = Utf8               I
          #19 = Utf8               ConstantValue
          #20 = Integer            123  //value初始化為123
          #21 = Utf8               lala  //初始化lala 為 0 
        
    • 解析: 將符號引用轉(zhuǎn)為直接引用的過程:編譯時,Java類并不知道所引用的類的實際地址,因此只能使用符號引用來代替
      • 類結(jié)構(gòu)文件的常量池中存儲了符號引用:包括類和接口的全限定名,類引用,方法引用以及成員變量引用等,如果需要使用以上類和方法,就西藥將他們轉(zhuǎn)化為JVM可以直接獲取的內(nèi)存地址或指針,即直接引用;
      • 解析動作主要針對 類或接口,字段,類方法,接口方法,方法類型,方法句柄和調(diào)用限定符的符號引用
  4. 類初始化
    • 類初始化是類加載的最后一個階段,初始化時,JVM首先將執(zhí)行構(gòu)造器<clinit>方法,編譯器會將.java文件編譯成.class文件時,收集所有類初始化代碼,包括靜態(tài)變量賦值語句,靜態(tài)代碼塊,靜態(tài)方法,收集在一起成為<clinit>方法
    • 初始化類的靜態(tài)變量和靜態(tài)代碼塊為用戶自定義的值,初始化的順序和Java源碼從上到下的順序一致(在準(zhǔn)備階段設(shè)置過默認值,在<clinit>方法中設(shè)置用戶定義的值)
    • 子類初始化時會首先調(diào)用父類的<clinit>()方法,在執(zhí)行子類的<clinit>方法
    • JVM會保證<clinit>()方法的線程安全,保證同一時間只有一個線程執(zhí)行,不論是靜態(tài)內(nèi)部類還是非靜態(tài)內(nèi)部類都是在第一次使用時才會被加載
      • 注意一個小例子: 在序列化和反序列化靜態(tài)變量都是不會參與的,因為序列化保存的是對象的狀態(tài),而靜態(tài)變量屬于類的狀態(tài),因此序列化并不會保存靜態(tài)變量
      //使用于靜態(tài)內(nèi)部類生成單例模式
      public class SingleTonFactory {
      
          private SingleTonFactory(){}
          //靜態(tài)內(nèi)部類不會引用外部類對象,不會內(nèi)存泄漏
          //當(dāng)引用類的靜態(tài)變量才會加載該Instance類,且賦值操作在<clinit>方法內(nèi),JVM保證其線程安全,且只執(zhí)行一次,得到的自然是單例對象
          public static class Instance{ 
              static SingleTonFactory instance = new SingleTonFactory();
          }
          public SingleTonFactory getInstance(){
              return Instance.instance ; 
          }
      }
      
    • <clinit>對于類和接口來說并不是一定的,如果一個類沒有靜態(tài)代碼塊,也沒有對類變量的賦值操作,則編譯器不會為該類生成<clinit>方法
    • JVM在初始化執(zhí)行代碼時,如果實例化一個新對象,會調(diào)用<init>方法對實例變量就行初始化,并執(zhí)行對應(yīng)的構(gòu)造方法內(nèi)的代碼
    • 注意: 類變量或者static修飾的類的靜態(tài)代碼塊執(zhí)行順序先執(zhí)行父類在執(zhí)行子類,同一個類由代碼順序決定的,注意<Clinit>方法由于<init>方法,所以構(gòu)造函數(shù)后與static執(zhí)行,也是由父類->子類,有一個特殊的情況
      static {
          i = 1 ; //對后面定義的類變量賦值是允許的,但是不能訪問,下面的句子報錯
      //  System.out.println(i);
      }
      public static int i = 2 ;
      
      • 解釋: 通過上面我們知道i的默認初始值是在準(zhǔn)備階段設(shè)置為 i = 0了,而初始化階段執(zhí)行的是靜態(tài)代碼塊的執(zhí)行和靜態(tài)變量的賦值,因此當(dāng)對i=1賦值時,此時i存在且為默認值0 ,但是卻不能訪問,因為他違反了happened before原則,不能通過編譯,是一個JVM規(guī)范
      • 這個報的錯是非法向前引用。這其實是一個語法規(guī)定,對于靜態(tài)變量,你可以在它的聲明前面賦值,但是不允許你在它的聲明前面訪問
      • 靜態(tài)代碼塊 -> 非靜態(tài)代碼塊 -> 構(gòu)造函數(shù) ,且靜態(tài)代碼塊只在加載類時執(zhí)行一次,而非靜態(tài)代碼塊同構(gòu)造函數(shù)是隨著對象執(zhí)行的
初始化時機,當(dāng)且僅有以下幾種情況:
  1. 創(chuàng)建類的實例: new方式
  2. 訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值(被final修飾已在編譯器放入常量池的靜態(tài)字段除外)
  3. 調(diào)用類的靜態(tài)方法
  4. 反射調(diào)用
  5. 初始化某個類的子類,則其父類也會被初始化
  6. jvm啟動時被標(biāo)明為啟動類的類(javaTest),直接使用java.exe命令來運行某個主類
  • 注意一下情況不會初始化類:
    1. 通過子類類名調(diào)用父類的靜態(tài)代碼,只會初始化父類,不會觸發(fā)子類的初始化
    2. 通過數(shù)組來創(chuàng)建對象不會觸發(fā)此類的初始化 StringTest[] strArray = new StringTest[10]并不會初始化類
    3. 通過調(diào)用靜態(tài)常量不會觸發(fā)初始化,比如調(diào)用final static修飾的其他類編譯器已經(jīng)替換成常量了,并不會調(diào)用其他類的初始化
  • 上面實例說到了final,那么我們就看看final的含義吧?

final關(guān)鍵字

  • 首先記住兩點:
    1. final修飾的類不可被繼承,但該類可以繼承其他類;
    2. final修飾的方法不可以被覆蓋,但可以被繼承使用;
      • 1,2反編譯class文件發(fā)現(xiàn)無論類屬性,還是方法屬性中的flags都有ACC_FINAL屬性標(biāo)識
    3. final修飾的變量
      1. 如果是基本數(shù)據(jù)類型,只能被賦值一次,不能在改變;
      2. 如果是指向?qū)ο蟮囊茫刂分挡荒芨淖儯刂分当坏膶傩詫ο罂梢愿?
  • final 方法在編譯階段綁定,稱為靜態(tài)綁定
  • 將類,方法,變量聲明為final能夠提高性能,JVM有機會進行優(yōu)化
    1. 提高了性能,常量池中緩存final變量
    2. final變量多線程并發(fā)安全,無需同步
    3. final方法時靜態(tài)編譯的,提高了調(diào)用速度,不存在多態(tài)動態(tài)分配
    4. final類創(chuàng)建的對象時只讀的,在多線程可以安全共享
final用法
  1. 空白final
    • final修飾的成員變量并不一定要立刻賦值,而是可以先聲明,而不給初始值,這種變量就是final空白,編譯器確保空白final在使用之前必須被初始化;同時給與final使用上更大的靈活性,根據(jù)對象不同而改變,卻有保持其值一旦賦值永恒不變特性
      public class FinalNull{
          final int a ;  //這個就是空白final
          final int b=3; 
          public FinalNull(int i){
              a = i ; //可以正常賦值的
              //b = 2 ; //不能再次賦值了
          }
      }
      
匿名內(nèi)部類訪問局部變量必須加final
  • 主流去有兩種觀點:
內(nèi)部類對象的生命周期和局部變量的生命周期不一致
  • 局部變量定義在方法內(nèi),是在java棧上分配的,隨著方法運行結(jié)束而終結(jié),但是內(nèi)部類此時并不一定結(jié)束,當(dāng)方法結(jié)束時,局部變量已死亡,但內(nèi)部類中在引用就會失敗,所以要求只要匿名內(nèi)部類對象還活著,那么棧中的數(shù)據(jù)就不能死亡
  • 解決方式: 將方法的局部變量使用final修飾,定義為final后,編譯器會把匿名內(nèi)部類對象要訪問的所有final類型局部變量,都拷貝一份作為該對象的成員變量,這樣即使棧中局部變量已死亡,匿名內(nèi)部類中依然可以拿到局部變量的值,因為他自己拷貝了一份,且與原來局部變量值始終保持一致(final類型不可變)
    • 基本數(shù)據(jù)類型,直接拷貝值
    • 引用類型表示兩個指針對應(yīng)相同位置,當(dāng)方法的局部變量死亡,但內(nèi)部類中依然可以通過指針找到對應(yīng)的引用對象(this.value = 外部value (被final修飾的))
    • Java8中如果局部變量被匿名類使用,會自動使用final修飾,只要你不在匿名內(nèi)部類中修改值,即使不適用final修飾他也是不會報錯的
    • https://blog.csdn.net/jiao_zg/article/details/78911469
匿名內(nèi)部類訪問局部變量,局部變量的拷貝會導(dǎo)致不一致
  • 匿名內(nèi)部類來自外部閉包環(huán)境的自由變量必須是final的
    • 如果不是final的,則內(nèi)部類會拷貝一份外部對象,但是在其內(nèi)部可以重新指向新的地址,導(dǎo)致變量不同步,指向了不同的對象
    • 由于一個拷貝,導(dǎo)致內(nèi)外兩個變量無法實時同步,其中一方修改,另外一方都無法同步修改,所以要求final限制不能修改
    • 為何要拷貝呢? JVM的局部變量是在虛擬機棧上,這個變量無法進行共享,因此匿名內(nèi)部類無法直接訪問,因此只能通過值傳遞的方式,傳遞到匿名內(nèi)部類中
    • 當(dāng)變量時成員變量時,則會分配到堆中,他是一個共享數(shù)據(jù)區(qū),我們可以通過this.成員變量獲取,因此在創(chuàng)建內(nèi)部類時,無需進行拷貝,甚至都無需將這個變量傳遞給內(nèi)部類,直接通過this即可獲取也就不用final修飾了
自由變量
  • 在A作用域中使用的變量X,卻沒有在A作用域中聲明,而是在其他作用域中聲明的,則對于A作用域來說X就是一個自由變量
    • 因此在內(nèi)部類中使用的外邊變量對于內(nèi)部類來說就是一個自由變量
外部閉包
  • 外部環(huán)境如果持有內(nèi)部函數(shù)所使用的自由變量,就會對內(nèi)部函數(shù)形成閉包
  • 匿名內(nèi)部類來自外部閉包環(huán)境的自由變量必須是final,除非自由變量來自類的成員變量(分配在堆上,是共享數(shù)據(jù))
  • 思考: 成員內(nèi)部類中不能有靜態(tài)變量和靜態(tài)方法?
    • 成員內(nèi)部類是屬于外部對象,不是屬于類的,跟著外部對象的生命周期一致,而靜態(tài)變量和靜態(tài)方法是屬于類的變量和方法,自然不能存在靜態(tài)的了

思考題

  • 思考題: 反射中Class.forName()和ClassLoader.loadClass()的區(qū)別
裝載:通過累的全限定名獲取二進制字節(jié)流,將二進制字節(jié)流轉(zhuǎn)換成方法區(qū)中的運行時數(shù)據(jù)結(jié)構(gòu),在內(nèi)存中生成Java.lang.class對象; 
鏈接:執(zhí)行下面的校驗、準(zhǔn)備和解析步驟,其中解析步驟是可以選擇的; 
    校驗:檢查導(dǎo)入類或接口的二進制數(shù)據(jù)的正確性;(文件格式驗證,元數(shù)據(jù)驗證,字節(jié)碼驗證,符號引用驗證) 
    準(zhǔn)備:給類的靜態(tài)變量分配并初始化存儲空間; 
    解析:將常量池中的符號引用轉(zhuǎn)成直接引用;
初始化:激活類的靜態(tài)變量的初始化Java代碼和靜態(tài)Java代碼塊,并初始化程序員設(shè)置的變量值。

Class.forName(className)方法,內(nèi)部實際調(diào)用的方法是  Class.forName(className,true,classloader);
第2個boolean參數(shù)表示類是否需要初始化,  Class.forName(className)默認是需要初始化。
一旦初始化,就會觸發(fā)目標(biāo)對象的 static塊代碼執(zhí)行,static參數(shù)也也會被再次初始化。

ClassLoader.loadClass(className)方法,內(nèi)部實際調(diào)用的方法是  ClassLoader.loadClass(className,false);
第2個 boolean參數(shù),表示目標(biāo)對象是否進行鏈接,false表示不進行鏈接,由上面介紹可以,
不進行鏈接意味著不進行包括初始化等一些列步驟,那么靜態(tài)塊和靜態(tài)對象就不會得到執(zhí)行
Java對象的創(chuàng)建
  • 虛擬機遇到一條new指令,首先檢查這個指令的參數(shù)是否能在常量池中定位一個類的符號引用,并檢查該符號引用代表的類是否已被加載,解析和初始化過,如果沒有就先執(zhí)行類加載過程
  • 類加載過以后,虛擬機就會為新生對象分配內(nèi)存: 對象所需內(nèi)存大小在類加載完成后便可完全確定,為對象分配空間就相當(dāng)于把一塊確定大小的內(nèi)存從Java堆中劃分出來
    • 對于使用Serial,ParNew等帶有整理過程的GC,系統(tǒng)采用分配算法是指針碰撞: 堆中內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個指針作為分界點的指示器,當(dāng)分配內(nèi)存時就將指針向空閑空間那邊移動一段與對象大小相等的距離;
    • 對于使用CMS這種局域Mark_Sweep算法GC,通常采用空閑列表: Java堆內(nèi)存并不規(guī)整,已使用內(nèi)存和空閑內(nèi)存相互交錯,虛擬機必須維護一個列表記錄那些內(nèi)存塊是可用的,在分配時從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄
  • 當(dāng)對象分配內(nèi)存可能帶來多線程安全性為題: 解決方式有兩種
    1. 使用CAS失敗重試保證更新分配操作的原子性
    2. 按照線程劃分在不同的空間之中進行,每個線程在Java堆中預(yù)先分配一小塊內(nèi)存即TLAB線程本地緩存區(qū),只有不足時才需要同步鎖定,是否使用 -Xx:+/-UseTLAB
  • 對象訪問方式: 一個對象已經(jīng)在堆中創(chuàng)建成功,那么棧中如何訪問它呢?
    1. 使用句柄: 在Java堆中劃分出一塊內(nèi)存作為句柄池,reference存儲的就是對象的句柄地址,而句柄中包括了對象實例數(shù)據(jù)和類型數(shù)據(jù)各自的具體地址信息
      • JNI引用java采用這種方式,這是為了GC回收不用遍歷JNI方法找了,而是直接通過句柄池中指引的對象都不回收
      • 優(yōu)點: 存儲的是穩(wěn)定的句柄地址,當(dāng)對象被移動時(GC回收移動)只會改變句柄中實例數(shù)據(jù)的指針,而reference本身不需要修改,但是需要兩次引用,開銷大
    2. 直接指針: Java棧中使用,reference中存儲的是對象在堆中的地址,同時注意還加上對象類型數(shù)據(jù)的指針(類型數(shù)據(jù)在方法區(qū)中,如:Class對象信息)
      • 優(yōu)點: 速度更快,節(jié)省了一次指針定位的開銷,Hotspot使用該方式!
并發(fā)情況下創(chuàng)建對象的安全性
  • 當(dāng)一個線程正在給對象A分配內(nèi)存,指針還沒來得及修改,對象B有同時使用了原來的指針來分配內(nèi)存了,怎么解決呢?
    • 分配內(nèi)存是進行同步處理(CAS保證更新操作的原子性)'
    • 將內(nèi)存分配的操作按照線程劃分在不同的空間中進行,即每個線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB),那個線程分配內(nèi)存就在它自己的TLAB上分配,只有當(dāng)TLAB用來重新分配新的TLAB時,才需要同步鎖定
  • 當(dāng)內(nèi)存分配完成后,虛擬機將分配到的內(nèi)存空間都初始化為零值(不包括對象頭),如果使用TLAB,這一工作在TLAB分配時進行,保證了對象實例子段在Java代碼中可以不賦初始值直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值,全局變量不用賦值就可使用
  • 虛擬機對對象進行必要的設(shè)置:放在對象頭里面
  • 作為以上工作,一個新的對象產(chǎn)生了,但是從Java程序看,對象創(chuàng)建才剛剛開始,需要執(zhí)行<init>方法,所有字段都還為零,一般執(zhí)行new指令之后會接著執(zhí)行<init>方法,把對象按照我們的意愿進行初始化
對象的訪問定位
  • Java程序通過棧上的reference數(shù)據(jù)來操作堆上的具體對象,那么如何訪問主流方式有 使用句柄和直接指針
    • 句柄訪問: Java堆中劃分一塊內(nèi)存作為句柄池,reference中存儲的是對象的句柄地址,句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的地址信息
      • 優(yōu)點:reference中存儲的是穩(wěn)定的句柄地址,在對象GC被移動時只需要更改句柄中實例數(shù)據(jù)指針,reference本身不需要更改
      • 缺點: 需要增加一次指針定位的時間開銷
    • 直接指針訪問: Java堆對象中放置了類型數(shù)據(jù)相關(guān)信息(在方法區(qū)中),而reference中存儲的就是對象地址 HotSpot虛擬機采用的
      • 優(yōu)點:速度快,節(jié)省了一次指針定位時間開銷
虛擬機棧和本地方法棧溢出
  • HotSpot虛擬機并不區(qū)分虛擬機棧和本地方法棧,他們有兩種異常
    1. 如果線程請求的棧深度大于虛擬機鎖允許的最大深度,拋出棧溢出異常(1000~2000完全沒問題)
    2. 如果虛擬機在擴展棧時無法申請到足夠的內(nèi)存空間,拋出OOM異常(在過多線程中內(nèi)存溢出,如果不能減少線程數(shù)或更換62位虛擬機情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程)
即時編譯
  • 解釋執(zhí)行: 將編譯好的字節(jié)碼一行一行的翻譯為機器碼執(zhí)行 逐條翻譯字節(jié)碼為可運行機器碼,不用等待: 用于大部分不常用的代碼,無需耗時將其編譯為機器碼,逐條解釋運行
  • 編譯執(zhí)行: 以方法為單位,將字節(jié)碼一次性的翻譯為機器碼后執(zhí)行 多次運行效率更高,小部分的熱點代碼(反復(fù)執(zhí)行的重要代碼),先翻譯為符合機器的機器碼從而高效運行
  • 初始化完成后,類在調(diào)用執(zhí)行過程中,執(zhí)行引擎會把字節(jié)碼轉(zhuǎn)為機器碼,然后在操作系統(tǒng)中才能執(zhí)行在字節(jié)碼轉(zhuǎn)換為機器碼的過程中,虛擬機中還存在著一道編譯,即為即時編譯
    1. 虛擬機中的字節(jié)碼是由解釋器(Interpreter)完成編譯的,當(dāng)虛擬機發(fā)現(xiàn)某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定為熱點代碼
    2. 為了提高熱點代碼的執(zhí)行效率,在運行時 JIT會把這些代碼編譯成與本地平臺相關(guān)的機器碼,并進行層次的優(yōu)化,然后保存到內(nèi)存中
  1. 即時編譯器類型
    • HotSpot虛擬機中,內(nèi)置了兩個JIT,分別為C1編譯器和C2編譯器,他們的過程不同
      • C1編譯器是一個簡單快速的編譯器,主要關(guān)注點在局部性的優(yōu)化,適用于執(zhí)行時間較短或?qū)有阅苡幸蟮某绦颍鏕UI應(yīng)用對界面啟動速度有一定要求
      • C2編譯器是為長期運行的服務(wù)器端應(yīng)用程序做性能調(diào)優(yōu)的編譯器,適用于執(zhí)行時間較長或?qū)Ψ逯敌阅軆?yōu)要求的程序,這兩種編譯器也被稱為Client Compiler和Server Compiler
    • Java7之前,根據(jù)程序特性來選擇對應(yīng)的JIT,虛擬機默認采用解釋器和其中一個編譯器配合工作
    • Java7引入了分層編譯,綜合了C1啟動性能優(yōu)勢和C2的峰值性能優(yōu)勢,通過設(shè)置參數(shù)可強制更改
    • C1跟C2編譯器主要差別在于編譯代碼的時機不同(client比server編譯器要早,C2編譯器能夠更好的優(yōu)化,運行更快): 分層編譯: 代碼先由C1編譯,隨著代碼變熱,再有C2編譯
    • 分層將JVM的執(zhí)行狀態(tài)分為5個層次
      • 第0層: 程序解釋執(zhí)行,默認開啟性能監(jiān)控功能(Profiling),如果不開啟,可觸發(fā)第二層編譯
      • 第1層: 可稱為C1編譯,將字節(jié)碼編譯為本地代碼,進行簡單可靠的優(yōu)化,不開啟Profiling
      • 第2層:也稱為C1編譯,開啟Profiling,僅執(zhí)行帶方法調(diào)用次數(shù)和循環(huán)回邊執(zhí)行次數(shù)Profiling
      • 第3層 也稱為C1編譯,執(zhí)行所有帶Profiling的C1編譯
      • 第4層 可稱為C2編譯,也是將字節(jié)碼編譯成本地代碼,但是會啟用一些編譯耗時較長的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進行一些不可靠的激進優(yōu)化
      • 通過 java -version 命令行可查看當(dāng)前系統(tǒng)使用的編譯模式
        java --version -> mixed mode :混合編譯模式
        java -Xint -version -> interpreted mode : 只有解釋器編譯,關(guān)閉JIT
        java -Xcomp -version -> compiled mode: 只有JIT編譯,關(guān)閉解釋器編譯
        
熱點探測:JVM編譯優(yōu)化條件
  • HotSpot虛擬機的熱點探測是基于計數(shù)器的熱點探測,虛擬機會為每個方法建立計數(shù)器統(tǒng)計方法的執(zhí)行次數(shù),如果次數(shù)超過一定的閾值就認為為熱點方法
  • 虛擬機為每個方法準(zhǔn)備了兩類計數(shù)器:方法調(diào)用計數(shù)器(Invocation Counter)和回邊計數(shù)器(Back Edge Counter) ,在確定虛擬機運行參數(shù)的前提下,這兩個計數(shù)器都有一個確定的閾值,當(dāng)計數(shù)器超過這個閾值就會觸發(fā)JIT編譯
  • 方法調(diào)用計數(shù)器: 用于統(tǒng)計方法被調(diào)用的次數(shù),默認閾值在C1模式下1500次,在C2模式下是1萬次,而在分層編譯下,將會根據(jù)當(dāng)前待編譯的方法數(shù)以及編譯線程數(shù)來動態(tài)調(diào)整
  • 回邊計數(shù)器: 用于統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為回邊(Back Edge),在不開啟分層編譯的情況下,C1默認13995,C2默認10700,在分層情況下,將根據(jù)當(dāng)前編譯的方法數(shù)以及編譯線程數(shù)來動態(tài)調(diào)整
  • 建立回邊計數(shù)器主要目的是為了出發(fā)OSR(On StackReplacement)編譯,即棧上編譯,對于一些循環(huán)周期比較長的代碼段,當(dāng)循環(huán)達到回邊計數(shù)器閾值時,JVM認為這段是熱點代碼,JIT編譯器就會將其編譯成機器語言并緩存,并在該循環(huán)時間段內(nèi),執(zhí)行緩存的機器語言

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

方法內(nèi)聯(lián)
  • 由于調(diào)用一個方法通常要經(jīng)歷壓棧和出棧:調(diào)用方法是將程序執(zhí)行順序轉(zhuǎn)移到存儲該方法的內(nèi)存地址,將方法的內(nèi)容執(zhí)行完成后,在返回到執(zhí)行該方法前的位置
  • 這樣執(zhí)行要求執(zhí)行前保護線程并記憶執(zhí)行的地址,執(zhí)行后恢復(fù)現(xiàn)場并按照原來保存的地址繼續(xù)執(zhí)行該方法調(diào)用會纏上一定的時間和空間方面的開銷
  • 但是對于方法體代碼不大有頻繁調(diào)用的方法,這個開銷就很大了
  • 方法內(nèi)聯(lián)的優(yōu)化就是將目標(biāo)方法的代碼復(fù)制到發(fā)起調(diào)用的方法之中,避免發(fā)生真是的方法調(diào)用,如kotlin擴展函數(shù)中的inline關(guān)鍵字
  • JVM會自動識別熱點方法,并對它們使用方法內(nèi)聯(lián)進行優(yōu)化,但是熱點方法并不一定會被JVM做內(nèi)聯(lián)優(yōu)化,如果這個方法太大將不會執(zhí)行內(nèi)聯(lián)操作
    • 經(jīng)常執(zhí)行的方法,默認情況下,方法體大小小于325字節(jié)都會進行內(nèi)聯(lián),可設(shè)置
    • 不是經(jīng)常執(zhí)行的方法,默認情況下,方法大小小于35字節(jié)才會進行內(nèi)聯(lián),可設(shè)置
    • 我們可以通過配置JVM參數(shù)參看(Intellij 類上Edit configurations 中設(shè)置VM options)
      -XX:+PrintCompilation // 在控制臺打印編譯過程信息
      -XX:+UnlockDiagnosticVMOptions // 解鎖對 JVM 進行診斷的選項參數(shù)。默認是關(guān)閉的,開啟后支持一些特定參數(shù)對 JVM 進行診斷
      -XX:+PrintInlining // 將內(nèi)聯(lián)方法打印出來
      
  • 熱點方法內(nèi)聯(lián)優(yōu)化可以有效提高系統(tǒng)性能,我們有一下方法提高:
    • 通過設(shè)置JVM參數(shù)來減小熱點閾值或增加方法體閾值,但是需要占用更多的內(nèi)存
    • 在編程中,避免在一個方法中寫大量代碼,習(xí)慣使用小方法體
    • 盡量使用final,private ,static關(guān)鍵字修飾方法,編碼方法因為繼承,會需要額外的類型檢查
  • @HotSpotInstinsicCandidate標(biāo)注
    • JDK中為了搞笑實現(xiàn)基于CPU指令,運行時HotSpot維護了
逃逸分析
  • 逃逸分析基本行為就是分析對象動態(tài)作用域:當(dāng)一個對象在方法中被定義后,他可能被外部所引用,例如作為參數(shù)傳遞到其他地方中,稱為方法逃逸
     public static StringBuffer craeteStringBuffer(String s1, String s2) { //sb對象逃逸了
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb;
    }
    
    public static String createStringBuffer(String s1, String s2) { //sb對象沒有逃逸
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }
    
  • 使用逃逸分析,編譯器可以做一下優(yōu)化:(Jdk 1.7開始默認開啟)
    1. 同步省略: 如果一個對象被發(fā)現(xiàn)只能從一個線程被訪問到,那么對于這個對象的操作就可以不考慮同步
    2. 將堆分配轉(zhuǎn)化為棧分配:如果一個對象在子程序中被分配,要使其指向改對象的指針永遠不會逃逸,對象可以在棧上分配而不是堆分配
    3. 分離對象或標(biāo)量替換: 有的對象可能不需要作為一個連續(xù)的內(nèi)存結(jié)構(gòu)存在也可以被訪問到,那么對象的部分或全部可以不存儲在內(nèi)存,而是存儲在CPU寄存器中
同步省略(鎖消除)
  • 在動態(tài)編譯同步塊時,JIT編譯器會借助逃逸分析來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問兒沒有被發(fā)布到其他線程,如果是只能被一個線程訪問,則會取消這部分代碼的同步,比如在使用synchronized時,如果JIT經(jīng)過逃逸分析發(fā)現(xiàn)并無線程安全問題,就會做鎖消除
    public static String getString(String s1, String s2) {
        StringBuffer sb = new StringBuffer();//只在當(dāng)前線程,所以會取消同步操作,鎖消除
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }
    
    public void f() {
        Object hollis = new Object();
        synchronized(hollis) { //鎖方法內(nèi)部對象,會鎖消除
            System.out.println(hollis);
        }
    }
    //相當(dāng)于
    public void f() {
        System.out.println(hollis);
    }
    
棧上分配
  • Java默認創(chuàng)建一個對象在堆中分配內(nèi)存的,當(dāng)對象不再使用時,則需要通過垃圾回收機制回收,這個過程相對于分配在棧中的對象的創(chuàng)建和銷毀來說,更加消耗時間和性能.這個時候逃逸分析如果發(fā)現(xiàn)一個對象只在方法中使用,就會將對象分配在棧上
  • 遺憾的是:HotSpot虛擬機目前的實現(xiàn)導(dǎo)致棧上分配實現(xiàn)比較復(fù)雜,暫時沒有實現(xiàn)這項優(yōu)化,相信不久將來會實現(xiàn)的
  • 雖然這項技術(shù)并不十分成熟,但是她也是即時編譯器優(yōu)化技術(shù)中一個十分重要的手段
標(biāo)量替換
  • 標(biāo)量(Scalar)是指一個無法再分解成更小的數(shù)據(jù)的數(shù)據(jù),Java中的原始數(shù)據(jù)類型就是標(biāo)量
  • 聚合量:相對于標(biāo)量那些還可以分解的數(shù)據(jù)叫做聚合量,Java中的對象就是聚合量,因為他可以分解成其他聚合量和標(biāo)量(如String為 char[] 數(shù)組和int hash)
  • 應(yīng)用: 在JIT階段,如果經(jīng)過逃逸分析,發(fā)現(xiàn)一個對象不會被外界訪問,那么經(jīng)過JIT優(yōu)化,就會吧這個對象拆分成若干個其中包含的若干個成員變量來代替(當(dāng)程序真正執(zhí)行時不用創(chuàng)建這個對象,而是直接創(chuàng)建他的成員變量來代替,拆分后,可以分配對象的成員變量在棧或寄存器上,則原本的對象就無需分配內(nèi)存空間了),這個過程就是標(biāo)量替換
    public static void main(String[] args) {
        Point point = new Point(1,2);
          System.out.println("point.x="+point.x+"; point.y="+point.y);
    }
    
    class Point{
        public int x;
        public int y;
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    //Point對象會被替換成兩個int型
    public static void main(String[] args) {
        x = 1;
        y = 2;
        System.out.println("point.x="+ x +"; point.y="+ y);
    }
    
  • 逃逸分析測試代碼
    public class HelloTest {
        public static void alloc() {
            byte[] b = new byte[2];
            b[0] = 1;
        }
    
        public static void main(String[] args) {
            long b = System.currentTimeMillis();
                for (int i = 0; i < 100000000; i++) {
                    alloc();
                }
                long e = System.currentTimeMillis();
                System.out.println(e - b);
            }
        }
    }
    
  • 使用下方命令配置JVM(上面有如何在IDEA中配置,本身默認開啟了,可關(guān)閉查看數(shù)據(jù))
    //C1編譯器參數(shù) -client C2編譯器 -server
    //開/關(guān) 逃逸分析(JDK 6u23以上) 開/關(guān)鎖消除        開/關(guān)標(biāo)量替換                打印GC日志
    //-XX:+DoEscapeAnalysis -XX:+EliminateLocks -XX:+EliminateAllocations -XX:+PrintGCDetails -Xmx10m -Xms10m
    //-XX:-DoEscapeAnalysis -XX:-EliminateLocks -XX:-EliminateAllocations -XX:+PrintGCDetails -Xmx10m -Xms10m
    
    //開啟標(biāo)量替換結(jié)果
    [GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->672K(9728K), 0.0014005 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2720K->712K(9728K), 0.0007950 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [PSYoungGen: 2536K->504K(2560K)] 2760K->736K(9728K), 0.0015657 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    10
    //關(guān)閉標(biāo)量替換結(jié)果
    無數(shù)次GC,運行時長 1873毫秒
    
  • 總結(jié): 棧上的空間一般而言是非常小的,只能存放若干變化和小的數(shù)據(jù)結(jié)構(gòu),大容量的存儲結(jié)構(gòu)是做不到。這里的例子是一個極端的千萬次級的循環(huán),突出了通過逃逸分析,讓其直接從棧上分配,從而極大降低了GC的次數(shù),提升了程序整體的執(zhí)行效能。所以,逃逸分析的效果只能在特定場景下,滿足高頻和高數(shù)量的容量比較小的變量分配結(jié)構(gòu),才可以生效!
堆外內(nèi)存
  • 堆內(nèi)存的缺點:
    1. GC有成本,堆中對象越多,GC開銷越大
    2. 使用堆內(nèi)存進行文件,網(wǎng)絡(luò)IO時,JVM會使用堆外內(nèi)存做一次額外的中轉(zhuǎn),也就是多一次內(nèi)存拷貝
  • 而相對應(yīng)的,堆外內(nèi)存就是把內(nèi)存對象分配在Java虛擬機堆外的內(nèi)存,這些內(nèi)存直接受操縱系統(tǒng)管理(不是JVM)
    • 一定程度上減少GC對應(yīng)用程序造成的影響
      ####### 堆外內(nèi)存實現(xiàn)
  1. 使用ByteBuffer.allocateDirect(分配byte數(shù)組大小),得到一個DirectByteBuffer對象,堆內(nèi)存回收,讀寫封裝
  2. 調(diào)用Unsafe.allocateMemory分配,只能在JDK代碼中調(diào)用,不常用
  • 堆內(nèi)存回收: 當(dāng)GC發(fā)現(xiàn)DirectByteBuffer對象變成垃圾時,會調(diào)用cleaner回收對應(yīng)的堆外內(nèi)存,防止內(nèi)存泄漏,也可以手動調(diào)用該方法回收Cleaner.clean回收堆外內(nèi)存
Cleaner繼承PhantomRefrence(虛引用)
  • 當(dāng)字段refrent(就是DirectByteBuffer對象)被回收時(當(dāng)虛引用被回收時會收到一個系統(tǒng)通知),會調(diào)用到Cleaner.clean方法,進行堆外內(nèi)存的回收
  • Cleaner是虛引用在JDK中的一個典型應(yīng)用場景
堆外內(nèi)存的使用場景
  • 適合長期存在或能復(fù)用的場景
  • 適合注重穩(wěn)定的場景,避免GC導(dǎo)致暫停問題
  • 適合簡單對象的存儲:只能存儲字節(jié)數(shù)組,需要序列化/反序列化操作
  • 適合注重IO效率的場景:讀寫文件性能優(yōu)良
  • 文件IO (BIO, NIO)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。