JVM

一、JVM虛擬機(jī)的內(nèi)部組成

image

假設(shè)我們創(chuàng)建了一個(gè)類叫Math.java,這個(gè)java文件會(huì)由javac編譯成Math.class字節(jié)碼文件,然后通過類加載,將類信息加載到運(yùn)行時(shí)數(shù)據(jù)區(qū)中,然后再由執(zhí)行引擎來執(zhí)行運(yùn)行時(shí)數(shù)據(jù)區(qū)中的代碼。

調(diào)優(yōu)主要是針對(duì)運(yùn)行時(shí)數(shù)據(jù)區(qū)。

1.1 棧

JVM會(huì)為每個(gè)線程分配一個(gè)棧空間,棧空間中保存一個(gè)一個(gè)的棧幀,一個(gè)方法對(duì)應(yīng)一塊棧幀內(nèi)存區(qū)域

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n9" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Math {
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
}</pre>

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n10" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Test {
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}</pre>

以上面的例子為例。JVM在棧中給main線程分配一塊其專屬的棧空間。main線程執(zhí)行的main方法是一個(gè)棧幀,被壓入??臻g中,然后math.compute()方法又是一個(gè)棧幀,被壓入到棧空間中。

image

每個(gè)棧幀中保存了:

  1. 局部變量表:保存局部變量,如果是基本類型直接存值,如果是引用類型則保存其地址。除此之外還保存了當(dāng)前對(duì)象本身的地址,即this

  2. 操作數(shù)棧

    比如:int a = 1;

    會(huì)先將1這個(gè)常量放入操作數(shù)棧,然后給變量a在局部變量表中分配一塊內(nèi)存空間,然后從操作數(shù)棧中把常量1彈出,然后把這個(gè)1放入到常量a對(duì)應(yīng)的內(nèi)存空間中。

    int c = (a + b) * 10;

    這行代碼首先會(huì)先將局部變量a對(duì)應(yīng)的內(nèi)存空間中的值1放入操作數(shù)棧,然后再將局部變量b對(duì)應(yīng)的內(nèi)存空間中的值2放入操作數(shù)棧,然后從操作數(shù)棧中彈出1和2進(jìn)行"+"操作,然后把計(jì)算出來的指3作為一個(gè)臨時(shí)值壓入操作數(shù)棧中,然后在將常量10壓入操作數(shù)棧中,然后再?gòu)牟僮鲾?shù)棧中彈出3和10進(jìn)行"*"操作,然后將得到的指30壓入到操作數(shù)棧中,然后再將這個(gè)30從操作數(shù)棧中彈出,存入局部變量c對(duì)應(yīng)的內(nèi)存空間

  3. 動(dòng)態(tài)鏈接:比如main方法調(diào)用了math.compute()方法,那么compute方法對(duì)應(yīng)的地址(保存在方法區(qū)中),就保存在動(dòng)態(tài)鏈接中

  4. 方法出口:記錄這個(gè)棧幀對(duì)應(yīng)的方法執(zhí)行完之后,要執(zhí)行的下一行代碼的位置;記錄棧幀對(duì)應(yīng)的方法的返回值

這里的math是main線程的??臻g中的一個(gè)局部變量,因?yàn)槭莕ew實(shí)例化的,所以math保存在??臻g中的是一個(gè)指針,其指向具體math對(duì)象在堆中的地址

1.2 方法區(qū)

JDK1.6及之前使用永久代,常量池和靜態(tài)變量存放在永久代中;JDK1.7開始,字符串常量池、靜態(tài)變量從永久代移動(dòng)到堆中;JDK1.8開始,使用元空間替代永久代。

永久代和元空間的區(qū)別:永久代是JVM虛擬機(jī)中的一塊內(nèi)存空間,而元空間并不在JVM虛擬機(jī)中,是物理機(jī)器本身的內(nèi)存。

方法區(qū)中存放:常量、靜態(tài)變量、加載的類信息、JIT代碼緩存、運(yùn)行時(shí)常量池。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n34" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Math {
public static final int intData = 666; // 常量,存放在方法區(qū)
public static User user = new User(); // 靜態(tài)變量,因?yàn)槭且粋€(gè)引用類型,所以存放其指針在方法區(qū),
// 指針指向其在堆中真正的地址
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
}</pre>

常量池:存在于class文件中,主要包含字面量和符號(hào)引用。字面量:比如類中的成員變量 int i = 5; 這里的5就是字面量;符號(hào)引用: 以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可。方法名,類名,字段名都是符號(hào)引用

運(yùn)行時(shí)常量池:運(yùn)行時(shí)常量池就是常量池被加載到內(nèi)存之后的版本,它的字面量能夠動(dòng)態(tài)添加

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n37" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class TestConst {
public static Integer CONST_A = 1;

public Integer const_B;

