兩個月,我也要進阿里!——(1)JVM長篇講解

最近身邊朋友很多去了美團、dd、阿里、拼多多等大廠,

在這裏插入圖片描述嫉妒使我醜陋

於是小祥給自己下了個目標——兩個月,我要進阿里!

在這裏插入圖片描述

在這裏插入圖片描述

並且還和之前同事互勉 了一把!

在這裏插入圖片描述

不多廢話了,一天一個知識點開始

1.JVM概述

JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。

2.JVM生命週期

在這裏插入圖片描述

生命週期一共分爲加載、連接、初始化、使用、卸載五大階段。

1.加載:類的加載過程主要完成三件事。
(1)通過類的全限定名來獲取定義此類的二進制字節流。
(2)將這個類字節流代表的靜態存儲結構轉爲方法區的運行時數據結構。
(3)在堆中生成一個代表此類的java.lang.Class對象,作爲訪問方法區這些數據結構的入口。這個過程主要是類加載器完成的。

2.連接:這個過程分三個階段(校驗,準備,解析)完成。
首先是校驗,此階段主要校驗class文件包含的信息是否符合jvm的規範。具體的校驗通過對文件格式,元數據,字節碼,符號引用驗證來完成。
然後是準備,此階段爲類變量分配內存,並將其初始化爲默認值。
最後是解析,即把類型中的符號引用轉換成爲直接引用。具體的解析有4種,1.類或接口的解析,2.字段解析,3.類方法解析,4.接口方法解析。完成這3個階段就完成了類的連接。

3.初始化:即執行類的構造器方法的過程。有5種方法可以完成初始化:1.調用new方法,2.使用Class類的newInstance方法(反射機制),3.使用Constructor類的newInstance方法(反射機制),4.使用Clone方法創建對象,5.使用(反)序列化機制創建對象

4.使用:完成類的初始化後,就可以對類進行實例化,在程序中進行使用了

5.卸載:當類被加載,連接和初始化後,它的生命週期就始了,當代表類的class對象不在被引用時,class對象就會結束生命週期,類在方法區內的數據就會被卸載。因此一個類何時結束生命,取決於代表它的class對象何時結束生命。

3.JVM內存結構

                                 JVM運行機制圖

在這裏插入圖片描述
方法區和對是所有線程共享的內存區域
本地方法棧程序計數器運行是線程私有的內存區域。

(1)Java堆(Heap),是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。

(2)方法區(Method Area),方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
程序計數器(Program Counter Register),程序計數器(Program Counter Register)是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的行號指示器

(3)JVM棧(JVM Stacks),與程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

(4)本地方法棧(Native Method Stacks),本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。

對於JVM內存,程序員只需要關注對象去哪兒——堆內存(包括新生代老年代,新生代又分爲Enden區和兩個Survivor區),堆內存用於存放對象,合理分區主要是爲了提高垃圾回收效率,新創建的對象會在Enden區,經歷Minor GC後會到Survivor區,經歷一般15次GC還存活的話會進入老年代。

函數如何調用——棧內存用於運行線程,它們包含了方法裏的臨時數據、堆裏其它對象引用的特定數據。

類去哪兒——方法區用來存儲類型信息(運行時常量和靜態變量)和方法代碼和構造函數代碼,通常也叫永久代,JDK8用元空間代替永久代

JVM資源一般分爲兩種CPU和內存,CPU代表着線程棧,內存代表着堆,而往往生產環境JVM問題一般都是這兩種資源不足導致的,當線程棧使用不當的時候,通常會CPU爆滿,當堆內存使用不當的時候,通常會出現內存溢出。

4.類的加載

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

啓動類加載器:Bootstrap ClassLoader,負責加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫

擴展類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載DK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴展類加載器。

應用程序類加載器:Application ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器

