尚硅谷-JVM-內存和垃圾回收篇(P1~P203)

老師好喜歡講一些題外話,適合上學的人聽,挺有意思的。找工作的話就快進。而且發現JavaGuide上有些東西講的是不準確的,還是要以尚硅谷爲準

本文默認環境:
HotSpot虛擬機

JDK1.8


JVM上篇:內存與垃圾回收篇:

鏈接:https://pan.baidu.com/s/1TcHFE6YEk32Td_zXpZRSrg

提取碼:7jc7

JVM中篇:字節碼與類的加載篇:

鏈接:https://pan.baidu.com/s/1k6TmnpRqXro5DjCMBz0Qgg

提取碼:sdxw

JVM下篇:性能監控與調優篇:

鏈接:https://pan.baidu.com/s/1MZoq_tsNCg2Cx_xIasSJng

提取碼:qrbt

csdn csdn csdn csdn csdn


目錄

🔥1. 內存和垃圾回收篇(P1~P203)

1.1. 基本概念

1.1.1 名詞解釋
1.1.1.1 JVM概念

​ Java虛擬機(默認HotSpot虛擬機,內部提供JIT編譯器):各種語言經過編譯器後只要編譯後的字節碼符合JVM規則,則都能在JVM上運行

image-20220709070821916

1.1.1.2 JVM整體結構

image-20220709072349088

1.1.1.3 Java執行流程
1. Java源碼進行編譯成.class字節碼
2. 字節碼進入Jvm執行
3. 整個流程由OS控制

image-20220709073301212

-- 對字節碼文件進行反編譯
javap -v JavaTest.class
1.1..1.4 JVM生命週期
  1. JVM啓動是由類加載器加載一個初始類完成
  2. JVM執行就是執行一個JAVA程序
  3. JVM正常或異常退出

1.2 類加載子系統

1.2.1 基本概念

​ 作用:把字節碼.class文件加載到內存當中,生成一個大的class實例。所實現的三步驟就是加載、鏈接、初始化

1.2.2 雙親委派機制

​ 原理:當類加載器收到加載一個class類加載請求時,會先遞歸給最頂級父類加載器加載,若找不到就遞歸返回讓子類加載器依次加載尋找(比如定義了一個String類和java核心包中相同的類)

​ 優點:

	1. 避免類重複加載
	2. 保護核心API被隨意修改

​ 沙箱:就是在一個受保護的虛擬環境下的環境,用戶可以任意修改,不會對實際程序產生影響

1.3 運行時數據區

1.3.1 基本架構

JVM啓動後其實就是對應一個運行時環境,5個組件如下

1. 堆、元數據區:線程共享
1. 虛擬機棧、PC、本地方法棧

image-20220709150527101

1.3.2 PC

PC寄存器:用於存下一條指令地址

1.3.3 虛擬機棧
1.3.2.1 基本概念

生命週期和線程一樣,保存方法的局部變量,並參與方法的調用與返回。保存了局部變量表,內部包含基本數據類型和對象引用

1.3.3.2 棧異常

虛擬機棧大小是動態或者固定不變的

  1. StackOverFLowError: 線程申請棧容量超過Java虛擬機棧允許的最大容量
  2. OutOfMemoryEoor:線程拓展時申請不到內內存,或者創建線程申請虛擬機棧申請不到內存,就會報OOM
-Xss256k  設置棧空間大小
1.3.3.3 存儲結構(略)

棧中數據以棧幀格式存在。

1.3.4 本地方法接口/本地方法庫

​ 不屬於運行時數據區,但是供本地方法棧來調用

1.3.4 本地方法棧

​ 虛擬機棧管理Java方法調用,本地方法棧管理本地方法調用