public static void main(String[] args) {
TestConst t = new TestConst();
t.const_B = 127; // 動(dòng)態(tài)添加字面量127,放入運(yùn)行時(shí)常量池

Integer const_B_i = 127; // 動(dòng)態(tài)添加字面量127,放入運(yùn)行時(shí)常量池

Integer const_C = 128; // 超過127,不會(huì)放入運(yùn)行時(shí)常量池

Integer const_D = 128; // 超過127,不會(huì)放入運(yùn)行時(shí)常量池

Float const_C_f = 2.0f; // 浮點(diǎn)類型不會(huì)放入運(yùn)行時(shí)常量池

Float const_D_f = 2.0f; // 浮點(diǎn)類型不會(huì)放入運(yùn)行時(shí)常量池

System.out.println(t.const_B == const_B_i); // 結(jié)果為true
System.out.println(t.const_C == const_D); // 結(jié)果為false
System.out.println(t.const_C_f == const_D_f); // 結(jié)果為false
}
}</pre>

1.3 本地方法棧

是JVM底層使用C或C++編寫的方法。

比如Thread的start方法

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n42" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">new Thread().start();</pre>

進(jìn)入到start方法中可以看到Thread的start方法會(huì)調(diào)用一個(gè)由native修飾的start0方法。這個(gè)由native修飾的方法就是本地方法。本地方法棧就是給執(zhí)行本地方法分配的內(nèi)存空間

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n44" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">private native void start0();</pre>

1.4 堆

image

可達(dá)性分析算法:將 GC ROOT 對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索引用的對(duì)象,找到的對(duì)象都被標(biāo)記為非垃圾對(duì)象,其余未被標(biāo)記的對(duì)象都是垃圾對(duì)象

GC ROOT :線程棧中的局部變量表中引用的對(duì)象、靜態(tài)屬性引用的對(duì)象;本地方法棧的變量;方法區(qū)中常量引用的對(duì)象

關(guān)于垃圾回收,例子:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n52" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class HeapTest {
public static void main(String[] args) throws InterruptedException {
ArrayList<HeapTest> heapTests = new ArrayList<>();
while(true) {
heapTests.add(new HeapTest());
Thread.sleep(10);
}
}
}</pre>

這里創(chuàng)建出來的一個(gè)個(gè)new HeapTest() 對(duì)象不會(huì)被垃圾回收,因?yàn)楦鶕?jù)可達(dá)性分析算法,所有創(chuàng)建出來的new HeapTest() 對(duì)象都被heapTests對(duì)象引用,而heapTests對(duì)象是在main線程棧幀的局部變量表中引用的對(duì)象

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

每個(gè)線程都有單獨(dú)的程序計(jì)數(shù)器,記錄該線程將要執(zhí)行的下一行代碼的位置,這里的代碼指的是javac編譯過后的字節(jié)碼文件代碼

Code下的編號(hào)就是用來被保存在程序計(jì)數(shù)器中的。

因?yàn)镴VM是支持多線程的,比如線程A執(zhí)行完第1行代碼后,線程B搶到了CPU時(shí)間碎片,等線程B釋放CPU后,線程A再次獲取到CPU時(shí)間碎片接著往下執(zhí)行,那么線程A重新獲取到CPU后怎么知道它接下來應(yīng)該執(zhí)行哪行代碼,就需要去它專屬的程序計(jì)數(shù)器中找。

二、JVM調(diào)優(yōu)

2.1 GC算法

2.1.1 三大垃圾回收算法

  1. 標(biāo)記清除算法:根據(jù)可達(dá)性分析算法,將不在GC Root引用鏈上的對(duì)象進(jìn)行標(biāo)記,然后清除。優(yōu)點(diǎn)是回收效率高,不浪費(fèi)內(nèi)存;缺點(diǎn)是會(huì)產(chǎn)生內(nèi)存碎片化。

  2. 復(fù)制算法:對(duì)內(nèi)存進(jìn)行分區(qū),根據(jù)可達(dá)性分析算法,將存活的對(duì)象進(jìn)行從內(nèi)存中的一塊區(qū)域復(fù)制到另一塊區(qū)域,并排好序,然后將原來的區(qū)域中的對(duì)象整個(gè)回收掉。優(yōu)點(diǎn)是不產(chǎn)生內(nèi)存碎片化,效率適中;缺點(diǎn)是浪費(fèi)內(nèi)存。

  3. 標(biāo)記整理算法:根據(jù)可達(dá)性分析算法,將存活的對(duì)象與要回收的對(duì)象進(jìn)行重新排列,先排列存活的對(duì)象,再排列被標(biāo)記的對(duì)象,然后再清除。優(yōu)點(diǎn)是不產(chǎn)生內(nèi)存碎片化,不浪費(fèi)內(nèi)存;缺點(diǎn)是回收效率下。

2.1.2 三色標(biāo)記算法

2.1.2.1 原理

