引言
先上圖

棧
功能:存放局部變量(線程棧)
1. 棧幀
(1)概念
一個(gè)方法對(duì)應(yīng)一塊棧幀內(nèi)存區(qū)域,該棧幀中保存方法中聲明的局部變量

(2)棧幀結(jié)構(gòu)
棧幀由局部變量表,操作數(shù)棧,動(dòng)態(tài)鏈接,方法出口等部分組成。其中:
操作數(shù)棧:方法運(yùn)行時(shí),用于變量操作的臨時(shí)內(nèi)存,如運(yùn)算(1 + 1 = 2)時(shí)在操作數(shù)棧分配的內(nèi)存中完成,后復(fù)制給棧幀中的局部變量;
動(dòng)態(tài)鏈接:運(yùn)行時(shí),將符號(hào)變?yōu)槠浯a對(duì)應(yīng)的直接內(nèi)存地址(直接引用);
例如:func()運(yùn)行時(shí),將方法區(qū)中對(duì)應(yīng)邏輯地址拷貝入動(dòng)態(tài)鏈接之中;
方法入口:返回main()棧幀;
2.程序計(jì)數(shù)器
(1) 概念:每一個(gè)線程獨(dú)有的內(nèi)存空間,存放每個(gè)線程即將執(zhí)行的代碼行號(hào)
(2) 作用:多線程場(chǎng)景下,當(dāng)前線程CPU被搶占,掛起后由程序計(jì)數(shù)器記錄的行號(hào)恢復(fù)程序

3. 棧,方法區(qū)與堆關(guān)系
棧中對(duì)象通過指針指向其堆中分配地址
方法區(qū)中常保存常量,靜態(tài)變量及類信息

4. 對(duì)象的創(chuàng)建及結(jié)構(gòu)

4.1 分配內(nèi)存的方法:
(1)指針碰撞方法:
條件:當(dāng)Java堆內(nèi)存絕對(duì)整齊的排列時(shí)(較常見)

(2)空閑列表方式:
當(dāng)java堆中已用內(nèi)存不規(guī)整時(shí),已使用的內(nèi)存和空閑內(nèi)存相互交錯(cuò),無法使用指針碰撞方法,虛擬機(jī)中維持一個(gè)map,記錄還有哪些內(nèi)存塊可用,有新對(duì)象要分配內(nèi)存時(shí),JVM分配map中的空閑內(nèi)存塊,并更新map中的信息。
5. 堆結(jié)構(gòu)與垃圾收集
5.1 堆結(jié)構(gòu)與垃圾收集結(jié)構(gòu)圖

注1: 一般老年代最后留存的對(duì)象為靜態(tài)對(duì)象,在7*24小時(shí)web服務(wù)中,靜態(tài)對(duì)象將一直存活
注2: 一個(gè)OOM(內(nèi)存溢出)小案例
public class HeapTest {
byte[] a = new byte[1024 * 100];
public static void main(String[] args) {
ArrayList<HeapTest> heapTests = new ArrayList<>();
while(True) {
heapTests.adds(new HeapTest());
# 通過可達(dá)性分析方式不能回收該部分內(nèi)存
}
}
}
5.2 對(duì)象的并發(fā)放入方式
在高并發(fā)web服務(wù)中,對(duì)象會(huì)嘗試通過高并發(fā)的方式放入堆中分配的空間,其中有兩種方式:
(1)Cas+失敗重試方案
(2)本地線程分配緩沖(TLAB:JDK1.8默認(rèn)方式)
JVM預(yù)先為每個(gè)線程分配對(duì)應(yīng)的內(nèi)存塊
5.3 對(duì)象結(jié)構(gòu)
(1)對(duì)象由對(duì)象頭,實(shí)例數(shù)據(jù)及對(duì)齊填充組成