1.3.5 堆(*)
1.3.5.1 基本概念
1. 一個JVM實例只有一個堆內存,在啓動時就創建了堆
2. 堆在物理上不連續,邏輯上連續
3. 堆中可以給不同線程劃分私有的空間:TLAB
4. 對象實例幾乎都在堆上分配(有些在棧上)
/*
 當執行完s1後,s1在棧中就出棧了,s1在堆中指向的實例不會立馬GC,s1所在的方法也保存在方法區中,如下圖所示
*/
String s1 = new SimpleHeap()

image-20220711101607306

//設置堆的最小和最大空間       每啓動一個main程序都會申請一個堆內存
-Xms10m -Xms10m
1.3.5.2 基本結構
  1. 新生代:分爲Eden區和Survivor區。
  2. 老年代
  3. 元空間:(jdk1.7之前叫永久代)
// 輸出3個空間大小情況。程序執行完纔打印
-Xms20m -Xms20m -XX:+PrintGCDetails
1.3.5.3 堆空間參數設置

初始內存大小:物理內存 / 64

最大內存大小:物理內存 / 4

// -Xms和-Xmx一般設置成一樣,避免擴容產生的不必要系統壓力
-Xms 堆空間(新生 + 老年)初始內存大小
-Xmx 對空間最大內存大小

/*
1. 查看空間容量:-XX:+PrintGCDetails
2. jsp查看進程    jstat -gc  進程號 查看容量
3. -XX:+PrintFlagsInitial 查看所有參數默認值
4. -XX:+PrintFlagsFinal 查看所有參數最終值

*/
1.3.5.4 新生代/老年代參數設置
1. 大部分Java對象都是在Eden中new出來,也是在新生代銷燬
1. 新生代和老年代內存比例默認是1: 2
-XX:NewRatio: 設置老年代所佔比例
-XX:Survivorratio: Eden空間和Survivor默認比例8:1:1,但是由於自適應分配策略,可能是6:1:1
1.3.5.5 對象分配
  1. 先在新生代-Eden區分配,滿了進行YGC,存活下來的進入Survivor-from並給age+1
  2. 下次新生代-Eden滿了YGC,Eden存活下來的和Survivor-From進入Survivor-To,然後Survivor-to變成survivor-from,原始survivor-from變成survivor-to,讓age+1。每次GC都會對eden和survivor進行回收。
  3. survovor-from達到閾值15之後會進入老年代。

​ 注意點:

  1. survivor中s0, s1不會進行觸發YGC,只是每次Eden中YGC時會順便把survivor中回收。YGC後複製之後有交換,誰空誰是to
  2. 頻繁在新生代回收,很少在老年代回收,幾乎不在元空間回收
  3. Eden中GC後就空了。Survivor中如果存不下就會放入老年代

image-20220711124440518

1.3.5.6 回收策略

​ 由於GC單獨有回收線程,會暫停用戶線程的運行,所以實際調用就是需要減少GC頻率

1. 部分收集
 	1. 新生代收集:MinorGC/YoungGC
 	2. 老年代收集:MajorGC/OldGC (CMS GC會單獨收集老年代)
 	3. 混合收集:新生代和老年代混合收集(G1 GC特有)
2. 整堆收集:整個java堆和方法區的垃圾回收

Minor GC:

  1. Java對象大多朝生夕滅,所以MinorGC非常頻繁
  2. MinorGC會引發STW(stop the world),暫停用戶線程,等待回收完再恢復執行

Major GC:

1. Major GC速度比MinorGC慢10倍以上,STW時間也更長
1.3.5.7 TLAB

​ TLAB(Thread Local Allocation Buffer):在Eden中爲每一個線程快速分配私有緩存。但所佔空間很小

-- 查看TLAB:默認開啓
jps
jinfo -flag UseTLAB 進程號
  1. 字節碼文件經過類加載子系統
  2. 先由TLAB分配,不行JVM用加鎖機制走Eden那一套內存分配
  3. 分配好後進行對象實例化,對象引用入棧,PC+1

image-20220711153459396

