继夫的玩弄H辣文的小说|女人与拘性猛交视频|精品欧美高清不卡高清|一起做亏亏的事情的视频|啦啦啦在线视频观看|望月直播下载ios版本|国产日韩欧美一区二区三区

五 重學(xué)Android基礎(chǔ)系列篇:Android虛擬機(jī)類和對(duì)象的結(jié)構(gòu)

1.Android高級(jí)開發(fā)工程師必備基礎(chǔ)技能2.Android性能優(yōu)化核心知識(shí)筆記3.Android+音視頻進(jìn)階開發(fā)面試題沖刺合集4.Android 音視頻開發(fā)入門到實(shí)戰(zhàn)學(xué)習(xí)手冊(cè)5.Android Framework精編內(nèi)核解析6.Flutter實(shí)戰(zhàn)進(jìn)階技術(shù)手冊(cè)7.近百個(gè)Android錄播視頻+音視頻視頻dome.......
虛擬機(jī)類和對(duì)象的結(jié)構(gòu)1.對(duì)象內(nèi)存結(jié)構(gòu)
在 JVM 中,Java對(duì)象保存在堆中時(shí) , 由以下三部分組成:
對(duì)象頭分為Mark Word(標(biāo)記字)和Class (類指針),如果是數(shù)組對(duì)象還得再加一項(xiàng)Array (數(shù)組長(zhǎng)度) 。
Mark Word
用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù) , 如哈希碼()、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等等 。
Mark Word在32位JVM中的長(zhǎng)度是32bit,在64位JVM中長(zhǎng)度是64bit 。我們打開的源碼包,對(duì)應(yīng)路徑///src/share/vm/oops,Mark Word對(duì)應(yīng)到C++的代碼.hpp,可以從注釋中看到它們的組成,本文所有代碼是基于Jdk1.8 。
在64位JVM中是這么存的:
雖然它們?cè)诓煌粩?shù)的JVM中長(zhǎng)度不一樣 , 但是基本組成內(nèi)容是一致的 。
Klass
即類型指針,是0對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例 。
實(shí)例數(shù)據(jù)
如果對(duì)象有屬性字段,則這里會(huì)有數(shù)據(jù)信息 。如果對(duì)象無屬性字段,則這里就不會(huì)有數(shù)據(jù) 。根據(jù)字段類型的不同占不同的字節(jié) , 例如類型占1個(gè)字節(jié),int類型占4個(gè)字節(jié)等等;
對(duì)齊數(shù)據(jù)
對(duì)象可以有對(duì)齊數(shù)據(jù)也可以沒有 。默認(rèn)情況下,Java虛擬機(jī)堆中對(duì)象的起始地址需要對(duì)齊至8的倍數(shù) 。如果一個(gè)對(duì)象用不到8N個(gè)字節(jié)則需要對(duì)其填充,以此來補(bǔ)齊對(duì)象頭和實(shí)例數(shù)據(jù)占用內(nèi)存之后剩余的空間大小 。如果對(duì)象頭和實(shí)例數(shù)據(jù)已經(jīng)占滿了JVM所分配的內(nèi)存空間,那么就不用再進(jìn)行對(duì)齊填充了 。
所有的對(duì)象分配的字節(jié)總SIZE需要是8的倍數(shù),如果前面的對(duì)象頭和實(shí)例數(shù)據(jù)占用的總SIZE不滿足要求,則通過對(duì)齊數(shù)據(jù)來填滿 。
為什么要對(duì)齊數(shù)據(jù)?字段內(nèi)存對(duì)齊的其中一個(gè)原因,是讓字段只出現(xiàn)在同一CPU的緩存行中 。如果字段不是對(duì)齊的,那么就有可能出現(xiàn)跨緩存行的字段 。也就是說,該字段的讀取可能需要替換兩個(gè)緩存行,而該
字段的存儲(chǔ)也會(huì)同時(shí)污染兩個(gè)緩存行 。這兩種情況對(duì)程序的執(zhí)行效率而言都是不利的 。其實(shí)對(duì)其填充的最終目的是為了計(jì)算機(jī)高效尋址 。
至此,我們已經(jīng)了解了對(duì)象在堆內(nèi)存中的整體結(jié)構(gòu)布局,如下圖所示
2. JVM內(nèi)存結(jié)構(gòu)、Java對(duì)象模型和Java內(nèi)存模型區(qū)別
JVM內(nèi)存結(jié)構(gòu)、Java對(duì)象模型和Java內(nèi)存模型,這就是三個(gè)截然不同的概念 , 而這三個(gè)概念很容易混淆 。這里詳細(xì)區(qū)別一下
2.1 JVM內(nèi)存結(jié)構(gòu)(5個(gè)部分)
我們都知道,Java代碼是要運(yùn)行在虛擬機(jī)上的 , 而虛擬機(jī)在執(zhí)行Java程序的過程中會(huì)把所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,這些區(qū)域都有各自的用途 。其中有些區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在 , 而有些區(qū)域則依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀 。
在《Java虛擬機(jī)規(guī)范(Java SE 8)》中描述了JVM運(yùn)行時(shí)內(nèi)存區(qū)域結(jié)構(gòu)如下:
JVM內(nèi)存結(jié)構(gòu),由Java虛擬機(jī)規(guī)范定義 。描述的是Java程序執(zhí)行過程中,由JVM管理的不同數(shù)據(jù)區(qū)域 。各個(gè)區(qū)域有其特定的功能 。
為了提高運(yùn)算效率,就對(duì)空間進(jìn)行了不同區(qū)域的劃分,因?yàn)槊恳黄瑓^(qū)域都有特定的處理數(shù)據(jù)方式和內(nèi)存管理方式 。
JVM 的內(nèi)存劃分:
棧內(nèi)存:存放的是方法中的局部變量 , 方法的運(yùn)行一定要在棧當(dāng)中,方法中的局部變量才會(huì)在棧中創(chuàng)建 。
成員變量在堆內(nèi)存 , 靜態(tài)變量在方法區(qū) 。
方法區(qū):存儲(chǔ).class相關(guān)信息 。
與開發(fā)相關(guān)的時(shí)方法棧、方法區(qū)、堆內(nèi)存 。
new出來的都放在堆內(nèi)存!堆內(nèi)存里面的東西都有一個(gè)地址值 。方法進(jìn)入方法棧中執(zhí)行 。
JVM堆內(nèi)存分為年輕代和老年代 。
2.2 Java對(duì)象模型()
Java是一種面向?qū)ο蟮恼Z言,而Java對(duì)象在JVM中的存儲(chǔ)也是有一定的結(jié)構(gòu)的 。而這個(gè)關(guān)于Java對(duì)象自身的存儲(chǔ)模型稱之為Java對(duì)象模型 。
虛擬機(jī)中(Sun JDK和中所帶的虛擬機(jī),也是目前使用范圍最廣的Java虛擬機(jī)),設(shè)計(jì)了一個(gè)OOP-Klass Model 。OOP()指的是普通對(duì)象指針,而Klass用來描述對(duì)象實(shí)例的具體類型 。
每一個(gè)Java類,在被JVM加載的時(shí)候 , JVM會(huì)給這個(gè)類創(chuàng)建一個(gè)對(duì)象 , 保存在方法區(qū),用來在JVM層表示該Java類 。當(dāng)我們?cè)贘ava代碼中,使用new創(chuàng)建一個(gè)對(duì)象的時(shí)候,JVM會(huì)創(chuàng)建一個(gè) 對(duì)象 , 這個(gè)對(duì)象中包含了對(duì)象頭以及實(shí)例數(shù)據(jù) 。對(duì)象的引用在方法棧中 。
這就是一個(gè)簡(jiǎn)單的 Java對(duì)象的OOP-Klass模型,即Java對(duì)象模型 。
2.3 java內(nèi)存模型
Java內(nèi)存模型就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪問都能保證效果一致的機(jī)制及規(guī)范 。
Java內(nèi)存模型是根據(jù)英文JavaModel(JMM)翻譯過來的 。其實(shí)JMM并不像JVM內(nèi)存結(jié)構(gòu)一樣是真實(shí)存在的 。他只是一個(gè)抽象的概念 。
JSR-133: JavaModel and中描述了,JMM是和多線程相關(guān)的,他描述了一組規(guī)則或規(guī)范 , 這個(gè)規(guī)范定義了一個(gè)線程對(duì)共享變量的寫入時(shí)對(duì)另一個(gè)線程是可見的 。
簡(jiǎn)單總結(jié)下,Java的多線程之間是通過共享內(nèi)存進(jìn)行通信的,而由于采用共享內(nèi)存進(jìn)行通信 , 在通信過程中會(huì)存在一系列如可見性、原子性、順序性等問題,而JMM就是圍繞著多線程通信以及與其相關(guān)的一系列特性而建立的模型 。JMM定義了一些語法集,這些語法集映射到Java語言中就是、等關(guān)鍵字 。
JMM 線程操作內(nèi)存的基本的規(guī)則:
第一條關(guān)于線程與主內(nèi)存:線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存(本地內(nèi)存)中進(jìn)行 , 不能直接從主內(nèi)存中讀寫 。
第二條關(guān)于線程間本地內(nèi)存:不同線程之間無法直接訪問其他線程本地內(nèi)存中的變量,線程間變量值的傳遞需要經(jīng)過主內(nèi)存來完成 。
主內(nèi)存
主要存儲(chǔ)的是Java實(shí)例對(duì)象,所有線程創(chuàng)建的實(shí)例對(duì)象都存放在主內(nèi)存中,不管該實(shí)例對(duì)象是成員變量還是方法中的本地變量(也稱局部變量),當(dāng)然也包括了共享的類信息、常量、靜態(tài)變量 。(主內(nèi)存中的數(shù)據(jù))由于是共享數(shù)據(jù)區(qū)域,多條線程對(duì)同一個(gè)變量進(jìn)行訪問可能會(huì)發(fā)現(xiàn)線程安全問題 。
本地內(nèi)存
主要存儲(chǔ)當(dāng)前方法的所有本地變量信息(本地內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝),每個(gè)線程只能訪問自己的本地內(nèi)存 , 即線程中的本地變量對(duì)其它線程是不可見的,就算是兩個(gè)線程執(zhí)行的是同一段代碼,它們也會(huì)各自在自己的工作內(nèi)存中創(chuàng)建屬于當(dāng)前線程的本地變量,當(dāng)然也包括了字節(jié)碼行號(hào)指示器、相關(guān)方法的信息 。注意由于工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù),線程間無法相互訪問工作內(nèi)存,因此存儲(chǔ)在工作內(nèi)存的數(shù)據(jù)不存在線程安全問題 。
3.堆內(nèi)管理策略3.1 對(duì)象分配過程完全解析
在開始之前,首先介紹一下HSDB工具使用
3.1.1 HSDB工具應(yīng)用
如圖所示
進(jìn)入對(duì)應(yīng)的JDK-Lib目錄 , 然后輸入java -cp .sa-jdi.jar sun.jvm..HSDB 就會(huì)出現(xiàn)HSDB窗體應(yīng)用程序
然后運(yùn)行對(duì)應(yīng)的Demo代碼
public class HSDBTest {public HSDBTest() {}public static void main(String[] args) {Teacher kerwin = new Teacher();kerwin.setName("kerwin");for(int i = 0; i < 15; ++i) {System.gc();}Teacher jett = new Teacher();jett.setName("jett");StackTest test = new StackTest();test.test1(1);System.out.println("掛起....");try {Thread.sleep(10000000L);} catch (InterruptedException var5) {var5.printStackTrace();}}}
開啟新的dos命令
如圖所示
當(dāng)運(yùn)行成功后,在對(duì)應(yīng)HSDB應(yīng)用上輸入對(duì)應(yīng)的進(jìn)程號(hào)就能看到對(duì)應(yīng)進(jìn)程的加載情況!
如圖所示
如果說對(duì)應(yīng)的HSDB一直出現(xiàn)加載情況,那么就得查看打開HSDB對(duì)應(yīng)的dos命令頁面上是否報(bào)錯(cuò) 。
如果說報(bào) 異常
那么說明:JDK目錄中缺失.dll文件
如圖所示
此時(shí),就需要把自己其中jrebin目錄下.dll 粘貼到另一個(gè)jrebin 目錄下,然后關(guān)閉HSDB,再次打開既ok
如圖所示
在這里選擇對(duì)應(yīng)的main線程,Stack就能看到對(duì)應(yīng)Stack詳細(xì)信息!
如圖所示
打開對(duì)應(yīng)的Tools -heap就能看到對(duì)應(yīng)的年輕代 , 老年代對(duì)應(yīng)的起始點(diǎn)!
如圖所示
從這兩張圖可知:年輕代里面包含Eden區(qū),F(xiàn)rom區(qū)和To區(qū) , 對(duì)應(yīng)的內(nèi)存地址塊都在年輕代范圍內(nèi)!
OK!到這里 , 相信你對(duì) 年輕代和老年代里面具體劃分有了一定的認(rèn)知?。。?
那么!年輕代和老年代它們之間是怎么運(yùn)作的呢?為什么年輕代要分為Eden、From、To三個(gè)模塊呢?
因此迎來了本篇重點(diǎn):對(duì)象的分配過程,前面都是引子!
3.1.2 堆的核心結(jié)構(gòu)解析
那么堆是什么呢?
堆概述:一個(gè)JVM進(jìn)程存在一個(gè)堆內(nèi)存,堆是JVM內(nèi)存管理的核心區(qū)域java 堆區(qū)在JVM啟動(dòng)是被創(chuàng)建,其空間大小也被確定,是JVM管理的 最大一塊內(nèi)存(堆內(nèi)存大小可以調(diào)整)本質(zhì)上堆是一組在物理上不連續(xù)的內(nèi)存空間,但是邏輯上是連續(xù)的 空間(參考上面HSDB分析的內(nèi)存結(jié)構(gòu))所有線程共享堆,但是堆內(nèi)對(duì)于線程處理還是做了一個(gè)線程私有的 部分(TLAB)
那么堆的對(duì)象分配、管理又是怎么的呢?
堆的對(duì)象管理堆的內(nèi)存細(xì)分
如圖所示
至于為什么要這樣分配,這就和分代相互關(guān)聯(lián)了!
那么!為什么要分代(年輕代和老年代)呢?
分代思想不同對(duì)象的生命周期不一致,但是在具體使用過程中70%- 90的對(duì)象是臨時(shí)對(duì)象分代唯一的理由是優(yōu)化GC性能 。如果沒有分代,那么所有對(duì)象在一塊空間,GC想要回收掃描他就必須掃描所有的對(duì)象,分代之后 , 長(zhǎng)期持有的對(duì)象可以挑出 , 短期持有的對(duì)象可以固定在一個(gè)位置進(jìn)行回收,省掉很 大一部分空間利用
如圖所示
堆的默認(rèn)大小
默認(rèn)空間大?。?
那么如何查看本機(jī)空間大小呢?
public class EdenSurvivorTest {public static void main(String[] args) {EdenSurvivorTest test = new EdenSurvivorTest();test.method1();//test.method2();}/*** 堆內(nèi)存大小示例* 默認(rèn)空間大?。?*初始大?。何錮淼縋閱詿媧笮?/ 64*最大內(nèi)存大?。何錮淼縋閱詿媧笮?/ 4*/public void method1(){long initialMemory = Runtime.getRuntime().totalMemory();long maxMemory = Runtime.getRuntime().maxMemory();System.out.println("初始內(nèi)存:"+(initialMemory / 1024 / 1024));System.out.println("最大內(nèi)存:"+(maxMemory / 1024 / 1024));try {Thread.sleep(100000000);} catch (InterruptedException e) {e.printStackTrace();}}}
運(yùn)行結(jié)果
初始內(nèi)存:245最大內(nèi)存:3621
當(dāng)然也可以使用jstat命令查看
如圖所示
這里簡(jiǎn)單的提一下這里面的類型表示什么意思,更多jstat命令查看
3.1.3 對(duì)象分配過程
到這里才開始講解本篇的重點(diǎn)
注意:Java 閾值是15,閾值是6 , 這里就拿舉例
正常分配過程
如圖所示
所有變量的產(chǎn)生都在Eden區(qū),當(dāng)Eden區(qū)滿了時(shí) , 將會(huì)觸發(fā)
如圖所示
當(dāng) 觸發(fā)后,不需要的變量將會(huì)被回收掉,正在使用中的變量將會(huì)移動(dòng)至From區(qū) , 并且對(duì)應(yīng)的閾值+1
如圖所示
當(dāng)下一次Eden區(qū)滿了后 , 對(duì)應(yīng),將會(huì)帶同F(xiàn)rom區(qū)、Eden區(qū)一起,標(biāo)記對(duì)象
如圖所示
回收成功后,對(duì)應(yīng)的From區(qū)以及Eden區(qū),正在使用的的都會(huì)進(jìn)入To區(qū),對(duì)應(yīng)閾值+1
同理,當(dāng)下一次Eden滿了后,對(duì)應(yīng)To區(qū)和Eden區(qū)都會(huì)被對(duì)應(yīng)標(biāo)記,正在使用中的對(duì)象又全部移動(dòng)至From區(qū),一直來回交替!對(duì)應(yīng)的閾值也會(huì)自增
如圖所示
當(dāng)對(duì)應(yīng)的From區(qū)或者To區(qū)存在未回收的對(duì)象的閾值滿足進(jìn)入老年代條件時(shí),對(duì)應(yīng)的對(duì)象將會(huì)移動(dòng)至老年代!
當(dāng)然在老年代里面,如果內(nèi)存滿了,也會(huì)觸發(fā)Full GC,未被回收的對(duì)象閾值+1
為了加深印象,這里用一段小故事來描述整段過程!
我是一個(gè)普通的java對(duì)象 , 我出生在Eden區(qū) , 在Eden區(qū)我還看到和我長(zhǎng)的很像的小兄弟,我們 在Eden區(qū)中玩了挺長(zhǎng)時(shí)間 。有一天Eden區(qū)中的人實(shí)在是太多了,我就被迫去了區(qū)的“From”區(qū),自從去了區(qū),我就開始了我漂泊的人生,有時(shí)候在的“From”區(qū),有時(shí)候在的“To”區(qū),居無定所直到我18歲(閾值達(dá)到老年代)的時(shí)候,爸爸說我成人了,該去社會(huì)上闖闖了 。于是我就去了年老代那邊,年老代 里,人很多,并且年齡都挺大的 , 我在這里也認(rèn)識(shí)了很多人 。在年老代里,我生活了20年(每次 GC加一歲),然后被回收 。
這就是一整段很標(biāo)準(zhǔn)的內(nèi)存分配過程,那么如果存在特殊情況將會(huì)是怎樣的呢?
比如說,產(chǎn)生的對(duì)象Eden直接裝不下的那種
非正常分配過程
如圖所示
進(jìn)入老年代的方式有四種方式:
驗(yàn)證對(duì)象分配過程
短生命周期分配過程
說了這么多,來驗(yàn)證一把哇
public class EdenSurvivorTest {public static void main(String[] args) {EdenSurvivorTest test = new EdenSurvivorTest();test.method2();}public void method2(){ArrayList list = new ArrayList();for (;;) {TestGC t = new TestGC();//list.add(t);try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}}
這里我們大概分析下代碼,在for死循環(huán)里,對(duì)象 生命周期僅限于當(dāng)前循環(huán)里,屬于短生命周期對(duì)象,那么我們來看看具體是對(duì)象是如何分配的!
如圖所示
打開JDK-BIN 目錄,然后雙擊對(duì)應(yīng)的exe
注意:
一切準(zhǔn)備就緒后,運(yùn)行上面代碼,然后打開該exe,就能看到
如圖所示
圖里面該說的都說了 , 不過注意的是,這里OLD區(qū)并沒有任何數(shù)據(jù)!
因?yàn)樵谏厦娲a解析的時(shí)候就已經(jīng)說了,產(chǎn)生的對(duì)象生命周期僅限于For循環(huán)里 , 并非長(zhǎng)生命周期對(duì)象
那么能否舉一個(gè)有長(zhǎng)生命周期對(duì)象的例子呢?
長(zhǎng)生命周期分配過程
public class EdenSurvivorTest {public static void main(String[] args) {EdenSurvivorTest test = new EdenSurvivorTest();test.method2();}public void method2(){ArrayList list = new ArrayList();for (;;) {TestGC t = new TestGC();list.add(t);try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}}
運(yùn)行該代碼,然后再次查看剛剛的Exe
如圖所示
因?yàn)閷?duì)應(yīng)變量的生命周期不再僅限于for內(nèi)部,因此當(dāng)閾值滿足老年代要求時(shí),將直接進(jìn)入老年代
如圖所示
因?yàn)槔夏甏锩娴膶?duì)象一直持有 , 并沒有未使用的對(duì)象 , 當(dāng)老年代滿了時(shí),就會(huì)觸發(fā)OOM異常?。?
在上面提到過好幾個(gè)GC,那么不同的GC有什么區(qū)別呢?
//的區(qū)別
JVM在進(jìn)行GC時(shí),并非每次都對(duì)上面三個(gè)內(nèi)存區(qū)域一起回收,大部分的只會(huì)針對(duì)于Eden區(qū)進(jìn)行 在JVM標(biāo)準(zhǔn)中,他里面的GC按照回收區(qū)域劃分為兩種:
GC觸發(fā)策略
年輕代觸發(fā)機(jī)制
老年代GC觸發(fā)機(jī)制:
觸發(fā)
Full GC 是開發(fā)或者調(diào)優(yōu)中盡量要避開的
GC日志查看
如圖所示
在這里添加:-Xms9m -Xmx9m -XX:+ 提交后,再次運(yùn)行代碼:
Connected to the target VM, address: '127.0.0.1:53687', transport: 'socket'[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->740K(9728K), 0.0032500 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2291K->504K(2560K)] 2544K->2280K(9728K), 0.0040878 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2343K->504K(2560K)] 4120K->4104K(9728K), 0.0010760 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2341K->504K(2560K)] 5942K->5912K(9728K), 0.0013867 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 5408K->5741K(7168K)] 5912K->5741K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0044415 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [Full GC (Ergonomics) [PSYoungGen: 1859K->600K(2560K)] [ParOldGen: 5741K->6941K(7168K)] 7601K->7541K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0042249 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 1836K->1800K(2560K)] [ParOldGen: 6941K->6941K(7168K)] 8778K->8742K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0018656 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 1800K->1800K(2560K)] [ParOldGen: 6941K->6925K(7168K)] 8742K->8725K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0043790 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap PSYoungGentotal 2560K, used 1907K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)eden space 2048K, 93% used [0x00000000ffd00000,0x00000000ffedcfd8,0x00000000fff00000)from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)tospace 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGentotal 7168K, used 6925K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)object space 7168K, 96% used [0x00000000ff600000,0x00000000ffcc3688,0x00000000ffd00000) Metaspaceused 3369K, capacity 4556K, committed 4864K, reserved 1056768Kclass spaceused 364K, capacity 392K, committed 512K, reserved 1048576K
就能查看對(duì)應(yīng)的GC日志了 。
3.2 對(duì)象創(chuàng)建過程解析3.2.1 對(duì)象創(chuàng)建
A a = new A();
限于普通對(duì)象,不包括數(shù)組和Class對(duì)象等
創(chuàng)建過程
當(dāng)遇到關(guān)鍵字new指令時(shí),Java對(duì)象創(chuàng)建過程便開始,整個(gè)過程如下:
Java對(duì)象創(chuàng)建過程
下面我將對(duì)每個(gè)步驟進(jìn)行講解 。
過程步驟
步驟1:類加載檢查
檢查 該new指令的參數(shù) 是否能在 常量池中 定位到一個(gè)類的符號(hào)引用檢查 該類符號(hào)引用 代表的類是否已被加載、解析和初始化過
步驟2:為對(duì)象分配內(nèi)存
對(duì)象所需內(nèi)存的大小在類加載完成后便可完全確定
Java堆內(nèi)存 規(guī)整:已使用的內(nèi)存在一邊,未使用內(nèi)存在另一邊Java堆內(nèi)存 不規(guī)整:已使用的內(nèi)存和未使用內(nèi)存相互交錯(cuò)
方式1:指針碰撞
方式2:空閑列表
額外知識(shí)
如、垃圾收集器
使用基于 算法的垃圾收集器時(shí) , 采用空閑列表 。
如 CMS垃圾收集器
特別注意
如 , 正在給對(duì)象A分配內(nèi)存,指針還沒有來得及修改,對(duì)象B又同時(shí)使用了原來的指針來分配內(nèi)存
所以,給對(duì)象分配內(nèi)存會(huì)存在線程不安全的問題 。
解決 線程不安全 有兩種方案:

五 重學(xué)Android基礎(chǔ)系列篇:Android虛擬機(jī)類和對(duì)象的結(jié)構(gòu)

文章插圖
五 重學(xué)Android基礎(chǔ)系列篇:Android虛擬機(jī)類和對(duì)象的結(jié)構(gòu)

文章插圖
同步處理分配內(nèi)存空間的行為
虛擬機(jī)采用 CAS + 失敗重試的方式 保證更新操作的原子性
把內(nèi)存分配行為 按照線程 劃分在不同的內(nèi)存空間進(jìn)行
即每個(gè)線程在 Java堆中預(yù)先分配一小塊內(nèi)存(本地線程分配緩沖( Local,TLAB)),哪個(gè)線程要分配內(nèi)存,就在哪個(gè)線程的TLAB上分配,只有TLAB用完并分配新的TLAB時(shí)才需要同步鎖 。虛擬機(jī)是否使用TLAB,可以通過-XX:+/-參數(shù)來設(shè)定 。
步驟3: 將內(nèi)存空間初始化為零值
內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間初始化為零(不包括對(duì)象頭)
保證了對(duì)象的實(shí)例字段在使用時(shí)可不賦初始值就直接使用(對(duì)應(yīng)值 = 0)如使用本地線程分配緩沖(TLAB),這一工作過程也可以提前至TLAB分配時(shí)進(jìn)行 。
步驟4: 對(duì)對(duì)象進(jìn)行必要的設(shè)置
如 , 設(shè)置 這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息 。
這些信息存放在對(duì)象的對(duì)象頭中 。
總結(jié)
下面用一張圖總結(jié) Java對(duì)象創(chuàng)建的過程
3.3 對(duì)象的內(nèi)存布局
下面我會(huì)詳細(xì)說明每一塊區(qū)域 。
3.2.1 對(duì)象頭 區(qū)域
此處存儲(chǔ)的信息包括兩部分:
如哈希碼()、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等該部分?jǐn)?shù)據(jù)被設(shè)計(jì)成1個(gè) 非固定的數(shù)據(jù)結(jié)構(gòu) 以便在極小的空間存儲(chǔ)盡量多的信息(會(huì)根據(jù)對(duì)象狀態(tài)復(fù)用存儲(chǔ)空間)
即對(duì)象指向它的類元數(shù)據(jù)的指針虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例
特別注意
如果對(duì)象 是 數(shù)組 , 那么在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)
因?yàn)樘摂M機(jī)可以通過普通Java對(duì)象的元數(shù)據(jù)信息確定對(duì)象的大?。?但是從數(shù)組的元數(shù)據(jù)中卻無法確定數(shù)組的大小 。
3.2.2 實(shí)例數(shù)據(jù) 區(qū)域
即代碼中定義的字段內(nèi)容
// HotSpot虛擬機(jī)默認(rèn)的分配策略如下:longs/doubles、ints、shorts/chars、bytes/booleans、oop(Ordinary Object Pointers)// 從分配策略中可以看出,相同寬度的字段總是被分配到一起// 在滿足這個(gè)前提的條件下,父類中定義的變量會(huì)出現(xiàn)在子類之前CompactFields = true;// 如果 CompactFields 參數(shù)值為true,那么子類之中較窄的變量也可能會(huì)插入到父類變量的空隙之中 。
3.2.3 對(duì)齊填充 區(qū)域
占位作用
3.2.4 總結(jié)
3.4 對(duì)象的訪問定位
實(shí)際上需訪問的是 對(duì)象類型數(shù)據(jù) & 對(duì)象實(shí)例數(shù)據(jù)
由于引用類型數(shù)據(jù)()在 Java虛擬機(jī)中只規(guī)定了一個(gè)指向?qū)ο蟮囊茫珱]定義該引用應(yīng)該通過何種方式去定位、訪問堆中的對(duì)象的具體位置
所以對(duì)象訪問方式取決于虛擬機(jī)實(shí)現(xiàn) 。目前主流的對(duì)象訪問方式有兩種:
具體請(qǐng)看如下介紹:
4.逃逸分析
JIT 即時(shí)編譯還有一個(gè)最前沿的優(yōu)化技術(shù):逃逸分析( )。廢話少說,我們直接步入正題吧 。
4.1 逃逸分析
首先我們需要知道 , 逃逸分析并不是直接的優(yōu)化手段,而是通過動(dòng)態(tài)分析對(duì)象的作用域,為其它優(yōu)化手段提供依據(jù)的分析技術(shù) 。具體而言就是:
逃逸分析是“一種確定指針動(dòng)態(tài)范圍的靜態(tài)分析,它可以分析在程序的哪些地方可以訪問到指針” 。Java虛擬機(jī)的即時(shí)編譯器會(huì)對(duì)新建的對(duì)象進(jìn)行逃逸分析,判斷對(duì)象是否逃逸出線程或者方法 。即時(shí)編譯器判斷對(duì)象是否逃逸的依據(jù)有兩種:
對(duì)象是否被存入堆中(靜態(tài)字段或者堆中對(duì)象的實(shí)例字段),一旦對(duì)象被存入堆中,其他線程便能獲得該對(duì)象的引用,即時(shí)編譯器就無法追蹤所有使用該對(duì)象的代碼位置 。簡(jiǎn)單來說就是 , 如類變量或?qū)嵗兞浚赡鼙黄渌€程訪問到,這就叫做線程逃逸,存在線程安全問題 。對(duì)象是否被傳入未知代碼中,即時(shí)編譯器會(huì)將未被內(nèi)聯(lián)的代碼當(dāng)成未知代碼,因?yàn)樗鼰o法確認(rèn)該方法調(diào)用會(huì)不會(huì)將調(diào)用者或所傳入的參數(shù)存儲(chǔ)至堆中,這種情況 , 可以直接認(rèn)為方法調(diào)用的調(diào)用者以及參數(shù)是逃逸的 。(未知代碼指的是沒有被內(nèi)聯(lián)的方法調(diào)用)比如說,當(dāng)一個(gè)對(duì)象在方法中定義之后,它可能被外部方法所引用,作為參數(shù)傳遞到其它方法中,這叫做方法逃逸,
方法逃逸我們可以用個(gè)案例來演示一下:
//StringBuffer對(duì)象發(fā)生了方法逃逸public static StringBuffer createStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb;}public static String createString(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();}
關(guān)于逃逸分析技術(shù),本人想過用代碼展示對(duì)象是否發(fā)生了逃逸 , 比如說上述代碼,根據(jù)理論知識(shí)可以認(rèn)為方法中發(fā)生了逃逸,但是具體是個(gè)什么情況 , 咱們都不清楚 。雖然 JVM 有個(gè)參數(shù)可以顯示分析結(jié)果,但是該參數(shù)僅限于 debug 版本的 JDK 才可以進(jìn)行調(diào)試,多次嘗試后,未能編譯出 debug 版本的 JDK,暫且沒什么思路,所以查看逃逸分析結(jié)果這件事先往后放一放,后續(xù)學(xué)習(xí) JVM 調(diào)優(yōu)再進(jìn)一步來學(xué)習(xí) 。
4.2 基于逃逸分析的優(yōu)化
即時(shí)編譯器可以根據(jù)逃逸分析的結(jié)果進(jìn)行諸如同步消除、棧上分配以及標(biāo)量替換的優(yōu)化 。
同步消除(鎖消除)
線程同步本身比較耗費(fèi)資源,JIT 編譯器可以借助逃逸分析來判斷,如果確定一個(gè)對(duì)象不會(huì)逃逸出線程kotlin使用for循環(huán),無法被其它線程訪問到,那該對(duì)象的讀寫就不會(huì)存在競(jìng)爭(zhēng),則可以消除對(duì)該對(duì)象的同步鎖,通過-XX:+(默認(rèn)開啟)可以開啟同步消除 。這個(gè)取消同步的過程就叫同步消除,也叫鎖消除 。
我們還是通過案例來說明這一情況,來看看何種情況需要線程同步 。
首先構(gòu)建一個(gè)對(duì)象
@Getterpublic class Worker {private String name;private double money;public Worker() {}public Worker(String name) {this.name = name;}public void makeMoney() {money++;}}
測(cè)試代碼如下:
public class SynchronizedTest {public static void work(Worker worker) {worker.makeMoney();}public static void main(String[] args) throws InterruptedException {long start = System.currentTimeMillis();Worker worker = new Worker("hresh");new Thread(() -> {for (int i = 0; i{for (int i = 0; i < 20000; i++) {work(worker);}}, "B").start();long end = System.currentTimeMillis();System.out.println(end - start);Thread.sleep(100);System.out.println(worker.getName() + "總共賺了" + worker.getMoney());}}
執(zhí)行結(jié)果如下:
52hresh總共賺了28224.0
可以看出,上述兩個(gè)線程同時(shí)修改同一個(gè)對(duì)象的 money 數(shù)據(jù),對(duì)于 money 字段的讀寫發(fā)生了競(jìng)爭(zhēng),導(dǎo)致最后結(jié)果不正確 。像上述這種情況,即時(shí)編譯器經(jīng)過逃逸分析后認(rèn)定對(duì)象發(fā)生了逃逸,那么肯定不能進(jìn)行同步消除優(yōu)化 。
換個(gè)對(duì)象不發(fā)生逃逸的情況試一下 。
//JVM參數(shù):-Xms60M -Xmx60M-XX:+PrintGCDetails -XX:+PrintGCDateStampspublic class SynchronizedTest {public static void lockTest() {Worker worker = new Worker();synchronized (worker) {worker.makeMoney();}}public static void main(String[] args) throws InterruptedException {long start = System.currentTimeMillis();new Thread(() -> {for (int i = 0; i{for (int i = 0; i < 500000; i++) {lockTest();}}, "B").start();long end = System.currentTimeMillis();System.out.println(end - start);}}
輸出結(jié)果如下:
56Heap PSYoungGentotal 17920K, used 9554K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000)eden space 15360K, 62% used [0x00000007bec00000,0x00000007bf5548a8,0x00000007bfb00000)from space 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000)tospace 2560K, 0% used [0x00000007bfb00000,0x00000007bfb00000,0x00000007bfd80000) ParOldGentotal 40960K, used 0K [0x00000007bc400000, 0x00000007bec00000, 0x00000007bec00000)object space 40960K, 0% used [0x00000007bc400000,0x00000007bc400000,0x00000007bec00000) Metaspaceused 4157K, capacity 4720K, committed 4992K, reserved 1056768Kclass spaceused 467K, capacity 534K, committed 640K, reserved 1048576K
在方法中針對(duì)新建的對(duì)象加鎖,并沒有實(shí)際意義 , 經(jīng)過逃逸分析后認(rèn)定對(duì)象未逃逸 , 則會(huì)進(jìn)行同步消除優(yōu)化 。JDK8 默認(rèn)開啟逃逸分析,我們嘗試關(guān)閉它,再看看輸出結(jié)果 。
-Xms60M -Xmx60M-XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+PrintGCDateStamps
輸出結(jié)果變?yōu)椋?br /> 732022-03-01T14:51:08.825-0800: [GC (Allocation Failure) [PSYoungGen: 15360K->1439K(17920K)] 15360K->1447K(58880K), 0.0018940 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] Heap PSYoungGentotal 17920K, used 16340K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000)eden space 15360K, 97% used [0x00000007bec00000,0x00000007bfa8d210,0x00000007bfb00000)from space 2560K, 56% used [0x00000007bfb00000,0x00000007bfc67f00,0x00000007bfd80000)tospace 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000) ParOldGentotal 40960K, used 8K [0x00000007bc400000, 0x00000007bec00000, 0x00000007bec00000)object space 40960K, 0% used [0x00000007bc400000,0x00000007bc402000,0x00000007bec00000) Metaspaceused 4153K, capacity 4688K, committed 4864K, reserved 1056768Kclass spaceused 466K, capacity 502K, committed 512K, reserved 1048576K
經(jīng)過對(duì)比發(fā)現(xiàn),關(guān)閉逃逸分析后 , 執(zhí)行時(shí)間變長(zhǎng),且內(nèi)存占用變大,同時(shí)發(fā)生了垃圾回收 。
不過,基于逃逸分析的鎖消除實(shí)際上并不多見 。一般來說 , 開發(fā)人員不會(huì)直接對(duì)方法中新構(gòu)造的對(duì)象進(jìn)行加鎖,如上述案例所示, 方法中的加鎖操作沒什么意義 。
事實(shí)上,逃逸分析的結(jié)果更多被用于將新建對(duì)象操作轉(zhuǎn)換成棧上分配或者標(biāo)量替換 。
標(biāo)量替換
在講解 Java 對(duì)象的內(nèi)存布局時(shí)提到過,Java 虛擬機(jī)中對(duì)象都是在堆上分配的,而堆上的內(nèi)容對(duì)任何線程大都是可見的(除開 TLAB) 。與此同時(shí),Java 虛擬機(jī)需要對(duì)所分配的堆內(nèi)存進(jìn)行管理,并且在對(duì)象不再被引用時(shí)回收其所占據(jù)的內(nèi)存 。
如果逃逸分析能夠證明某些新建的對(duì)象不逃逸 , 那么 Java 虛擬機(jī)完全可以將其分配至棧上,并且在 new 語句所在的方法退出時(shí),通過彈出當(dāng)前方法的棧楨來自動(dòng)回收所分配的內(nèi)存空間 。這樣一來 , 我們便無須借助垃圾回收器來處理不再被引用的對(duì)象 。
但是目前并沒有實(shí)現(xiàn)真正意義上的棧上分配,而是使用了標(biāo)量替換這么一項(xiàng)技術(shù) 。
所謂的標(biāo)量,就是僅能存儲(chǔ)一個(gè)值的變量,比如 Java 代碼中的局部變量 。與之相反,聚合量則可能同時(shí)存儲(chǔ)多個(gè)值,其中一個(gè)典型的例子便是 Java 對(duì)象 。
若一個(gè)數(shù)據(jù)已經(jīng)無法再分解成更小的數(shù)據(jù)來表示了,Java虛擬機(jī)中的原始數(shù)據(jù)類型(int、long等數(shù)值類型及類型等)都不能再進(jìn)一步分解了,那么這些數(shù)據(jù)就可以被稱為標(biāo)量 。相對(duì)的,如果一個(gè)數(shù)據(jù)可以繼續(xù)分解,那它就被稱為聚合量(),Java 中的對(duì)象就是典型的聚合量 。
標(biāo)量替換這項(xiàng)優(yōu)化技術(shù),可以看成將原本對(duì)對(duì)象的字段的訪問,替換為一個(gè)個(gè)局部變量的訪問 。
如下述案例所示:
public class ScalarTest {public static double getMoney() {Worker worker = new Worker();worker.setMoney(100.0);return worker.getMoney() + 20;}public static void main(String[] args) {getMoney();}}
經(jīng)過逃逸分析kotlin使用for循環(huán),對(duì)象未逃逸出 ()的調(diào)用 , 因此可以對(duì)聚合量進(jìn)行分解,得到局部變量 money,進(jìn)行標(biāo)量替換后的偽代碼:
public class ScalarTest {public static double getMoney() {double money = 100.0;return money + 20;}public static void main(String[] args) {getMoney();}}
對(duì)象拆分后,對(duì)象的成員變量改為方法的局部變量,這些字段既可以存儲(chǔ)在棧上,也可以直接存儲(chǔ)在寄存器中 。標(biāo)量替換因?yàn)椴槐貏?chuàng)建對(duì)象,減輕了垃圾回收的壓力 。
另外,可以手動(dòng)通過-XX:+可以開啟標(biāo)量替換(默認(rèn)是開啟的),-XX:+tions(同樣需要debug版本的JDK)查看標(biāo)量替換情況 。
棧上分配
故名思議就是在棧上分配對(duì)象,其實(shí)目前并沒有實(shí)現(xiàn)真正意義上的棧上分配,實(shí)際上是標(biāo)量替換 。
在一般情況下,對(duì)象和數(shù)組元素的內(nèi)存分配是在堆內(nèi)存上進(jìn)行的 。但是隨著 JIT 編譯器的日漸成熟,很多優(yōu)化使這種分配策略并不絕對(duì) 。JIT編譯器就可以在編譯期間根據(jù)逃逸分析的結(jié)果,來決定是否需要?jiǎng)?chuàng)建對(duì)象,是否可以將堆內(nèi)存分配轉(zhuǎn)換為棧內(nèi)存分配 。
4.3 部分逃逸分析
C2 的逃逸分析與控制流無關(guān),相對(duì)來說比較簡(jiǎn)單 。Graal 則引入了一個(gè)與控制流有關(guān)的逃逸分析,名為部分逃逸分析() 。它解決了所新建的實(shí)例僅在部分程序路徑中逃逸的情況 。
如下代碼所示:
public static void bar(boolean cond) {Object foo = new Object();if (cond) {foo.hashCode();}}// 可以手工優(yōu)化為:public static void bar(boolean cond) {if (cond) {Object foo = new Object();foo.hashCode();}}
假設(shè) if 語句的條件成立的可能性只有 1%,那么在 99% 的情況下,程序沒有必要新建對(duì)象 。其手工優(yōu)化的版本正是部分逃逸分析想要自動(dòng)達(dá)到的成果 。
部分逃逸分析將根據(jù)控制流信息,判斷出新建對(duì)象僅在部分分支中逃逸,并且將對(duì)象的新建操作推延至對(duì)象逃逸的分支中 。這將使得原本因?qū)ο筇右荻鵁o法避免的新建對(duì)象操作,不再出現(xiàn)在只執(zhí)行 if-else 分支的程序路徑之中 。
我們通過一個(gè)完整的測(cè)試案例來間接驗(yàn)證這一優(yōu)化 。
public class PartialEscapeTest {long placeHolder0;long placeHolder1;long placeHolder2;long placeHolder3;long placeHolder4;long placeHolder5;long placeHolder6;long placeHolder7;long placeHolder8;long placeHolder9;long placeHoldera;long placeHolderb;long placeHolderc;long placeHolderd;long placeHoldere;long placeHolderf;public static void foo(boolean flag) {PartialEscapeTest o = new PartialEscapeTest();if (flag) {o.hashCode();}}public static void main(String[] args) {for (int i = 0; i < 1000000; i++) {foo(false);}}}
本次測(cè)試選用的是 JDK11 , 開啟 Graal 編譯器需要配置如下參數(shù):
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler
分別輸出使用 C2 編譯器或 Graal 編譯器的 GC 日志 , 對(duì)應(yīng)命令為:
java -Xlog:gc* PartialEscapeTestjava -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -Xlog:gc* PartialEscapeTest
通過對(duì)比 GC 日志可以發(fā)現(xiàn)內(nèi)存占用情況不一致 , Graal 編譯器下內(nèi)存占用更小一點(diǎn) 。
C2
[0.012s][info][gc,heap] Heap region size: 1M[0.017s][info][gc] Using G1[0.017s][info][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3[0.345s][info][gc,heap,exit ] Heap[0.345s][info][gc,heap,exit ]garbage-first heaptotal 262144K, used 21504K [0x0000000700000000, 0x0000000800000000)[0.345s][info][gc,heap,exit ]region size 1024K, 18 young (18432K), 0 survivors (0K)[0.345s][info][gc,heap,exit ]Metaspaceused 6391K, capacity 6449K, committed 6784K, reserved 1056768K[0.345s][info][gc,heap,exit ]class spaceused 552K, capacity 571K, committed 640K, reserved 1048576K
Graal
[0.019s][info][gc,heap] Heap region size: 1M[0.025s][info][gc] Using G1[0.025s][info][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3[0.611s][info][gc,start] GC(0) Pause Young (Normal) (G1 Evacuation Pause)[0.612s][info][gc,task] GC(0) Using 6 workers of 10 for evacuation[0.615s][info][gc,phases] GC(0)Pre Evacuate Collection Set: 0.0ms[0.615s][info][gc,phases] GC(0)Evacuate Collection Set: 3.1ms[0.615s][info][gc,phases] GC(0)Post Evacuate Collection Set: 0.2ms[0.615s][info][gc,phases] GC(0)Other: 0.6ms[0.615s][info][gc,heap] GC(0) Eden regions: 24->0(150)[0.615s][info][gc,heap] GC(0) Survivor regions: 0->3(3)[0.615s][info][gc,heap] GC(0) Old regions: 0->4[0.615s][info][gc,heap] GC(0) Humongous regions: 5->5[0.615s][info][gc,metaspace ] GC(0) Metaspace: 8327K->8327K(1056768K)[0.615s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 29M->11M(256M) 3.941ms[0.615s][info][gc,cpu] GC(0) User=0.01s Sys=0.01s Real=0.00sCannot use JVMCI compiler: No JVMCI compiler found[0.616s][info][gc,heap,exit ] Heap[0.616s][info][gc,heap,exit ]garbage-first heaptotal 262144K, used 17234K [0x0000000700000000, 0x0000000800000000)[0.616s][info][gc,heap,exit ]region size 1024K, 9 young (9216K), 3 survivors (3072K)[0.616s][info][gc,heap,exit ]Metaspaceused 8336K, capacity 8498K, committed 8832K, reserved 1056768K[0.616s][info][gc,heap,exit ]class spaceused 768K, capacity 802K, committed 896K, reserved 1048576K
查看 Graal 在 JDK11 上的編譯結(jié)果,可以執(zhí)行下述命令:
java -XX:+PrintCompilation -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -cp /Users/xxx/IdeaProjects/java_deep_learning/src/main/java/com/msdn/java/javac/escape ScalarTest > out-jvmci.txt
5.Minor GC、Major GC和Full GC對(duì)比與GC日志分析5.1 Minor GC、Major GC和Full GC對(duì)比
GC類型
GC區(qū)域
觸發(fā)條件
Stop The World時(shí)間
Minor GC
Eden 和區(qū)域
Eden區(qū)域 > 設(shè)定內(nèi)存閾值
對(duì)于大部分應(yīng)用程序,**Minor GC停頓導(dǎo)致的延遲都是可以忽略不計(jì)的 。**大部分 Eden 區(qū)中的對(duì)象都能被認(rèn)為是垃圾,永遠(yuǎn)也不會(huì)被復(fù)制到區(qū)或者老年代空間 。如果Eden 區(qū)大部分新生對(duì)象不符合 GC 條件,Minor GC 執(zhí)行時(shí)暫停的時(shí)間將會(huì)長(zhǎng)很多 。
Major GC
Old區(qū)域
根據(jù)不同的GC配置由Minor GC觸發(fā)
的速度一般會(huì)比 Minor GC 慢 10倍以上 。
Full GC
整個(gè)Heap空間包括年輕代和永久代
1. 調(diào)用.gc時(shí)
Old老年代空間不足;方法區(qū)空間不足;通過Minor GC后進(jìn)入老年代的平均大小大于老年代的可用內(nèi)存。
5.2 GC日志分析5.2.1 GC 日志能幫我們做什么
GC 日志是由 JVM 產(chǎn)生的對(duì)垃圾回收活動(dòng)進(jìn)行描述的日志文件 。
通過 GC 日志,我們能夠直觀的看到內(nèi)存回收的情況及過程,是能夠快速判斷內(nèi)存是否存在故障的重要依據(jù) 。
5.2.2 如何生成 GC 日志
在 JAVA 命令中增加 GC 相關(guān)的參數(shù),以生成 GC日志:
JVM 參數(shù)
參數(shù)說明
備注
-XX:+
打印 GC 日志
-XX:+
打印詳細(xì)的 GC 日志
配置此參數(shù)時(shí),-XX:+ 可省略
-XX:+
以基準(zhǔn)形式記錄時(shí)間(即啟動(dòng)后多少秒,如:21.148:)
默認(rèn)的時(shí)間記錄方式,可省略
-XX:+
以日期形式記錄時(shí)間(如:2022-05-27T18:01:37.545+0800: 30.122:)
當(dāng)以日期形式記錄時(shí)間時(shí) , 日期后其實(shí)還帶有基準(zhǔn)形式的時(shí)間
-XX:+
打印堆的詳細(xì)信息
-:gc.log
配置 GC 日志文件的路徑
常用的選項(xiàng)組合:
java -Xms512m -Xmx2g -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar xxx.jar
5.3 讀懂 GC 日志
前文我們介紹了通過 -XX:+ 、 -XX:+ (同 -XX:+ )、 -XX:+ 、 -XX:+ 、 -XX:+ 這些參數(shù),來指定使用不同的垃圾回收器組合 。不同的垃圾回收器所生成的 GC 日志也是有差異的,尤其是 CMS 、 G1 所生成的日志 , 會(huì)比、 所生成的日志復(fù)雜許多 。
這里我們以 JDK1.8 默認(rèn)使用的 -XX:+ (即+Old )為例,講解其 GC 日志 。
開頭部分的環(huán)境信息
上圖是 GC 日志的開頭部分:
第 1 部分是 Java 環(huán)境信息:第 2 部分是服務(wù)器內(nèi)存信息:第 3 部分打印出與 GC 相關(guān)的 JVM 啟動(dòng)參數(shù) , 其中:
Young GC
上圖描述的是 Young GC 活動(dòng):
第 1 部分是日志時(shí)間:第 2 部分是 GC 的類型與發(fā)生 GC 的原因:第 3 部分是 GC 活動(dòng)的詳情:第 4 部分是 GC 耗時(shí)的詳情:
Full GC
上圖描述的是 Full GC 活動(dòng):
第 1 部分是日志時(shí)間,與 Minor GC 日志相同,不再贅述;第 2 部分是 GC 的類型與發(fā)生 GC 的原因:第 3 部分是 GC 活動(dòng)的詳情:第 4 部分是 GC 耗時(shí)的詳情,與 Minor GC 日志相同,不再贅述 。
以上便是 JDK1.8 下 -XX:+ (即+Old )模式下,GC 日志的詳細(xì)解讀 。不同的模式下的日志,對(duì)于新生代、老年代的別稱也是不同的,我們將上一篇文章中的一張?zhí)卣餍畔⒖偨Y(jié)表格拿過來,再次加深一下印象:
JVM 參數(shù)
日志中對(duì)新生代的別稱
日志中對(duì)老年代的別稱
-XX:+
-XX:+
-XX:+
-XX:+
-XX:+
CMS
-XX:+
沒有專門的新生代描繪,有 Eden 和
沒有專門的老年代描繪,有 Heap
5.4 GC 日志的圖形化分析工具
接下來我們需要配合一些圖形化的工具來分析 GC 日志 。
5.4.1
是筆者最為推薦的 GC 日志分析工具,它是一個(gè)在線工具 , 號(hào)稱業(yè)界第一個(gè)以機(jī)器學(xué)習(xí)算法為基礎(chǔ)的 GC 日志分析工具 。它不僅能夠生成多元化的分析圖例(這是免費(fèi)的),還能夠推薦 JVM 配置、提出存在的內(nèi)存問題并推薦對(duì)應(yīng)的解決方案(后面這些功能是付費(fèi)的) 。
我們來看一下免費(fèi)的功能:
5.4.2
是一個(gè)離線的 GC 可視化分析工具,在同類離線工具中,可以說是功能最為強(qiáng)大的了 。
5.4.3
也是一個(gè)離線工具 , 功能相較于顯得比較簡(jiǎn)單,下載地址:/jewes/gchis…
5.4.4
也是一個(gè)離線工具,官方地址國內(nèi)無法打開(/p/…),想要下載的話可以到 CSDN 上找一找 。
其功能與比較相似,效果圖如下:
【五 重學(xué)Android基礎(chǔ)系列篇:Android虛擬機(jī)類和對(duì)象的結(jié)構(gòu)】本文到此結(jié)束,希望對(duì)大家有所幫助 。