并發(fā)垃圾回收算法:三色標(biāo)記算法:

  1. 首先我們從GC Roots開始枚舉,它們所有的直接引用變?yōu)榛疑?,自己變?yōu)楹谏?梢韵胂?strong>有一個(gè)隊(duì)列用于存儲(chǔ)灰色對(duì)象,會(huì)把這些灰色對(duì)象放到這個(gè)隊(duì)列中

  2. 然后從隊(duì)列中取出一個(gè)灰色對(duì)象進(jìn)行分析:將這個(gè)對(duì)象所有的直接引用變?yōu)榛疑?,放入?duì)列中,然后這個(gè)對(duì)象變?yōu)楹谏?;如果取出的這個(gè)灰色對(duì)象沒有直接引用,那么直接變成黑色

  3. 繼續(xù)從隊(duì)列中取出一個(gè)灰色對(duì)象進(jìn)行分析,分析步驟和第二步相同,一直重復(fù)直到灰色隊(duì)列為空

  4. 分析完成后仍然是白色的對(duì)象就是不可達(dá)的對(duì)象,可以作為垃圾被清理

  5. 最后重置標(biāo)記狀態(tài)

  6. 顏色會(huì)記錄在對(duì)象頭的markwork

前面的描述都比較抽象,這里以一個(gè)例子進(jìn)行說明,假設(shè)現(xiàn)在有以下引用關(guān)系:

image

首先,所有GC Root的直接引用(A、B、E)變?yōu)榛疑湃腙?duì)列中,GC Root變?yōu)楹谏?/p>

image

然后從隊(duì)列中取出一個(gè)灰色對(duì)象進(jìn)行分析,比如取出A對(duì)象,將它的直接引用C、D變?yōu)榛疑湃腙?duì)列,A對(duì)象變?yōu)楹谏?/p>

image

然后從隊(duì)列中取出一個(gè)灰色對(duì)象進(jìn)行分析,比如取出A對(duì)象,將它的直接引用C、D變?yōu)榛疑湃腙?duì)列,A對(duì)象變?yōu)楹谏?/p>

image

繼續(xù)從隊(duì)列中取出一個(gè)灰色對(duì)象E,但是E對(duì)象沒有直接引用,變?yōu)楹谏?/p>

image

同理依次取出C、D、F對(duì)象,他們都沒有直接引用,那么變成黑色(這里就不一個(gè)一個(gè)的畫了):

image

到這里分析已經(jīng)結(jié)束了,還剩一個(gè)G對(duì)象是白色,證明它是一個(gè)垃圾對(duì)象,不可訪問,可以被清理掉。

2.1.2.2 存在的問題

非垃圾變成了垃圾:

比如我們回到上述流程中的這個(gè)狀態(tài):

image

此時(shí)E對(duì)象已經(jīng)被標(biāo)記為黑色,表示不是垃圾,不會(huì)被清除。此時(shí)某個(gè)用戶線程將GC Root2和E對(duì)象之間的關(guān)聯(lián)斷開了(比如 xx.e=null;):

image

后面的圖就不用畫了,很顯然,E對(duì)象變?yōu)榱死鴮?duì)象,但是由于已經(jīng)被標(biāo)記為黑色,就不會(huì)被當(dāng)做垃圾刪除,姑且也可以稱之為浮動(dòng)垃圾。

垃圾變成了非垃圾:

如果上面提到的浮動(dòng)垃圾你覺得沒啥所謂,即使本次不清理,下一次GC也會(huì)被清理,而且并發(fā)清理階段也會(huì)產(chǎn)生所謂的浮動(dòng)垃圾,影響不大。但是如果一個(gè)垃圾變?yōu)榱朔抢?,那么后果就?huì)比較嚴(yán)重。比如我們回到上述流程中的這個(gè)狀態(tài):

image

標(biāo)記的下一步操作是從隊(duì)列中取出B對(duì)象進(jìn)行分析,但是這個(gè)時(shí)候GC線程的時(shí)間片用完了,操作系統(tǒng)調(diào)度用戶線程來運(yùn)行,而用戶線程先執(zhí)行了這個(gè)操作:A.f = F;那么引用關(guān)系變成了:

image

接著執(zhí)行:B.f=null;那么引用關(guān)系變成了:

image

好了,用戶線程的事兒干完了,GC線程重新開始運(yùn)行,按照之前的標(biāo)記流程繼續(xù)走:從隊(duì)列中取出B對(duì)象,發(fā)現(xiàn)B對(duì)象沒有直接引用,那么將B對(duì)象變?yōu)楹谏?/p>

image

接著繼續(xù)分別從隊(duì)列中取出E、C、D三個(gè)灰色對(duì)象,它們都沒有直接引用,那么變?yōu)楹谏珜?duì)象:

image

到現(xiàn)在所有灰色對(duì)象分析完畢,你肯定已經(jīng)發(fā)現(xiàn)問題了,出現(xiàn)了黑色對(duì)象直接引用白色對(duì)象的情況,而且雖然F是白色對(duì)象,但是它是垃圾嗎?顯然不是垃圾,如果F被當(dāng)做垃圾清理掉了,就會(huì)造成空指針異常。