1.3.5.8 逃逸分析--棧上分配對象
  1. 堆可以分配對象
  2. 棧上也可以分配對象:若一個對象實例只在方法內部使用,那麼JIT編譯器在編譯期間經過逃逸分析就能再棧上分配,方法一退出就能回收對象。XXX.getInstance獲取對象實例會發生逃逸
//默認開啓
-XX:+DoEscapeAnalysis
    
    
    //未逃逸,棧上分配
    public static String createStringBuffer(String s1, String s2){
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(s1);
        stringBuffer.append(s2);
        //內部重新new String,方法中堆stringBuffer採用棧上分配,沒有發生逃逸,方法退出就能回收對象實例
        return stringBuffer.toString();
    }    

1.3.5.9 逃逸分析--同步省略

​ 經過逃逸分析,如果一個對象只能從一個線程訪問到,那麼對於對象的同步可以不考慮,提高併發性和性能,叫鎖消除

    /**
     * Author: HuYuQiao
     * Description:
     *  由於每次來都會new個新對象,所以synchronized實際鎖失效了,所以可以同步省略,
     *  免去加鎖流程.
     *
     */
    public static void f(){
        Object o = new Object();
        // 以下JIT編譯階段優化成:System.out.println(o.toString());
        synchronized (o){
            System.out.println(o.toString());
        }
    }
1.3.5.10 逃逸分析--標量替換

類:聚合量 基本數據類型:標量

​ 經過逃逸分析,若一個類的對象實例只在某個方法中使用,就把類替換成各個標量。也是默認開啓

1.3.5.11 逃逸分析--總結
  1. 逃逸分析:是JIT編譯優化時的一種分析方法
  2. 逃逸分析目前並不成熟,但是JVM也是引入了
  3. 靜態變量分配在堆上,其實也可以默認對象都分配在堆中,結合棧上分配講解即可
1.3.6 方法區
1.3.6.1 基本概念
  1. PC沒有異常,也沒有GC
  2. 虛擬機棧、本地方法棧有異常,沒有GC
  3. 堆、元空間,有異常,有GC

元空間使用的是本地內存(電腦內存,而不是JVM內存)。若系統定義過多類,方法區也會報錯OOM:metaspace。

image-20220711175328044

1.3.6.2 方法區/棧/堆--交互關係
/*
         User: 類的信息在方法區
         user: 引用變量或基本數據類型在棧
         new User(): 對象實例在堆
         */
User user = new User();
1.3.6.3 OOM簡單排查思路

​ 先看OOM是內存溢出還是泄漏

1. 內存泄漏:用GC ROOT查看泄漏代碼位置
1. 內存溢出:查看是堆還是方法區溢出,調整參數大小
1.3.6.4 內部結構
  1. 存儲類相關信息
  2. 存儲靜態變量:定義一個空對象,也能直接調用對象的靜態變量,說明靜態變量是存在方法區的
  3. 運行時常量池:類的相關信息,字符串常量池(代碼定義的"字符串")
1.3.6.5 運行時常量池

​ 字節碼文件中存在常量池,類加載子系統把常量池加載到方法區中,就是運行時常量池。所以需要分析常量池

​ 常量池:內部包含了各種符號引用:比如下面存的String, System指向父類引用,"存儲在運行時常量池中"這種字符串常量池符號,在運行時纔會加載其父類

​ 運行時常量池:把常量池中存的符號引用轉成真實地址,String,System這些真實的父類

    public static void main(String[] args) {
        Order order = null;
        System.out.println(order.count + "存儲在運行時常量池中");
    }
1.3.6.6 字符串常量池、靜態變量