雙親委派模型是一種組織類加載器之間關係的一種規範,他的工作原理是:如果一個類加載器收到了類加載的請求,它不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,這樣層層遞進,最終所有的加載請求都被傳到最頂層的啓動類加載器中,只有當父類加載器無法完成這個加載請求(它的搜索範圍內沒有找到所需的類)時,纔會交給子類加載器去嘗試加載.
在這裏插入圖片描述
如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個java.lang.Object的同名類並放在ClassPath中,多個類加載器都去加載這個類到內存中,系統中將會出現多個不同的Object類,那麼類之間的比較結果及類的唯一性將無法保證,而且如果不使用這種雙親委派模型將會給虛擬機的安全帶來隱患。所以,要讓類對象進行比較有意義,前提是他們要被同一個類加載器加載。

5.Java對象結構

Java對象組成:對象頭、實例數據、對齊填充。

對象頭由兩部分組成,第一部分存儲對象自身的運行時數據:哈希碼、GC分代年齡、鎖標識狀態、線程持有的鎖、偏向線程ID(一般佔32/64 bit)。第二部分是指針類型,指向對象的類元數據類型(即對象代表哪個類)。如果是數組對象,則對象頭中還有一部分用來記錄數組長度。

實例數據用來存儲對象真正的有效信息(包括父類繼承下來的和自己定義的)

對齊填充:JVM要求對象起始地址必須是8字節的整數倍(8字節對齊)

6.Java對象創建過程

1.JVM遇到一條新建對象的指令時首先去檢查這個指令的參數是否能在常量池中定義到一個類的符號引用。然後加載這個類(類加載過程在後邊講)

2.爲對象分配內存。一種辦法“指針碰撞”、一種辦法“空閒列表”,最終常用的辦法“本地線程緩衝分配(TLAB)”

3.將除對象頭外的對象內存空間初始化爲0

4.對對象頭進行必要設置

7.GC算法及垃圾回收器

常見的垃圾回收算法:標記-清除,複製,標記-壓縮,分代收集

常用的垃圾回收集器:Serial收集器,ParNew收集器,Paralle收集器,Paralle Old收集器,Cms收集器,G1收集器

GC是垃圾收集的意思,內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會導致程序或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測對象是否超過作用域從而達到自動回收內存的目的,Java語言沒有提供釋放已分配內存的顯示操作方法。
Java程序員不用擔心內存管理,因爲垃圾收集器會自動進行管理。要請求垃圾收集,可以調用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉顯示的垃圾回收調用。 垃圾回收可以有效的防止內存泄露,有效的使用可以使用的內存。
垃圾回收器通常是作爲一個單獨的低優先級的線程運行,不可預知的情況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收,程序員不能實時的調用垃圾回收器對某個對象或所有對象進行垃圾回收。
在這裏插入圖片描述

下面講解幾個垃圾回收算法

  1. 引用計數算法
    給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都爲0的對象就是不再被使用的,垃圾收集器將回收該對象使用的內存。引用計數算法實現簡單,效率很高,微軟的COM技術、ActionScript、Python等都使用了引用計數算法進行內存管理,但是引用計數算法對於對象之間相互循環引用問題難以解決,因此java並沒有使用引用計數算法。

  2. 根搜索算法:
    通過一系列的名爲“GC Root”的對象作爲起點,從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Root沒有任何引用鏈相連時,則該對象不可達,該對象是不可使用的,垃圾收集器將回收其所佔的內存。

在Java語言裏,可作爲GC Roots對象的包括如下幾種:
(1)System Class,像rt.jar裏面的java.util.*

(2)Thread,開始狀態的線程

(3)虛擬機棧(棧楨中的本地變量表)中的引用的對象

(4)方法區中的類靜態屬性引用的對象

(5)方法區中的常量引用的對象