所以在后來新版本的CMS工具中對(duì)三色標(biāo)記算法進(jìn)行了優(yōu)化

寫屏障:如果黑色對(duì)象有引用指向白色對(duì)象,則在這個(gè)引用建立好之后,黑色對(duì)象變?yōu)榛疑?/strong>

GC工具:

  1. Serial:工作在年輕代,單線程。因?yàn)楣ぷ髟谀贻p代,所以是復(fù)制算法

  2. Serial Old:工作在老年代,單線程。因?yàn)楣ぷ髟诶夏甏杂玫氖菢?biāo)記整理算法

    以上兩個(gè)進(jìn)行組合,只能用在小型程序中(幾十M)

  3. Parallel Scavenge:相當(dāng)于多線程的Serial

  4. Parallel Old:相當(dāng)于多線程的Serial Old

    以上兩個(gè)是JDK1.8的默認(rèn)組合,一般用在幾個(gè)G的程序中

  5. ParaNew:是基于Parallel Scavenge的優(yōu)化,用于配合CMS

  6. CMS:并發(fā)垃圾回收工具?;谌珮?biāo)記算法。即業(yè)務(wù)線程和垃圾回收線程可以同時(shí)工作。因?yàn)槎嗑€程的Parallel Old并不是線程越多越好,線程多了會(huì)造成線程管理上的消耗,所以在幾十G的大型程序中會(huì)考慮使用CMS

    過程如下:

    image

    初始標(biāo)記:找到 GC Root 對(duì)象,這一步是STW的

    并發(fā)標(biāo)記:基于三色標(biāo)記算法。標(biāo)記的同時(shí)業(yè)務(wù)線程也在運(yùn)行。CMS的優(yōu)化:寫屏障:如果黑色對(duì)象有引用指向白色對(duì)象,則在這個(gè)引用建立好之后,黑色對(duì)象變?yōu)榛疑?/strong>

    重新標(biāo)記:在經(jīng)過了三色標(biāo)記算法并發(fā)標(biāo)記之后,從黑色的對(duì)象開始重新掃描一邊,這一步是STW。因?yàn)橹粡暮谏珜?duì)象開始掃描,就會(huì)比從頭開始STW掃描所有對(duì)象要快得多

    并發(fā)清理:把所有重新標(biāo)記過后的白色對(duì)象進(jìn)行垃圾回收,同時(shí)業(yè)務(wù)線程也在運(yùn)行

  7. G1:是對(duì)CMS的一個(gè)優(yōu)化:使用原始快照(STAB):就是當(dāng)灰色對(duì)象指向白色對(duì)象的引用消失的時(shí)候,將這種情況下的白色對(duì)象的引用記錄下來,直到重新標(biāo)記的時(shí)候,對(duì)這些記錄下來的對(duì)象進(jìn)行最終判斷,然后再進(jìn)行清理?,F(xiàn)在大型程序一般不用CMS了,通常使用G1

2.2 調(diào)優(yōu)

2.2.1 調(diào)優(yōu)步驟

調(diào)優(yōu)第一步:指定堆內(nèi)存大小,如果我們能確定程序大概需要的堆內(nèi)存,就把最小堆內(nèi)存和最大堆內(nèi)存設(shè)置為一樣的,減少內(nèi)存抖動(dòng)

調(diào)優(yōu)第二步:換垃圾回收器。

查看當(dāng)前Java程序使用的垃圾回收器。根據(jù)JVM內(nèi)存大小更換垃圾回收器

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n156" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">java -XX:+PrintCommondLineFlags -version</pre>