(2)對(duì)象結(jié)構(gòu)的優(yōu)化:指針壓縮(JDK默認(rèn):8字節(jié)對(duì)象壓縮至4字節(jié))
- VM options: -XX: +UseCompressedOps
- 對(duì)象指針壓縮優(yōu)點(diǎn):大大縮小對(duì)象占用內(nèi)存,例如:
通過一定的壓縮算法,使得:
35位的對(duì)象可以由32位的機(jī)器進(jìn)行保存。
5.4 對(duì)象內(nèi)存流程

(1)對(duì)象逃逸現(xiàn)象及分析
例1:
User u = CreateUser();
public User CreateUser() {
User user = new User();
user.setId(1);
return user;
}
// 局部對(duì)象user逃逸出了它的create方法,JVM視情況將這種對(duì)象的內(nèi)存分配至堆中保存
例2:
public void CreateUser() {
User user = new User();
user.setId(1);
return ;
}
// 該局部變量user未逃逸,對(duì)于該種情況,JVM將判斷對(duì)象所占內(nèi)存大小,若內(nèi)存較小,將對(duì)象保存入方法createUser對(duì)應(yīng)的棧楨中;
// 優(yōu)點(diǎn):該方法結(jié)束時(shí),內(nèi)存(棧楨)被銷毀,該對(duì)象內(nèi)存被回收,以這種方式降低GC頻率
(2)JVM對(duì)象逃逸處理
JVM虛擬機(jī)通過參數(shù)(-XX:+DoEscapeAnalysis) JDK默認(rèn)開啟
標(biāo)量替換:若棧楨中無連續(xù)空間放置對(duì)象,僅將對(duì)象中成員變量放入棧楨中

標(biāo)量替換參數(shù):XX: +EliminateAllocations
5.5 對(duì)象的分配:
(1)對(duì)象主要在Eden區(qū)分配,當(dāng)Eden區(qū)空間不足時(shí)觸發(fā)一次minor GC
(2)大對(duì)象直接進(jìn)入老年代(建議直接進(jìn)入老年代,不然增加年輕代回收頻率)
大對(duì)象定義: -XX : PretenureSize Threshold = 10000000(大對(duì)象對(duì)應(yīng)閾值)
巧妙設(shè)計(jì)分代年齡:根據(jù)業(yè)務(wù)需要設(shè)置分代年齡,使對(duì)象不必經(jīng)過15次GC才進(jìn)入老年代區(qū)(-XX:MaxTenuringThreshold)
6. 老年代空間分配擔(dān)保機(jī)制

注:在做gc之前,會(huì)判斷老年代空間是否可存下eden區(qū)所有Obj,若存不下進(jìn)行一次minor gc,通過老年代擔(dān)保機(jī)制判斷老年代剩余空間是否小于歷史每次minor gc后進(jìn)入OLD區(qū)對(duì)象平均大小,若不滿足進(jìn)行一次full gc。
優(yōu)點(diǎn):盡量減少做full gc的次數(shù)
7. 對(duì)象內(nèi)存的回收
(1)引用計(jì)算方法:
main() {
obj A = new Obj(); // A計(jì)數(shù)值+1
obj B = new Obj();
A.instance = B; // A計(jì)數(shù)+2
B.instance = A;
}
A = null; // A計(jì)數(shù)值-1
(2) 可達(dá)性分析
將GC roots對(duì)象作為起點(diǎn),從此向下搜索引用對(duì)象,可找到的標(biāo)為非垃圾對(duì)象,其余為垃圾對(duì)象.
根結(jié)點(diǎn)一般為:
- 線程棧本地變量
- 靜態(tài)變量
- 本地方法棧變量
8. 常見引用類型
- 強(qiáng)引用
- 軟引用
- 弱引用
- 虛引用
9. 如何判斷一個(gè)類已無用
回收元空間(方法區(qū))內(nèi)存:3個(gè)條件需均滿足
- 類所有對(duì)象實(shí)例已被回收(該對(duì)象已無引用)
- 加載該類classLoader被回收(一般為自定義類加載器被回收)
- 該類對(duì)應(yīng)java.lang.class 對(duì)象未被引用,無法在其他地方由反射訪問該類
一般被回收對(duì)象:Tomcat, JSP對(duì)象