(6)本地方法棧中JNI的引用的對象

  1. 標記-清除算法
    . 在這裏插入圖片描述
    最基礎的垃圾收集算法,算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成之後統一回收掉所有被標記的對象。
    標記-清除算法的缺點有兩個:
    首先,效率問題,標記和清除效率都不高。
    其次,標記清除之後會產生大量的不連續的內存碎片,空間碎片太多會導致當程序需要爲較大對象分配內存時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

  2. 複製算法: 將可用內存按容量分成大小相等的兩塊,每次只使用其中一塊,當這塊內存使用完了,就將還存活的對象複製到另一塊內存上去,然後把使用過的內存空間一次清理掉。這樣使得每次都是對其中一塊內存進行回收,內存分配時不用考慮內存碎片等複雜情況,只需要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效
    複製算法的缺點顯而易見,可使用的內存降爲原來一半

  3. **標記-清除-整理算法:**標記-整理算法在標記-清除算法基礎上做了改進,標記階段是相同的標記出所有需要回收的對象,在標記完成之後不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,在移動過程中清理掉可回收的對象,這個過程叫做整理。
    標記-整理算法相比標記-清除算法的優點是內存被整理以後不會產生大量不連續內存碎片問題。
    複製算法在對象存活率高的情況下就要執行較多的複製操作,效率將會變低,而在對象存活率高的情況下使用標記-整理算法效率會大大提高。

  4. 分代算法:
    根據對象的存活週期的不同將內存劃分爲幾塊。一般把java堆分爲新生代老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對他進行分配擔保,就必須使用==“標記-整理”算法==進行回收。

相關的JVM參數:
-Xms / -Xmx — 堆的初始大小 / 堆的最大大小
-Xmn — 堆中年輕代的大小
-XX:-DisableExplicitGC — 讓System.gc()不產生任何作用
-XX:+PrintGCDetails — 打印GC的細節
-XX:+PrintGCDateStamps — 打印GC操作的時間戳
-XX:NewSize / XX:MaxNewSize — 設置新生代大小/新生代最大大小
-XX:NewRatio — 可以設置老生代和新生代的比例
-XX:PrintTenuringDistribution — 設置每次新生代GC後輸出倖存者樂園中對象年齡的分佈
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:設置老年代閥值的初始值和最大值
-XX:TargetSurvivorRatio:設置倖存區的目標使用率

8.JVM調優

1、初始化內存最大內存儘量保持一致,避免內存不夠用繼續擴充內存。最大內存不要超過物理內存,例如內存8g,你可以設置最大內存4g/6g但是不能超過8g否則加載類的時候沒有空間會報錯。

2、gc/full gc頻率不要太高、每次gc時間不要太長、根據系統應用來定。

摘錄GC日誌一部分(前部分爲年輕代gc回收;後部分爲full gc回收):
2020-05-22T10:43:18.093+0800: 25.395: 
[GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs] 
2020-05-22T10:43:18.160+0800: 25.462: 
[Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K)
 [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]

通過上面日誌分析得出,PSYoungGen、ParOldGen、PSPermGen屬於Parallel收集器。其中PSYoungGen表示gc回收前後年輕代的內存變化;ParOldGen表示gc回收前後老年代的內存變化;PSPermGen表示gc回收前後永久區的內存變化。young gc 主要是針對年輕代進行內存回收比較頻繁,耗時短;full gc 會對整個堆內存進行回城,耗時長,因此一般儘量減少full gc的次數

常用調優工具:

jps:查看所有的jvm進程,包括進程ID,進程啓動的路徑等等。

jstack:觀察jvm中當前所有線程的運行情況和線程當前狀態。

jstat:利用JVM內建的指令對Java應用程序的資源和性能進行實時的命令行的監控,包括了對進程的classloader,compiler,gc情況;

jmap:監視進程運行中的jvm物理內存的佔用情況,該進程內存內,所有對象的情況,例如產生了哪些對象,對象數量;

jinfo:觀察進程運行環境參數,包括Java System屬性和JVM命令行參數。

9JVM常會問的面試題

1.jvm的初始化步驟?類的加載過程
2.jvm的內存結構?每塊內存分別存的什麼信息
3.雙親委派模型
4.GC算法,如何調優
5.FULL GC的條件

在這裏插入圖片描述

看完了鐵汁麻煩給點個贊👍!

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