2.2.2 調(diào)優(yōu)工具

  1. jps:定位到我們要找的Java程序的進(jìn)程號(hào)

  2. jinfo 進(jìn)程號(hào):打印進(jìn)程號(hào)對(duì)應(yīng)的Java程序信息,可以查看啟動(dòng)參數(shù)

  3. jstat -gc 進(jìn)程號(hào) 500:打印進(jìn)程號(hào)對(duì)應(yīng)的Java程序的GC信息,可以看到Full GC次數(shù),Eden區(qū) S0 S1區(qū)占用空間大小,最后跟著的500單位是毫秒,代表每500毫秒就打印一次

  4. jstack 進(jìn)程號(hào):會(huì)把進(jìn)程號(hào)對(duì)應(yīng)的Java程序所有的線程都打印出來,包括線程名稱、線程優(yōu)先級(jí),還會(huì)打印每個(gè)線程執(zhí)行的方法鏈條(即線程堆棧),以及線程狀態(tài),是否在等待鎖(WAITING),以及是垃圾回收線程(VM GC)還是業(yè)務(wù)線程

  5. top:Linux系統(tǒng)級(jí)命令,類似Windows的任務(wù)管理器,可以查看到進(jìn)程占用的CPU比例,占用的內(nèi)存比例;top -Hp 進(jìn)程號(hào):可以查看到對(duì)應(yīng)進(jìn)程內(nèi)部所有線程所占的CPU比例,占用的內(nèi)存比例,根據(jù)這個(gè)打印結(jié)果可以查看占用CPU最高的線程,然后到j(luò)stack中去查找其對(duì)應(yīng)執(zhí)行的方法。如果是垃圾回收線程占用比較高,可以去看垃圾回收日志,看是否是頻繁Full GC

  6. jmap -histo 進(jìn)程號(hào):可以打印出進(jìn)程號(hào)對(duì)應(yīng)的Java程序當(dāng)前哪些類都有多少個(gè)對(duì)象,占多少內(nèi)存。當(dāng)出現(xiàn)頻繁的異常Full GC時(shí)(每次Full GC只回收掉一點(diǎn)點(diǎn)內(nèi)存就是異常)就可以用來查看到底是哪些對(duì)象一直沒有被釋放。

    注意:jmap命令不能在生產(chǎn)環(huán)境運(yùn)行,因?yàn)閖map命令會(huì)讓JVM暫時(shí)卡住,然后輸出卡住的那個(gè)狀態(tài)下的對(duì)象占用。使用場(chǎng)景一般是在測(cè)試環(huán)境壓測(cè)后執(zhí)行

    jmap -dump:format=b,file=20220601.hprof 進(jìn)程號(hào):可以進(jìn)行Java程序的堆轉(zhuǎn)儲(chǔ),即可以把JVM中的堆信息以文件的形式導(dǎo)出,然后可以使用JDK自帶的Java VisualVM工具來進(jìn)行圖形化界面的展示

  7. 在Java程序啟動(dòng)的時(shí)候可以設(shè)置這樣一個(gè)啟動(dòng)參數(shù),在OOM的時(shí)候自動(dòng)存儲(chǔ)堆轉(zhuǎn)儲(chǔ)文件

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n176" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">-XX:+HeapDumpOnOutOfMemoryError</pre>

2.2.3 調(diào)優(yōu)案例

JVM調(diào)優(yōu)主要就是減少Full GC的次數(shù)。因?yàn)镕ull GC的調(diào)用會(huì)有一個(gè)STW機(jī)制,即把正在運(yùn)行的線程掛起,來執(zhí)行我們的Full GC,等Full GC執(zhí)行完畢后,線程才能接著工作。因?yàn)槿绻籗TW,可能就會(huì)有一邊回收,對(duì)象的引用狀態(tài)一邊改變的可能,會(huì)導(dǎo)致垃圾回收不徹底。

什么情況下會(huì)產(chǎn)生Full GC:

  1. 老年代空間滿了

  2. 元空間滿了

  3. 手動(dòng)調(diào)用了System.gc()方法

實(shí)例:

對(duì)于一個(gè)每秒創(chuàng)建300個(gè)訂單的機(jī)器,假設(shè)每秒產(chǎn)生的對(duì)象總量為60M(這些對(duì)象大多數(shù)都會(huì)在1秒之后變?yōu)槔鴮?duì)象,這是因?yàn)楸热缬唵螌?duì)象,是一個(gè)局部變量,等訂單寫入后臺(tái)成功,方法執(zhí)行結(jié)束后,其指針就會(huì)從棧中彈出,導(dǎo)致堆中的對(duì)象失去了引用,變?yōu)槔鴮?duì)象)

如果對(duì)于一個(gè)8G的內(nèi)存的服務(wù)器來說。如果我們分配3個(gè)G給堆內(nèi)存

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n193" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">java -Xms3G -Xmx3G -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar xxxxxxx-service.jar

-Xms堆內(nèi)存最小,不指定是物理內(nèi)存的64分之1

-Xmx堆內(nèi)存最大,不指定是物理內(nèi)存的4分之1

-Xss棧內(nèi)存 -XX:MetaspaceSize元空間內(nèi)存

最大堆和最小堆設(shè)置的值一樣是為了防止內(nèi)存抖動(dòng),

即剛開始按照最小堆來分配內(nèi)存,內(nèi)存不足時(shí)要進(jìn)行擴(kuò)容,等內(nèi)存太充足又要釋放內(nèi)存</pre>

image

如果我們不指定堆內(nèi)部的內(nèi)存分配默認(rèn)會(huì)按照老年代2:1新生代來分配內(nèi)存。其中新生代的eden區(qū)和s0 s1是8:1:1。

