前言
文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/bin392328206/six-finger
種一棵樹最好的時間是十年前,其次是現在
Tips
面試指南系列,很多情況下不會去深挖細節,是小六六以被面試者的角色去回顧知識的一種方式,所以我默認大部分的東西,作為面試官的你,肯定是懂的。
https://www.processon.com/view/link/600ed9e9637689349038b0e4
上面的是腦圖地址
叨絮
可能大家覺得有點老生常談了,確實也是。面試題,面試寶典,隨便一搜,根本看不完,也看不過來,那我寫這個的意義又何在呢?其實嘛我寫這個的有以下的目的
- 第一就是通過一個體系的復習,讓自己前面的寫的文章再重新的過一遍,總結升華嘛
- 第二就是通過寫文章幫助大家建立一個復習體系,我會將大部分會問的的知識點以點帶面的形式給大家做一個導論
然后下面是前面的文章匯總
- 2021-Java后端工程師面試指南-(引言)
- 2021-Java后端工程師面試指南-(Java基礎篇)
-
2021-Java后端工程師面試指南-(并發-多線程)
JVM 作為一個Java工程師,必須要掌握和理解的一個點
聊聊什么是JVM
JVM是Java Virtual Machine(Java虛擬機)的縮寫,引入Java語言虛擬機后,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。
什么是類加載器,類加載器有哪些?
實現通過類的全限定名獲取該類的二進制字節流的代碼塊叫做類加載器。
主要有一下四種類加載器:
- 啟動類加載器(Bootstrap ClassLoader)用來加載java核心類庫,無法被java程序直接引用。
- 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。
- 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。
- 用戶自定義類加載器,通過繼承 java.lang.ClassLoader類的方式實現。
說說JVM類的生命周期和加載過程
類的生命周期就包含了加載過程了,我們JVM類的生命周期有以下7個階段
- 加載:
- 通過全類名獲取定義此類的二進制字節流
- 將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構
- 在內存中生成一個代表該類的 Class 對象,作為方法區這些數據的訪問入口
- 驗證:驗證文件格式,字節碼驗證,魔數驗證等
- 準備 準備階段是正式為類變量分配內存并設置類變量初始值的階段,如果是基本數據類型,就會給他們設置默認值
- 解析:解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程
- 初始化:首先明確一點的就是,必須存在以下的行為,才會進行類的初始化
- 當jvm執行new指令時會初始化類。即當程序創建一個類的實例對象。
- 當jvm執行getstatic指令時會初始化類。即程序訪問類的靜態變量(不是靜態常量,常量會被加載到運行時常量池)。
- 當jvm執行putstatic指令時會初始化類。即程序給類的靜態變量賦值。
- 當jvm執行invokestatic指令時會初始化類。即程序調用類的靜態方法。
- 使用 java.lang.reflect 包的方法對類進行反射調用時如Class.forname("..."),newInstance()等等。 ,如果類沒初始化,需要觸發其初始化。
- 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化。
- 當虛擬機啟動時,用戶需要定義一個要執行的主類 (包含 main 方法的那個類),虛擬機會先初始化這個類。
- 使用:就是我們正常使用了
- 卸載: 卸載類即該類的Class對象被GC。卸載類需要滿足3個要求:
- 該類的所有的實例對象都已被GC,也就是說堆不存在該類的實例對象。
- 該類沒有在其他任何地方被引用
- 該類的類加載器的實例已被GC
說說類加載器雙親委派模型機制?說說它的好處
每一個類都有一個對應它的類加載器。系統中的 ClassLoder 在協同工作的時候會默認使用 雙親委派模型 。即在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載。加載的時候,首先會把該請求委派該父類加載器的 loadClass() 處理,因此所有的請求最終都應該傳送到頂層的啟動類加載器 BootstrapClassLoader 中。當父類加載器無法處理時,才由自己來處理。當父類加載器為null時,會使用啟動類加載器 BootstrapClassLoader 作為父類加載器。
小六六總結一句話總結就是 類加載總是向上檢查,向下加載。
雙親委派模型保證了Java程序的穩定運行,可以避免類的重復加載(JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,比如我們編寫一個稱為 java.lang.Object 類的話,那么程序運行的時候,系統就會出現多個不同的 Object 類。
說說為啥要打破雙親委派模型,如何打破?
JDBC之所以要破壞雙親委派模式是因為,JDBC的核心在rt.jar中由啟動類加載器加載,而其實現則在各廠商實現的的jar包中,根據類加載機制,若A類調用B類,則B類由A類的加載器加載,也就是說啟動類加載器要加載jar包下的類,我們都知道這是不可能的,啟動類加載器負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class,那么JDBC是如何加載這些Driver實現類的?
通過Thread.currentThread().getContextClassLoader()得到線程上下文加載器來加載Driver實現類。
還有就是我們可以自定義的加載器繼承ClassLoad然后修改的loadClass和find classde 方法,從而可以打破雙親的委派機制
聊聊JVM內存分哪幾個區,每個區的作用是什么?
- 方法區:
- 有時候也成為永久代(元空間),在該區內很少發生垃圾回收,但是并不代表不發生GC,在這里進行的GC主要是對方法區里的常量池和對類 型的卸載
- 方法區主要用來存儲已被虛擬機加載的類的信息、常量、靜態變量和即時編譯器編譯后的代碼等數據。
- 該區域是被線程共享的。
- 方法區里有一個運行時常量池,用于存放靜態編譯產生的字面量和符號引用。該常量池具有動態性,也就是說常量并不一定是編 譯時確定,運行時生成的常量也會存在這個常量池中。
- 虛擬機棧:
- 虛擬機棧也就是我們平常所稱的棧內存,它為java方法服務,每個方法在執行的時候都會創建一個棧幀,用于存儲局部變量表、操 作數棧、動態鏈接和方法出口等信息。
- 虛擬機棧是線程私有的,它的生命周期與線程相同。
- 局部變量表里存儲的是基本數據類型、returnAddress類型(指向一條字節碼指令的地址)和對象引用,這個對象引用有可能是指 向對象起始地址的一個指針,也有可能是代表對象的句柄或者與對象相關聯的位置。局部變量所需的內存空間在編譯器間確定
- 操作數棧的作用主要用來存儲運算結果以及運算的操作數,它不同于局部變量表通過索引來訪問,而是壓棧和出棧的方式
- 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接.動態鏈接就是將常量池中的符號引用在運行期轉化為直接引用。
- 本地方法棧
- 本地方法棧和虛擬機棧類似,只不過本地方法棧為Native方法服務。
- 堆
- Java堆是所有線程所共享的一塊內存,在虛擬機啟動時創建,幾乎所有的對象實例都在這里創建,因此該區域經常發生垃圾回收操作 。
- 程序計數器
- 內存空間小,字節碼解釋器工作時通過改變這個計數值可以選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理和線程恢復等功能都需要依賴這個計數器完成。該內存區域是唯一一個java虛擬機規范沒有規定任何OOM情況的區域。
說說如何判斷一個對象是否存活?(或者GC對象的判定方法)
- 虛擬機棧棧幀中引用的變量
- 本地方法棧中引用的變量
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
你知道Java中垃圾收集的方法有哪些嗎
- 標記-清除算法:該算法分為“標記”和“清除”階段:首先標記出所有不需要回收的對象,在標記完成后統一回收掉所有沒有被標記的對象,但是會產生大量的空間碎片。
- 復制算法:為了解決效率問題,“復制”收集算法出現了。它可以將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。
- 標記-整理算法:根據老年代的特點提出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內存。
- 分代收集算法:當前虛擬機的垃圾收集都采用分代收集算法,這種算法沒有什么新的思想,只是根據對象存活周期的不同將內存分為幾塊。一般將 java 堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
你有沒有遇到過OutOfMemory問題?你是怎么來處理這個問題的?處理 過程中有哪些收獲?
- 集合類中有對對象的引用,使用后未清空,GC不能進行回收;
- 代碼中存在循環產生過多的重復對象;
- 啟動參數堆內存值小。
- 頻發的創建超大對象
JDK 1.8之后Perm Space有哪些變動? MetaSpace??默認是?限的么? 還是你們會通過什么?式來指定??
JDK 1.8后用元空間替代了 Perm Space;字符串常量存放到堆內存中。
MetaSpace大小默認沒有限制,一般根據系統內存的大小。JVM會動態改變此值。
-XX:MetaspaceSize:分配給類元數據空間(以字節計)的初始大小(Oracle邏輯存儲上的初始高水位,the initial high-water-mark)。此值為估計值,MetaspaceSize的值設置的過大會延長垃圾回收時間。垃圾回收過后,引起下一次垃圾回收的類元數據空間的大小可能會變大。-XX:MaxMetaspaceSize:分配給類元數據空間的最大值,超過此值就會觸發Full GC,此值默認沒有限制,但應取決于系統內存的大小。JVM會動態地改變此值。
說說發生mior GC的條件是什么
我們知道新生代一般分為三個區 eden s1 和 s2,而我們每次創建的對象在新生代里面,他只會分配在eden和其中一個s,當他們都滿了,就會發生一次mior gc ,對象進入老年代。
說說進入老年代的條件吧
- 第一個就是我們上面說的 新生代滿了 eden和一個s滿了,對象經過mior gc之后進入老年代
- 大對象直接進入老年代:如果老年代剩余的連續內存空間大于之前Minor GC晉升老年代對象的平均大小的話,就進行Minor GC,如果小于的話就直接進行Full GC。對于parNew 是XX:PretenureSizeThreshold 設置大對象大小
- 長期存活的對象將進入老年代 XX:MaxTenuringThreshold cms默認是6次,
- Hotspot 遍歷所有對象時,按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了 survivor 區的一半時,取這個年齡和 MaxTenuringThreshold 中更小的一個值,作為新的晉升年齡閾值,也就是說當一個s的一半以上的對象都是這個,那么他們就會進入老年代了。
說說jdk1.8默認的垃圾回收器,你們的線上環境用的是哪個垃圾回收器呢?
1.8默認的是 UseParallelGC,ParallelGC 默認的是 Parallel Scavenge(新生代)+ Parallel Old(老年代)
小六六自己負責的項目是我配置的,用的parNew+CMS;
為啥要用CMS呢?是這樣的,CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。
那你說說CMS收集的過程
- 初始標記: 暫停所有的其他線程,并記錄下直接與 root 相連的對象,速度很快 ;
- 并發標記: 同時開啟 GC 和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構并不能保證包含當前所有的可達對象。因為用戶線程可能會不斷的更新引用域,所以 GC 線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發生引用更新的地方。
- 重新標記: 重新標記階段就是為了修正并發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發標記階段時間短
- 并發清除: 開啟用戶線程,同時 GC 線程開始對未標記的區域做清掃。
他只有第一個 和第三個階段需要stw
它的缺點: 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。
其實我建議呢?如果自己的內存夠大還是用G1吧,只要達到8G的內存,我們建議使用G1,而且jdk9開始已經沒有cms了
那你聊聊G1吧
G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征.
- 并行與并發:G1 能充分利用 CPU、多核環境下的硬件優勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過并發的方式讓 java 程序繼續執行。
- 分代收集:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但是還是保留了分代的概念。
- 空間整合:與 CMS 的“標記--清理”算法不同,G1 從整體來看是基于“標記整理”算法實現的收集器;從局部上來看是基于“復制”算法實現的。
- 可預測的停頓:這是 G1 相對于 CMS 的另一個大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內。
G1 收集器在后臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來) 。這種使用 Region 劃分內存空間以及有優先級的區域回收方式,保證了 G1 收集器在有限時間內可以盡可能高的收集效率(把內存化整為零)。
其實,我們只要控制了GC的每次回收時間,對于用戶來說感知就不會那么大了。
那你說說G1 和cms+parNew的好處體現在哪?
其實呢 G1 和cms 很大程度上很相似,怎么說,就是當我們回收垃圾的時候,都是要經歷 初始標記,并發標記,重新標記,這些階段。但是有幾個區別
- 第一個就是假設我們這個應用很多,我們把新生代的內存設置的很大,對不對,這樣垃圾回收的時間就會很小,但是如果剛好,有一個用戶剛好請求的時候,它剛好再gc,那你想想這個GC的時間會不會很長,那對于用戶來說是不是體驗差呢?所以說大內存其實并不是那么適合cms
- G1就不同,他并不需要說一定要等到,達到內存的多少才開始回收垃圾,他可以設置我們垃圾回收的時間,來判斷什么時候來回收,這樣對于大部分用戶來說,相當于平均了gc的時候,那么體驗上會好很多。
說說你一般用來排查問題的工具唄
jps jmap jstat MAT arthas等,用的比較多,還有就是最后打印出我們的gc日志,通過gc日志去分析gc問題
總結一下你的JVM調優的一些心得
這個是小六六自己的一些見解,不一定對哈,其實大部分我們去分析gc日志,然后去調優并不一定說要去修改JVM的參數,很多時候是我們自己代碼的問題,所以我們要把自己的代碼先去排查,如果代碼沒有問題了,那么就是JVM的參數,我們有以下原則
- 第一個就是讓那些應該在新生代被回收的對象,盡量不要進入到老年代,讓他們再新生代被回收
- 讓那些長期存活的對象,盡快的進入到老年代
- 如果內存夠大,盡量使用G1
- 寫代碼的使用 如果你使用完一個對象,最好把那個對象的引用置空
結束
JVM寫完了,可能也不全部,但是呢,這些問題你熟悉的話基本上問題不大了,下一章就MySql吧
日常求贊
好了各位,以上就是這篇文章的全部內容了,能看到這里的人呀,都是真粉。
創作不易,各位的支持和認可,就是我創作的最大動力,我們下篇文章見
微信 搜 "六脈神劍的程序人生" 回復888 有我找的許多的資料送給大家