堆:

  1. 字符串常量池:就是代碼定義的"字符串",包括一些常量摺疊什麼的(之所以放到堆中,就是因爲實際代碼中定義字符串情況很多,放到堆中便於GC回收)。(區分字符串常量池和運行時常量池,兩者是不同的東西
  2. 靜態變量:static int a這種

方法區:依然保存類的相關信息

(人都傻了,關於字符串常量池、靜態變量這2個東西網上衆說紛紜,感覺不用太較真,先默認在堆中,說不定面試官都不曉得在哪裏)

image-20220715141720769

1.3.7 直接內存
1.3.7.1 基本概念

​ 元空間的具體實現就是直接內存(也叫本地內存,直接內存包含了元空間)

1.3.7.2 IO/NIO

IO NIO(非阻塞IO)
Byte/char Buffer
Stream Channel
1.3.7.3 基本流程

原始流程:CPU(線程) -> 內存(JVM)->(需要進行用戶態和內核態切換) 磁盤

直接內存:CPU->磁盤(減少了用戶態到內核態切換),適合頻繁讀寫磁盤

1. 直接內存回收成本高,不受JVM內存回收管理
1. 也會導致OOM

1.4 對象實例化

1.4.1 創建對象方式
  1. new
  2. Class.newInstance,Constructor.newInstance
  3. clone()
  4. 反序列化獲取
  5. 第三方庫
1.4.2 創建對象步驟
  1. 判讀對象所屬的類是否加載、鏈接、初始化

  2. 爲對象分配內存

    1. 內存規整:指針碰撞
    2. 內存不規整:空閒列表
  3. 處理分配內存時候的併發安全問題

  4. 初始化分配到空間

  5. 設置對象的對象頭

  6. 執行構造器相關的init方法進行初始化

1.4.3 對象內存佈局
  1. 對象頭
    1. 運行時元數據markword(hashcode, 哪個線程持有鎖,鎖次數,GC分代年齡)
    2. 類型指針:對象所屬類的信息
  2. 實例數據:父類繼承、本類的實際字段
  3. 對齊填充:爲了補位填充的無用數據
1.4.4 對象訪問方式
  1. 句柄訪問:棧先訪問到堆中句柄池,然後一個指向實際數據,一個類型指針指向對象所屬類信息

    image-20220718200400295

  2. 直接指針(HotSpot默認):

    image-20220718200430158

1.5 執行引擎

1.5.1 基本概念

​ 執行引擎是將字節碼指令解釋/編譯成平臺上的機器指令。依賴於PC計數器識別一條條指令

1.5.2 編譯與解釋

​ Java也是對代碼進行JIT編譯(快)和解釋執行(慢,但是啓動的時候就可以發揮作用)一起執行。

​ 棧上替換:JIT編譯器把熱點代碼編譯成本地機器指令

image-20220719171939020

1.5.3 垃圾回收
1.5.3.1 基本概念

​ 垃圾:沒有任何指針指向的對象(和基本數據類型無關)

​ 垃圾回收:回收永久代和堆中(頻繁回收新生代,較少老年代,幾乎不動永久代),(類似於理財管理,也提供了對理財管理的監控),垃圾回收又分標記和清除(類執行完成之後可以進行回收,代碼執行的時候內存不足也會觸發)

image-20220724143933436

image-20220724144020149

1.5.3.2 GC--標記
1. 引用計數(Java沒使用):給每個對象分一個引用計數器,沒有引用指向就回收
1. 可達性分析(Java使用):從對象集合GC Roots 從上到下進行引用鏈搜索,不能查到的就是垃圾對象

finalize:對象銷燬前做的調用的操作

1.5.3.3 Jprofile--OOM排查
1. 配置參數:-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
1. Java VisualVM打開XXX.hprof,然後就能查看到相關內存情況
1.5.3.4 GC--清除
1. MS(mark-sweep標記-清除):先利用可達性標記,然後清除,容易產生內存碎片,還會STW用戶線程。**清除也不是真的置Null,而且把對象放入空閒空間,下次新對象使用就能複用這一塊地址**
 	1. ![image-20220725145710703](https://1-1257837791.cos.ap-nanjing.myqcloud.com/202207251457808.png)
2. MC(Mark-Compact標記-整理算法):先標記,然後清除,然後整理排好,避免內存碎片
3. CP(coping複製算法):內存分2半,每次用一般,然後把可達對象複製到另一半中(和survivor2個區一樣 )
4. 分代收集算法(常見):就是分成新生代和老年代,不同對象採用不同的收集算法(Http的session對象、線程就生命週期長。String就生命週期短,內部還是上面MS,MC,CP)
5. 增量收集算法:處理stw下垃圾回收線程和用戶線程的衝突,底層還是MS,CP,MC
1.5.3.5 System.gc

​ System.gc:進行一次full GC,但是不確定是否立刻GC,但是最終會GC

1.5.3.6 內存溢出/泄漏

​ 內存溢出:可用內存不夠,無法申請

​ 內存泄漏:無用內存太多,卻無法回收

		1. 單例的Runtime生命週期很長,分配一個引用對象的話,那麼那個引用對象就很難回收
		1. 和外部的數據庫連接、socket連接,如果不關閉的話就無法回收
1.5.3.7 STW

​ stop the world ,由於GC Root 是不斷變化的,在垃圾回收的時候要保證數據一致性,就需要暫停用戶線程,根據GC Root 進行垃圾回收,這種在GC時候暫停用戶線程就叫STW

1.5.3.8 垃圾回收--併發與並行

​ 垃圾回收-並行:多條垃圾回收線程並行工作,用戶線程STW

​ 垃圾回收-併發:垃圾回收線程和用戶線程併發工作,中間穿插STW

1.5.3.8 安全點和安全區域

​ 在程序執行的時候,對於選定STW時間點很重要,在那種循環、調用方法執行時間長的地方選定STW,就叫安全點。如果程序阻塞了,那麼程序這一段區域內也能進行STW,然後進行GC,這一段區域就叫安全區域

1.5.3.9 強軟弱虛引用(略)--JUC講過
1.5.4 垃圾回收器
1.5.4.1 性能指標

​ 在下面2個指標中找個折中辦法

1. 吞吐量 : 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)
1. 暫停時間
1.5.4.2 基本分類
1. 串行回收器:Serial, Serial Old
1. 並行回收器: ParNew, Paralled Scavenge, Parallel Old
1. 併發回收器:CMS, G1
1.5.4.3 jdk8默認垃圾回收器

​ 默認UseParallelGC(新生代 cp算法 吞吐量優先) + ParallelOldGC(老年代 mc算法) 。 可以在JVM參數中配置相應的垃圾回收器

C:\Users\EDY>jps
14416 GCUseTest
22576 Jps
45240 RemoteMavenServer36
13484 Launcher
14556 RemoteMavenServer36
39372

C:\Users\EDY>jinfo -flag UseParallelGC 14416
-XX:+UseParallelGC

C:\Users\EDY>jinfo -flag UseParallelOldGC 14416
-XX:+UseParallelOldGC
1.5.4.4 垃圾回收器選擇

最小化內存用Serial GC

最大化吞吐量用Paralled GC

最小化GC中斷時長用CMS

image-20220725170428300

1.5.5 GC--回收日誌與導出
--打印GC日誌
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

image-20220725171535919

1.6 String

1.6.1 基本概念
1.6.1.1 不可變

​ 內部是final的char數組,申明的話是在堆的字符串常量池重新創建,然後賦值,且字符串常量池中相同字符串數據唯一

private final char value[]
1.6.1.2 實現原理

​ String的字符串池是採用HashTable(數組+鏈表)實現,因爲有Hash值計算,所以字符串常量池數據不重複。

1.6.1.3 面試考點
1. String s1 = s2 + "":出現了s2這種變量,返回結果就是重新在堆new了個對象 new String()。底層是StringBuilder非線程安全(StringBuilder.toString就是重新new String()),所以併發環境下拼接字符串應該用StringBuffer(final String的話就沒有這種情況,因爲就變成常量了)
1. s1.intern():返回s1常量池中字符串,沒有則重新在常量池創建一個返回

1.7 個人小結

image-20220730132755286

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章