如果假設(shè)我們機(jī)器運(yùn)行14秒會(huì)把eden區(qū)占滿,在進(jìn)行minor gc前1秒產(chǎn)生的對(duì)象因?yàn)镾TW機(jī)制,線程被掛起,掃描對(duì)象的時(shí)候不會(huì)被認(rèn)為是垃圾對(duì)象,從而這些最后一秒產(chǎn)生的60M的對(duì)象不會(huì)被minor gc回收,按照常理來說應(yīng)該會(huì)被移動(dòng)到s0區(qū),但是有些情況下對(duì)象會(huì)直接進(jìn)入到老年代:

  1. 大對(duì)象直接進(jìn)入到老年代,這個(gè)閾值是可以設(shè)定的,默認(rèn)值是0,表示任何大小的對(duì)象都會(huì)先嘗試在eden區(qū)中分配

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n200" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">-XX:PretenureSizeThreshold=1024000(這個(gè)單位是字節(jié)) -XX:+UseSerialGC</pre>

  2. 長(zhǎng)期存活的對(duì)象:即每經(jīng)過一次minor gc,年齡增長(zhǎng)一歲,達(dá)到閾值后進(jìn)入到老年代,默認(rèn)值是15

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n203" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">-XX:MaxTenuringThreshold=10</pre>

  3. 對(duì)象動(dòng)態(tài)年齡判斷:一批對(duì)象的總大小如果大于將要進(jìn)入的S0區(qū)域的50%(默認(rèn)50%,可以修改),那么這批對(duì)象都會(huì)直接進(jìn)入老年代

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n206" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">-XX:TargetSurvivorRatio=80</pre>

上面的例子就是因?yàn)榈?點(diǎn),所以這一批60M的對(duì)象超過了100M的50%,直接進(jìn)入了老年代,導(dǎo)致頻繁產(chǎn)生Full GC。

解決方法:1. 提高對(duì)象動(dòng)態(tài)年齡判斷百分比;2.在內(nèi)存分配的時(shí)候給新生代分配內(nèi)存更大些

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n209" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">java -Xms3G -Xmx3G -Xmn2G -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar xxxxxxx-service.jar

-Xmn分配堆中新生代的內(nèi)存大小</pre>

三、類加載

3.1 類加載過程

image
  1. loading:加載,把.class文件加載到內(nèi)存

  2. linking

    2.1 verification:校驗(yàn),驗(yàn)證.class文件是否符合規(guī)范

    2.2 preparation:準(zhǔn)備,靜態(tài)成員變量賦默認(rèn)值

    2.3 resolution:解析,將類、方法、屬性等符號(hào)引用解析為直接引用,常量池中的各種符號(hào)引用解析為指針、偏移量等內(nèi)存地址的直接引用

  3. initializing:初始化,給靜態(tài)成員變量賦初值

3.2 類加載器(ClassLoader)

一個(gè)class文件被加載到內(nèi)存中分為兩部分:一部分是class文件的二進(jìn)制內(nèi)容;還有一部分是生成了一個(gè)Class對(duì)象(在方法區(qū)中),指向了class文件的二進(jìn)制內(nèi)容。

比如 String.class 獲取的就是String這個(gè)class文件的Class對(duì)象,小class可以理解為Class的實(shí)例,且是單例模式,因?yàn)橥粋€(gè)類只會(huì)有一個(gè)小class。所以 String.class 也可以寫成String.getClass()

在反射中,我們都是通過這個(gè)class對(duì)象去找它對(duì)應(yīng)的class文件的二進(jìn)制內(nèi)容。比如 String.class.getMethods() 通過反射去獲取String這個(gè)類的所有方法,就會(huì)通過String的class對(duì)象去找它對(duì)應(yīng)的String.class文件的二進(jìn)制內(nèi)容,來解析成Java指令運(yùn)行,或者說String.class文件的二進(jìn)制內(nèi)容被解析成Java指令,存放到class對(duì)象中,通過class對(duì)象的方法來進(jìn)行調(diào)用。

3.2.1 類加載器的層次(雙親委派模型)

image
  1. Bootstrap:最頂層的核心類庫(kù)中的類是由Bootstrap類加載器加載的,比如String類。是C++實(shí)現(xiàn)的一個(gè)類加載器

  2. Extension:用來加載擴(kuò)展jar包 jre/lib/ext/*.jar下的所有類

  3. App:我們自己寫的類,是由App類加載器加載

  4. 自定義的ClassLoader

一個(gè)類被類加載器加載的過程(雙親委派機(jī)制)是這樣:

比如我們要通過自定義ClassLoader來加載一個(gè)類,如果從來沒有被加載過,并不會(huì)直接由自定義ClassLoader進(jìn)行加載,自定義ClassLoader會(huì)先去它已加載過的緩存中找看是否這個(gè)類已經(jīng)被加載,如果已經(jīng)被加載就直接返回結(jié)果,否則就去詢問他的父類加載器App加載器,看App加載器中是否已經(jīng)加載了這個(gè)類,如果App加載器中沒有,就再去App的父類加載器Extension中詢問,如果沒有就去最頂層的Bootstrap加載器中詢問,如果Bootstrap中還是沒有加載這個(gè)類進(jìn)來,Bootstrap又會(huì)向他的子類加載器Extension詢問,如果Extension沒有,就再去App詢問,如果還是沒有,最終才會(huì)由自定義ClassLoader進(jìn)行類加載,如果最終都沒有加載成功,就會(huì)報(bào)ClassNotFoundException異常。

image

A:為什么要使用雙親委派機(jī)制進(jìn)行類加載?

Q:主要是為了安全。比如我們想使用一個(gè)自定義類加載器加載 java.lang.String,如果沒有雙親委派機(jī)制,就會(huì)把我們自己寫的一個(gè) java.lang.String 覆蓋掉JDK原來的 java.lang.String。次要問題是資源浪費(fèi)的問題,如果一個(gè)類已經(jīng)加載過直接返回結(jié)果就行,不需要重復(fù)加載。

注意:父加載器不是“類加載器的加載器”!也不是“類加載器的父類加載器”。父加載器只是這個(gè)當(dāng)前這個(gè)加載器里的一個(gè)parent變量定義的加載器。

實(shí)例:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n248" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class ParentClassLoaderTest {
public static void main(String[] args) {
// 返回類型為AppClassLoader的一個(gè)對(duì)象
System.out.println(ParentClassLoaderTest.class.getClassLoader());
/*
返回null,因?yàn)檫@里的getClass得到的是AppClassLoader這個(gè)類的class對(duì)象,
這個(gè)類是由最頂層的Bootstrap加載器加載的,所以getClassLoader()理論上應(yīng)該返回Bootstrap的一個(gè)對(duì)象,
但實(shí)際上Bootstrap的實(shí)現(xiàn)是由C++實(shí)現(xiàn)的,所以并沒有一個(gè)Java類型的對(duì)象與之對(duì)應(yīng),所以在Java程序中g(shù)et不到
*/
System.out.println(ParentClassLoaderTest.class.getClassLoader().getClass().getClassLoader());

/*
返回類型為ExtensionClassLoader的一個(gè)對(duì)象,因?yàn)槭茿ppClassLoader的父加載器
/
System.out.println(ParentClassLoaderTest.class.getClassLoader().getParent());
/

返回null,原因是ExtensionClassLoader的父加載器是Bootstrap
*/
System.out.println(ParentClassLoaderTest.class.getClassLoader().getParent().getParent());
}
}</pre>

3.2.2 自定義類加載器

自定義類加載器需要繼承ClassLoader類,并重寫其findClass方法

實(shí)例:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n253" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 這里的name就是class的全類名
// 指定去哪個(gè)文件加載class文件,這個(gè)class文件就不一定是我們項(xiàng)目路徑下的了
File f = new File("c:/test/" + name.replaceAll(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;

while ((b = fis.read()) != 0) {
baos.write(b);
}

byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
// defineClass方法是把全類名跟加載進(jìn)來的二進(jìn)制class文件進(jìn)行綁定,然后返回一個(gè)Class對(duì)象
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}

public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> clazz = myClassLoader.loadClass("com.atguigu.gulimall.classLoader.Hello");
System.out.println(clazz.getName());
}
}
</pre>

loadClass方法源碼:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n255" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
/*
可以看到loadClass方法如果父加載器都沒找到的話就會(huì)調(diào)用自己的findClass方法,
所以我們自定義的ClassLoader只要重寫findClass方法就好
*/
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}</pre>

比如我們想加載一個(gè)不在我們項(xiàng)目目錄下的class文件,就可以使用自定義的ClassLoader來寫。首先創(chuàng)建一個(gè)類,繼承ClassLoader,然后重寫findClass方法,傳入的參數(shù)是全類名,然后實(shí)例化一個(gè)自定義的類加載器對(duì)象,然后調(diào)用它的loadClass方法,loadClass方法的過程就是雙親委派的過程

四、對(duì)象

4.1 對(duì)象在內(nèi)存中的布局

4.1.1 普通對(duì)象

image

分為四部分:

  1. markword:8個(gè)字節(jié)。

    1.1 記錄了hashCode,即調(diào)用過一次對(duì)象的hashCode方法后,會(huì)將hashCode(32bit)保存在markword中,下次再調(diào)用hashCode方法就直接從這里拿,不再計(jì)算一次

    1.2 記錄了鎖信息,即如果把這個(gè)對(duì)象當(dāng)成資源進(jìn)行加鎖,會(huì)記錄這個(gè)對(duì)象被加了鎖

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n267" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">System.out.println(ClassLayout.parseInstance(t).toPrintable()); // 打印對(duì)象內(nèi)存信息
    synchronized(t) {
    // ...
    System.out.println(ClassLayout.parseInstance(t).toPrintable()); // 打印對(duì)象內(nèi)存信息
    }
    System.out.println(ClassLayout.parseInstance(t).toPrintable()); // 打印對(duì)象內(nèi)存信息

    /*
    會(huì)發(fā)現(xiàn),加了鎖之后的markword和加鎖前,鎖釋放后的markword不一樣
    */</pre>

    1.3 記錄了垃圾回收信息,即這個(gè)對(duì)象處于垃圾回收的哪個(gè)階段(新生代Eden、S1、S2;老年代),還有垃圾回收時(shí)的年齡

  2. class pointer:4個(gè)字節(jié),保存一個(gè)指針,指向new的這個(gè)對(duì)象的class文件

    (1和2算對(duì)象頭)

  3. 實(shí)例數(shù)據(jù):比如int a int b,這些成員變量。這里注意,如果成員變量不是基本類型,比如String,在當(dāng)前對(duì)象里就只會(huì)保留指針,即4個(gè)字節(jié)

  4. 對(duì)齊:因?yàn)楝F(xiàn)在的機(jī)器大多是64位的,64位占8個(gè)字節(jié),所以如果當(dāng)前new出來的對(duì)象只有60個(gè)字節(jié),需要補(bǔ)齊到8的整數(shù)倍,即64個(gè)字節(jié),才是這個(gè)對(duì)象所占的內(nèi)存大小

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n276" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class T {
int a;
int b;
boolean flag;
String str = "hello";
}

/*
這個(gè)對(duì)象所占內(nèi)存大小分析:
1. makrword + class point = 12 字節(jié)
2. 成員變量: 兩個(gè)int 8 字節(jié) + 一個(gè)boolean 4 字節(jié)
(因?yàn)椴紶栴愋停瑂hort類型,byte類型雖然都不足4個(gè)字節(jié),但是有一個(gè)內(nèi)部對(duì)齊的機(jī)制
所以需要補(bǔ)齊到4個(gè)字節(jié))+ 一個(gè)引用類型的String 4 個(gè)字節(jié)
(只保存指針,真正的hello字符串在字符串常量池中)
= 16個(gè)字節(jié)
3. 12 + 16 = 28個(gè)字節(jié),不能被8整除,需要再補(bǔ)4個(gè)字節(jié)補(bǔ)齊到32個(gè)字節(jié)
所以這個(gè)T對(duì)象占32個(gè)字節(jié)
*/</pre>

指針大?。杭词故?4位機(jī)器也默認(rèn)是32bit的指針,即4個(gè)字節(jié), 這是因?yàn)镴ava虛擬機(jī)啟動(dòng)時(shí)默認(rèn)使用了壓縮指針,把指針壓縮為32bit

4.1.2 數(shù)組對(duì)象

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n280" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">int[] a = new int[4];
T[] t = new T[5];</pre>

數(shù)組對(duì)象和普通對(duì)象所占內(nèi)存的四個(gè)部分是一樣的,區(qū)別在于數(shù)組對(duì)象多了4個(gè)字節(jié)來記錄數(shù)組長(zhǎng)度

image

4.2 對(duì)象怎么分配

對(duì)象分配步驟:

  1. 剛創(chuàng)建的對(duì)象會(huì)先嘗試看是否能在棧上分配。在棧上分配的好處是每個(gè)方法在棧中有一塊自己管理的空間,當(dāng)方法運(yùn)行結(jié)束,這個(gè)方法所管理的空間就從棧里被彈出,那么這個(gè)空間里所有的對(duì)象都會(huì)被清理掉,也就不存在垃圾回收了。在棧上分配的條件是根據(jù)逃逸分析來的,即在方法中創(chuàng)建的這個(gè)對(duì)象只用于這個(gè)方法內(nèi)部,如果脫離了這個(gè)方法所管理的空間,在這個(gè)方法所管理的空間中就只能保存這個(gè)對(duì)象的指針,真實(shí)的對(duì)象會(huì)放到堆內(nèi)存

  2. 對(duì)于不能在棧上分配的對(duì)象。先判斷對(duì)象的大小,如果超過了新生代內(nèi)存閾值,會(huì)直接把對(duì)象放入到老年代中

  3. 如果對(duì)象大小沒超過閾值,會(huì)根據(jù)JVM的TLAB分配方法,將對(duì)象分配到新生代的Eden區(qū)中。TLAB是指,為了防止線程沖突(比如有兩個(gè)內(nèi)存都想搶占同一個(gè)地址的內(nèi)存,這時(shí)候就會(huì)加鎖,看誰先搶到鎖,誰來占用地址,這會(huì)導(dǎo)致效率低下),JVM會(huì)為每一個(gè)線程分配一塊內(nèi)存區(qū)域,這個(gè)區(qū)域是線程私有的,所以線程在分配對(duì)象內(nèi)存的時(shí)候優(yōu)先在自己私有的內(nèi)存區(qū)域中分配,這樣就不會(huì)和其他內(nèi)存產(chǎn)生沖突,提高運(yùn)行效率,當(dāng)這塊區(qū)域滿了之后,才會(huì)到公共區(qū)域中進(jìn)行搶鎖分配內(nèi)存。

  4. 進(jìn)入到Eden區(qū)域后就進(jìn)行垃圾回收,如果第一次回收還存活的就年齡+1被分配到S1區(qū),第二次回收還存活就年齡+1被分配到S2區(qū),第三次回收還存活就年齡+1被分配到S1區(qū),直到年齡到達(dá)閾值后被分配到老年代區(qū),然后直到Full GC才被清除(當(dāng)老年代空間不足就會(huì)執(zhí)行Full GC)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容