JVM虛擬機器只關注位元組碼檔案是否符合規範。
JVM位元組碼、 多語言混合編程、JVM作爲執行平臺,進行跨語言平臺操作
虛擬機器:系統虛擬機器,程式虛擬機器(Java虛擬機器執行Java位元組碼,自動記憶體管理,垃圾回收)
Java原始碼–前段編譯器–位元組碼檔案–類載入器–位元組碼效驗器–翻譯位元組碼,JIT編譯器
高階語言–彙編–機器指令
Java指令是基於基於棧的指令架構集,基於暫存器的指令架構集
jvm生命週期 Java虛擬機器的啓動通過類載入器建立一個初始類來完成 執行:Java程式的的執行其實是一個Java虛擬機器的進程,虛擬機器的退出
類載入器子系統 class檔案標識 CAFE BABY , classloader只負責載入,執行由execution決定 載入的類資訊存放在方法區
反編譯 javap -v 檔名.class
類的載入過程 達到能基本的描述三個步驟
載入loading:生成一個代表這個類的class物件,作爲方法區這個類的各種數據的存取入口
鏈接linking: 驗證 CAFE BABY,保證載入類的正確性; 準備 類變數分配記憶體,預設零值 除非final static,靜態變數的預設初始化 解析
初始化 :執行類構造器方法clinit 的過程,此方法無需定義,javac編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中語句合併而來(有靜態就有這個檔案)靜態變數的前初始化和靜態程式碼塊的執行
init()是類的本身構造器
虛擬機器保證一個類的clinit()方法在多執行緒下被同步加鎖
載入器 引導類載入器bootstarp, 自定義載入器: 系統類載入器 擴充套件類載入器, 派生於抽象類classloader的類載入器爲自定義載入器
自定義類 系統載入器; 核心類由引導類載入器載入(c/c++)
自定義載入器:隔離載入類(類的衝突), 修改類的載入方式, 擴充套件載入源, 防止原始碼泄露(類的加密)
實現步驟: 繼承classloader ,重寫findclas 或者直接繼承URLClassLoder類
classloader 抽象類 除了bootstarp,其餘都繼承
類載入器的獲取 getclassloder 執行緒獲取 系統類載入器的getparent
雙親委派機制 機製:Java虛擬機器對class檔案採取按需載入,載入類的class檔案時採用雙親委派機制 機製,即把請求交由父類別處理,一種任務委派模式 優點:避免類的重複載入,保護程式安全,防止核心API被惡意篡改
類載入依次向上委託 由包名判斷載入器
反向委派機制 機製 jdbc的jar包載入
沙箱安全機制 機製:對Java核心原始碼的保護
JVM中表示兩個class物件是否爲同一個類的必要條件:類的完整類名包括包名一致,載入類的classloader一致
如果一個型別是由使用者類載入器載入的,那麼JVM會將這個類載入器的一個參照作爲型別資訊的一部分儲存在方法區中。
當解析一個型別到另一個型別的參照的時候,JVM需要保證這兩個型別的類載入器是相同的。
Java程式對類的使用方式分爲:主動使用和被動使用。 類的初始化,反射,呼叫類的靜態方法
類的載入-> 驗證 -> 準備 -> 解析 -> 初始化 這幾個階段完成後,就會用到執行引擎對我們的類進行使用,同時執行引擎將會使用到我們執行時數據區
記憶體是非常重要的系統資源,是硬碟和CPU的中間倉庫及橋樑
每個執行緒:獨立包括程式計數器、棧、本地棧。 執行緒間共用:堆、方法區
JVM允許一個應用有多個執行緒並行的執行。 在Hotspot JVM裡,每個執行緒都與操作系統的本地執行緒直接對映。
守護執行緒和非守護執行緒(JVM停止)
後臺系統執行緒:虛擬機器執行緒、GC執行緒、週期任務執行緒
程式計數暫存器、PC暫存器: 儲存下一條指令地址 一塊很小的記憶體空間 執行速度最快的儲存區域 每個執行緒都有它自己的程式計數器,是執行緒私有的,生命週期與執行緒的生命週期保持一致。 任何時間一個執行緒都只有一個方法在執行,也就是所謂的當前方法 唯一一個在Java虛擬機器規範中沒有規定任何outotMemoryError情況的區域。
堆 方法區 GC 堆 方法區 棧OOM
指令地址(偏移地址)–PC暫存器
使用PC暫存器儲存位元組碼指令地址有什麼用:CPU需要不停的切換各個執行緒,這時候切換回來以後,就得知道接着從哪開始繼續執行。
PC暫存器爲什麼被設定爲私有:準確地記錄各個執行緒正在執行的當前位元組碼指令地址,最好的辦法自然是爲每一個執行緒都分配一個PC暫存器,這樣一來各個執行緒之間便可以進行獨立計算,從而不會出現相互幹擾的情況。
CPU時間片 :CPU分配給各個程式的時間,宏觀上多個應用程式同時執行。微觀上引入時間片,每個程式輪流執行。
並行 vs 序列 併發
虛擬機器棧
Java的指令都是根據棧來設計的。不同平臺CPU架構不同,所以不能設計爲基於暫存器的。 優點是跨平臺,指令集小,編譯器容易實現,缺點是效能下降,實現同樣的功能需要更多的指令。
棧是執行時的單位,而堆是儲存的單位
每個執行緒在建立時都會建立一個虛擬機器棧,其內部儲存一個個的棧幀,對應着一次次的Java方法呼叫 執行緒私有 生命週期和執行緒一致,主管Java程式的執行,它儲存方法的區域性變數(八 種基本數據型別,物件的參照地址)、部分結果,並參與方法的呼叫和返回。
區域性變數 成員變數 基本數據型別變數 VS 參照型別變數(類、陣列、介面)
JVM直接對Java棧的操作只有兩個:入棧出棧
對於棧來說不存在垃圾回收問題(棧存在溢位的情況)
棧中可能出現的異常:Java 虛擬機器規範允許Java棧的大小是動態的或者是固定不變的。
固定大小的Java虛擬機器棧 StackoverflowError 異常
Java虛擬機器棧可以動態擴充套件 無法申請到足夠的記憶體,outofMemoryError 異常
使用參數 -Xss選項來設定執行緒的最大棧空間 預設11420
棧的儲存單位:每個執行緒都有自己的棧,棧中的數據都是以棧幀的格式存在。執行緒上正在執行的每個方法都各自對應一個棧幀,棧幀是一個記憶體區塊,是一個數據集,維繫着方法執行過程中的各種數據資訊。
OOP(物件導向)的基本概念:類和物件
類中基本結構:field(屬性、欄位、域)、method
執行引擎執行的所有位元組碼指令只針對當前棧幀進行操作。
不同線程中所包含的棧幀是不允許存在相互參照的,即不可能在一個棧幀之中參照另外一個執行緒的棧幀。
Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另外一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出
棧幀的內部結構:區域性變數表,運算元棧,動態鏈接,方法返回地址,附加資訊
並行每個執行緒下的棧都是私有的,因此每個執行緒都有自己各自的棧,並且每個棧裏面都有很多棧幀,棧幀的大小主要由區域性變數表 和 運算元棧決定的
區域性變數表
定義爲一個數字陣列,主要用於儲存方法參數和定義在方法體內的區域性變數這些數據型別包括各類基本數據型別、物件參照(reference),以及returnAddress型別。
區域性變數表是建立線上程的棧上,是執行緒的私有數據,因此不存在數據安全問題,方法執行期間是不會改變區域性變數表的大小的,方法巢狀呼叫的次數由棧的大小決定(棧幀的大小)。maximum local variables
關於Slot的理解 最基本的儲存單元是Slot(變數槽)區域性變數表中存放編譯期可知的各種基本數據型別(8種),參照型別(reference),returnAddress型別的變數。
32位元以內的型別只佔用一個slot(包括returnAddress型別),64位元的型別(1ong和double)佔用兩個slot。
JVM會爲區域性變數表中的每一個Slot都分配一個存取索引,通過這個索引即可成功存取到區域性變數表中指定的區域性變數值
如果需要存取區域性變數表中一個64bit的區域性變數值時,只需要使用前一個索引即可。
如果當前幀是由構造方法或者實體方法建立的,那麼該物件參照this將會存放在index爲0的s1ot處,其餘的參數按照參數表順序繼續排列。
靜態方法不能使用this,應爲this變數不存在於當前方法的區域性變數表中
Slot的重複利用 棧幀中的區域性變數表中的槽位是可以重用的
變數的分類: 按數據型別分:基本數據型別、參照數據型別;
按類中宣告的位置分:成員變數(類變數,範例變數)、區域性變數
類變數:linking的paper階段,給類變數預設賦值,init階段給類變數顯示賦值即靜態程式碼塊
範例變數:隨着物件建立,會在堆空間中分配範例變數空間,並進行預設賦值
區域性變數:在使用前必須進行顯式賦值,不然編譯不通過。
在棧幀中,與效能調優關係最爲密切的部分就是前面提到的區域性變數表。在方法執行時,虛擬機器使用區域性變數表完成方法的傳遞。
區域性變數表中的變數也是重要的垃圾回收根節點,只要被區域性變數表中直接或間接參照的物件都不會被回收。
運算元棧
棧:使用陣列或者鏈表來實現
運算元棧,主要用於儲存計算過程的中間結果,同時作爲計算過程中變數臨時的儲存空間。每一個運算元棧都會擁有一個明確的棧深度用於儲存數值,其所需的最大深度在編譯期就定義好了
運算元棧並非採用存取索引的方式來進行數據存取的,而是隻能通過標準的入棧和出棧操作來完成一次數據存取
Java虛擬機器的執行引擎是基於棧的執行引擎,其中的棧指的就是運算元棧。
使用javap 命令反編譯class檔案: javap -v 類名.class
棧頂快取技術:基於棧式架構的虛擬機器所使用的零地址指令更加緊湊 記憶體讀/寫次數多 將棧頂元素全部快取在物理CPU的暫存器中,以此降低對記憶體的讀/寫次數,提升執行引擎的執行效率。
動態鏈接:每一個棧幀內部都包含一個指向執行時常數池中該棧幀所屬方法的參照
在Java原始檔被編譯到位元組碼檔案中時,所有的變數和方法參照都作爲符號參照(symbolic Reference)儲存在class檔案的常數池裏。
動態鏈接的作用就是爲了將這些符號參照轉換爲呼叫方法的直接參照。
爲什麼需要執行時常數池:在不同的方法,都可能呼叫常數或者方法,所以只需要儲存一份即可,節省了空間
常數池的作用:就是爲了提供一些符號和常數,便於指令的識別
方法呼叫:解析與分配 將符號參照轉換爲呼叫方法的直接參照與方法的系結機制 機製相關
方法返回地址:存放呼叫該方法的pc暫存器的值
方法正常退出時,呼叫者的pc計數器的值作爲返回地址,即呼叫該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會儲存這部分資訊。
一些附加資訊:對程式偵錯提供支援的資訊
本地方法介面:Native Methodt是一個Java呼叫非Java程式碼的接囗
本地方法:native修飾 有方法體,不是Java實現
Java外面的環境互動,與操作系統的互動,Sun’s Java
本地方法棧
Java虛擬機器棧於管理Java方法的呼叫,而本地方法棧用於管理本地方法的呼叫。也是執行緒私有的,本地方法是使用C語言實現的。
當某個執行緒呼叫一個本地方法時,它就進入了一個全新的並且不再受虛擬機器限制的世界。它和虛擬機器擁有同樣的許可權。
在Hotspot JVM中,直接將本地方法棧和虛擬機器棧合二爲一。
本地方法可以通過本地方法介面來存取虛擬機器內部的執行時數據區。
堆
堆針對一個JVM進程來說是唯一的,也就是一個進程只有一個JVM,但是進程包含多個執行緒,他們是共用同一堆空間的。
Java堆區在JVM啓動的時候即被建立,其空間大小也就確定了。是JVM管理的最大一塊記憶體空間。堆記憶體的大小是可以調節的
Java VisualVM檢視堆空間的內容,通過 jdk bin提供的外掛
堆可以處於物理上不連續的記憶體空間中,但在邏輯上它應該被視爲連續的
所有的執行緒共用Java堆,在這裏還可以劃分執行緒私有的緩衝區TLAB
幾乎所有的物件範例以及陣列都應當在執行時分配在堆上,還有一些物件是在棧上分配的
在方法結束後,堆中的物件不會馬上被移除,僅僅在垃圾收集的時候纔會被移除。
觸發了GC的時候,纔會進行回收,堆中物件馬上被回收GC頻率高,那麼使用者執行緒就會收到影響,因爲有stop the word
堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域。
堆記憶體細分:Java 8及之後堆記憶體邏輯上分爲三部分:新生區+養老區+元空間
設定堆記憶體大小與OOM:通常會將-Xms和-Xmx兩個參數設定相同的值,其目的是爲了能夠在ava垃圾回收機制 機製清理完堆區後不需要重新分隔計算堆區的大小,從而提高效能。
如何檢視堆記憶體的記憶體分配情況:cmd:jps -> jstat -gc 進程id ide : -XX:+PrintGCDetails
Java堆區進一步細分的話,可以劃分爲年輕代(YoungGen)和老年代(oldGen)1 : 2
其中年輕代又可以劃分爲Eden空間、Survivor0空間和Survivor1空間(有時也叫做from區、to區) 8:1:1 -xx:SurvivorRatio=8 預設爲8但實際操作值爲6
幾乎所有的Java物件都是在Eden區被new出來的。絕大部分的Java物件的銷燬都在新生代進行了。
在Eden區滿了的時候,纔會觸發MinorGC,而倖存者區滿了後,不會觸發MinorGC操作
如果Survivor區滿了後,將會觸發一些特殊的規則,也就是可能直接晉升老年代
針對倖存者s0,s1:複製之後又交換,誰空誰是to
垃圾回收:新生代頻繁,老年區很少,元空間不動
常用的調優工具:Visual VM ,Jprofiler
Minor GC,MajorGC、Full GC 我們需要儘量的避免垃圾回收,因爲在垃圾回收的過程中,容易出現STW的問題 主要調優MajorGC、Full GC
JVM在進行GC時,並非每次都對上面三個記憶體區域(新生代,老年代,方法區)一起回收的
GC按照回收區域又分爲兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(FullGC)
新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集
老年代收集(MajorGC/o1dGC):只是老年代的圾收集。
目前,只有CMSGC會有單獨收集老年代的行爲。很多時候Major GC會和FullGC混淆使用,需要具體分辨是老年代回收還是整堆回收。
混合收集(MixedGC):收集整個新生代以及部分老年代的垃圾收集。目前,只有G1 GC會有這種行爲
整堆收集(FullGC):收集整個java堆和方法區的垃圾收集。
Minor GC:Eden代滿,Survivor滿不會引發GC
Major GC:在老年代空間不足時,會先嚐試觸發MinorGc。如果之後空間還不足,則觸發Major GC,Major GC的速度一般會比MinorGc慢1e倍以上,STW的時間更長,如果Major GC後,記憶體還不足,就報OOM了
Full GC:五種 System.gc()老年代空間不足,方法區空間不足
GC 舉例:設定JVM啓動參數, -Xms10m -Xmx10m -XX:+PrintGCDetails
觸發OOM的時候,一定是進行了一次Full GC,因爲只有在老年代空間不足時候,纔會爆出OOM異常
堆空間分代思想: 分代的唯一理由就是優化GC效能
記憶體分配策略 : 如果物件在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到survivor空間中,並將物件年齡設爲1。物件在survivor區中每熬過一次MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(預設爲15歲,其實每個JVM、每個GC都有所不同)時,就會被晉升到老年代
針對不同年齡段的物件分配原則: 優先分配到Eden 大物件直接分配到老年代 長期存活的物件分配到老年代 動態物件年齡判斷 空間分配擔保
經過Minor GC後,所有的物件都存活,因爲Survivor比較小,所以就需要將Survivor無法容納的物件,存放到老年代中。
爲物件分配記憶體:TLAB: 在堆中劃分出一塊區域,爲每個執行緒所獨佔 爲每個執行緒單獨分配了一個緩衝區
在併發環境下從堆區中劃分記憶體空間是執行緒不安全的
TLAB : 從記憶體模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM爲每個執行緒分配了一個私有快取區域,它包含在Eden空間內。 -Xx:UseTLAB」設定是否開啓TLAB空間 預設開啓。一旦物件在TLAB空間分配記憶體失敗時,JVM就會嘗試着通過使用加鎖機制 機製確保數據操作的原子性,從而直接在Eden空間中分配記憶體。
堆空間的參數設定
-XX:+PrintFlagsInitial:檢視所有的參數的預設初始值
-XX:+PrintFlagsFinal:檢視所有的參數的最終值(可能會存在修改,不再是初始值)
-Xms:初始堆空間記憶體(預設爲實體記憶體的1/64)
-Xmx:最大堆空間記憶體(預設爲實體記憶體的1/4)
-Xmn:設定新生代的大小。(初始值及最大值)
-XX:NewRatio:設定新生代與老年代在堆結構的佔比
-XX:SurvivorRatio:設定新生代中Eden和S0/S1空間的比例
-XX:MaxTenuringThreshold:設定新生代垃圾的最大年齡
-XX:+PrintGCDetails:輸出詳細的GC處理日誌
列印gc簡要資訊:①-Xx:+PrintGC ② - verbose:gc
-XX:HandlePromotionFalilure:是否設定空間分配擔保
在發生Minor GC之前,虛擬機器會檢查老年代最大可用的連續空間是否大於新生代所有物件的總空間。I
如果大於,則此次Minor GC是安全的
如果小於,則虛擬機器會檢視-xx:HandlePromotionFailure設定值是否允擔保失敗。
如果HandlePromotionFailure=true,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的物件的平均大小。
如果大於,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;
如果小於,則改爲進行一次FullGC。
如果HandlePromotionFailure=false,則改爲進行一次Ful1 Gc。
堆不是分配物件的唯一選擇 : 逃逸分析 經過逃逸分後發現,一個物件並沒有逃逸出方法的話,那麼就可能被優化成棧上分配
GCIH(GC invisible heap)技術實現off-heap,將生命週期較長的Java物件從heap中移至heap外,並且GC不能管理GCIH內部的Java物件,以此達到降低GC的回收頻率和提升GC的回收效率的目的
當一個物件在方法中被定義後,物件只在方法內部使用,則認爲沒有發生逃逸。
當一個物件在方法中被定義後,它被外部方法所參照,則認爲發生逃逸。例如作爲呼叫參數傳遞到其他地方中。
如何快速的判斷是否發生了逃逸分析,大家就看new的物件實體是否在方法外被呼叫。
使用逃逸分析,編譯器可以對程式碼做如下優化:
棧上分配: 花費的時間快速減少,同時不會發生GC操作
同步省略 :執行緒同步的代價是相當高的,同步的後果是降低併發性和效能,如果一個物件被發現只有一個執行緒被存取到,那麼對於這個物件的操作可以不考慮同步。這個取消同步的過程就叫同步省略,也叫鎖消除。
分離物件和標量替換 : 有的物件可能不需要作爲一個連續的記憶體結構存在也可以被存取到,那麼物件的部分(或全部)可以不儲存在記憶體,而是儲存在CPU暫存器中。
標量(scalar)是指一個無法再分解成更小的數據的數據。Java中的原始數據型別就是標量。
相對的,那些還可以分解的數據叫做聚合量(Aggregate),Java中的物件就是聚合量,因爲他可以分解成其他聚合量和標量。
如果經過逃逸分析,發現一個物件不會被外界存取的話,即時編譯器就會把這個物件拆解成若幹個其中包含的若幹個成員變數來代替。這個過程就是標量替換
參數-server:啓動Server模式,因爲在server模式下,纔可以啓用逃逸分析。
小結
年輕代是物件的誕生、成長、消亡的區域,一個物件在這裏產生、應用,最後被垃圾回收器收集、結束生命。
老年代放置長生命週期的物件,通常都是從survivor區域篩選拷貝過來的Java物件。當然,也有特殊情況,我們知道普通的物件會被分配在TLAB上;如果物件較大,JVM會試圖直接分配在Eden其他位置上;如果物件太大,完全無法在新生代找到足夠長的連續空閒空間,JVM就會直接分配到老年代。當GC只發生在年輕代中,回收年輕代物件的行爲被稱爲MinorGc。
當GC發生在老年代時則被稱爲MajorGc或者FullGC。一般的,MinorGc的發生頻率要比MajorGC高很多,即老年代中垃圾回收發生的頻率將大大低於年輕代。
方法區
方法區看作是一塊獨立於Java堆的記憶體空間。
ThreadLocal:如何保證多個執行緒在併發環境下的安全性?典型應用就是數據庫連線管理,以及對談管理
方法區主要存放的是 Class,而堆中主要存放的是 範例化的物件
方法區(Method Area)與Java堆一樣,是各個執行緒共用的記憶體區域。
方法區在JVM啓動的時候被建立,並且它的實際的實體記憶體空間中和Java堆區一樣都可以是不連續的。
方法區的大小,跟堆空間一樣,可以選擇固定大小或者可延伸。
方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器同樣會拋出記憶體溢位錯誤:java.lang.OutofMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace; 載入大量的第三方的jar包,Tomcat部署的工程過多(30~50個),大量動態的生成反射類
關閉JVM就會釋放這個區域的記憶體。
元空間與永久代最大的區別在於:元空間不在虛擬機器設定的記憶體中,而是使用本地記憶體
設定方法區大小與OOM :元數據區大小可以使用參數 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定
如何解決這些OOM: 分清楚到底是出現了記憶體漏失(Memory Leak)還是記憶體溢位(Memory Overflow)
記憶體漏失就是 有大量的參照指向某些物件,但是這些物件以後不會使用了,但是因爲它們還和GC ROOT有關聯,所以導致以後這些物件也不會被回收,這就是記憶體漏失的問題,如果是記憶體漏失,可進一步通過工具檢視泄漏物件到GC Roots的參照鏈。於是就能找到泄漏物件是通過怎樣的路徑與GCRoots相關聯並導致垃圾收集器無法自動回收它們的。掌握了泄漏物件的型別資訊,以及GCRoots參照鏈的資訊,就可以比較準確地定位出泄漏程式碼的位置。
如果不存在記憶體漏失,換句話說就是記憶體中的物件確實都還必須存活着,那就應當檢查虛擬機器的堆參數(-Xmx與-Xms),與機器實體記憶體對比看是否還可以調大,從程式碼上檢查是否存在某些物件生命週期過長、持有狀態時間過長的情況,嘗試減少程式執行期的記憶體消耗
方法區的內部結構
用於儲存已被虛擬機器載入的型別資訊、常數、靜態變數、即時編譯器編譯後的程式碼快取等。
型別資訊(類class、介面interface、列舉enum、註解annotation):
這個型別的完整有效名稱(全名=包名.類名)
這個型別直接父類別的完整有效名(對於interface或是java.lang.object,都沒有父類別)
這個型別的修飾符(public,abstract,final的某個子集)
這個型別直接介面的一個有序列表
域資訊 :
JVM必須在方法區中儲存型別的所有域的相關資訊以及域的宣告順序。
域的相關資訊包括:域名稱、域型別、域修飾符(public,private,protected,static,final,volatile,transient的某個子集)
方法(Method)資訊:
JVM必須儲存所有方法的以下資訊,同域資訊一樣包括宣告順序:
方法名稱
方法的返回型別(或void)
方法參數的數量和型別(按順序)
方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
方法的位元組碼(bytecodes)、運算元棧、區域性變數表及大小(abstract和native方法除外)
異常表(abstract和native方法除外)
non-final的類變數 :
靜態變數和類關聯在一起,隨着類的載入而載入,他們成爲類數據在邏輯上的一部分
類變數被類的所有範例共用,即使沒有類範例時,你也可以存取它
全域性常數:全域性常數就是使用 static final 進行修飾,被宣告爲final的類變數的處理方法則不同,每個全域性常數在編譯的時候就會被分配了。
執行時常數池 VS 常數池
一個有效的位元組碼檔案中除了包含類的版本資訊、欄位、方法以及介面等描述符資訊外,還包含一項資訊就是常數池表(Constant Pool Table),包括各種字面量和對型別、域和方法的符號參照
常數池可以看做是一張表,虛擬機器指令根據這張常數表找到要執行的類名、方法名、參數型別、字面量等型別
執行時常數池:執行時常數池(Runtime Constant Pool)是方法區的一部分,常數池表(Constant Pool Table)是Class檔案的一部分在類載入後存放到方法區的執行時常數池中。
執行時常數池,相對於Class檔案常數池的另一重要特徵是:具備動態性。
只有Hotspot纔有永久代,BEA JRockit、IBMJ9等來說,是不存在永久代的概唸的
JDK1.6及以前 有永久代,靜態變數儲存在永久代上
JDK1.7 有永久代,但已經逐步 「去永久代」,字串常數池,靜態變數移除,儲存在堆中
JDK1.8 :無永久代,型別資訊,欄位,方法,常數儲存在本地記憶體的元空間,但字串常數池、靜態變數仍然在堆中。
爲什麼永久代要被元空間替代:爲永久代設定空間大小是很難確定的。對永久代進行調優是很困難的。
方法區的垃圾收集主要回收兩部分內容:常數池中廢棄的常數和不在使用的型別
StringTable爲什麼要調整位置:
jdk7中將StringTable放到了堆空間中。因爲永久代的回收效率很低,在full gc的時候纔會觸發。而ful1gc是老年代的空間不足、永久代不足時纔會觸發。
這就導致stringTable回收效率不高。而我們開發中會有大量的字串被建立,回收效率低,導致永久代記憶體不足。放到堆裡,能及時回收記憶體。
靜態變數存放在那裏:靜態參照對應的物件實體始終都存在堆空間
方法區的垃圾回收:
方法區的垃圾收集主要回收兩部分內容:常數池中廢棄的常數和不再使用的型別。
類的解除安裝比較費勁
物件建立方式:
new:最常見的方式、單例類中呼叫getInstance的靜態類方法,XXXFactory的靜態方法
Class的newInstance方法:在JDK9裏面被標記爲過時的方法,因爲只能呼叫空參構造器
Constructor的newInstance(XXX):反射的方式,可以呼叫空參的,或者帶參的構造器
使用clone():不呼叫任何的構造器,要求當前的類需要實現Cloneable介面中的clone介面
使用序列化:序列化一般用於Socket的網路傳輸
第三方庫 Objenesis
建立物件的步驟:判斷物件對應的類是否載入、鏈接、初始化;爲物件分配記憶體;處理併發問題;初始化分配到的記憶體,所有屬性設定預設值,保證物件範例欄位在不賦值可以直接使用;設定物件的物件頭;執行init方法進行初始化顯示初始化、程式碼塊中的初始化、構造器初始化
物件範例化的過程:
載入類元資訊
爲物件分配記憶體
處理併發問題
屬性的預設初始化(零值初始化)
設定物件頭資訊
屬性的顯示初始化、程式碼塊中初始化、構造器中初始化
物件記憶體佈局:
物件頭:執行時元數據(雜湊值 GC分代年齡 鎖狀態標誌)和 型別指針(指向的其實是方法區中存放的類元資訊)範例數據;
物件的存取定位:JVM是如何通過棧幀中的物件參照存取到其內部的物件範例:控制代碼存取(控制代碼池)、直接指針(HotSpot採用
直接記憶體 Direct Memory 元空間使用 來源於NIO,通過存在堆中的DirectByteBuffer操作Native記憶體
非直接快取區和快取區 原來採用BIO的架構,我們需要從使用者態切換成內核態 NIO的方式使用了快取區的概念,實體記憶體對映檔案,存取直接記憶體的速度會優於Java堆。即讀寫效能高。
存在的問題:也可能導致outofMemoryError異常 分配回收成本較高 不受JVM記憶體回收管理
執行引擎:執行引擎屬於JVM的下層,裏面包括 直譯器、及時編譯器、垃圾回收器
虛擬機器的執行引擎則是由軟體自行實現的,能夠執行那些不被硬體直接支援的指令集格式
JVM的主要任務是負責裝載位元組碼到其內部,但位元組碼並不能夠直接執行在操作系統之上,因爲位元組碼指令並非等價於本地機器指令,它內部包含的僅僅只是一些能夠被JVM所識別的位元組碼指令、符號表,以及其他輔助資訊。
想要讓一個Java程式執行起來,執行引擎(Execution Engine)的任務就是將位元組碼指令解釋/編譯爲對應平臺上的本地機器指令纔可以
執行引擎的工作流程
執行引擎在執行的過程中究竟需要執行什麼樣的位元組碼指令完全依賴於PC暫存器。
每當執行完一項指令操作後,PC暫存器就會更新下一條需要被執行的指令地址。
當然方法在執行的過程中,執行引擎有可能會通過儲存在區域性變數表中的物件參照準確定位到儲存在Java堆區中的物件範例資訊,以及通過物件頭中的元數據指針定位到目標物件的型別資訊。
直譯器:當Java虛擬機器啓動時會根據預定義的規範對位元組碼採用逐行解釋的方式執行,將每條位元組碼檔案中的內容「翻譯」爲對應平臺的本地機器指令執行。
JIT編譯器:就是虛擬機器將原始碼直接編譯成和本地機器平臺相關的機器語言
Java是半編譯半直譯語言:既可以編譯也可以解釋
機器碼:二進制編碼方式表示的指令
指令:把機器碼中特定的0和1序列,簡化成對應的指令(一般爲英文簡寫,如mov,inc等
指令集
當程式啓動後,直譯器可以馬上發揮作用,省去編譯的時間,立即執行。
當虛擬機器啓動的時候,直譯器可以首先發揮作用,而不必等待即時編譯器全部編譯完成再執行,這樣可以省去許多不必要的編譯時間。並且隨着程式執行時間的推移,即時編譯器逐漸發揮作用,根據熱點探測功能,將有價值的位元組碼編譯爲本地機器指令,以換取更高的程式執行效率。
String的基本特性:
string宣告爲final的,不可被繼承
String實現了Serializable介面:表示字串是支援序列化的。實現了Comparable介面:表示string可以比較大小
string在jdk8及以前內部定義了final char[] value用於儲存字串數據。JDK9時改爲byte[]
String再也不用char[] 來儲存了,改成了byte [] 加上編碼標記,節約了一些空間
字串常數池是不會儲存相同內容的字串的 在JDK8中,StringTable可以設定的最小值爲1009,太多就會造成Hash衝突嚴重
Java 7 String的記憶體分配:字串常數池的位置調整到Java堆內
爲什麼StringTable從永久代調整到堆中:永久代的預設比較小、永久代垃圾回收頻率低
String的基本操作:Java語言規範裡要求完全相同的字串字面量,應該包含同樣的Unicode字元序列(包含同一份碼點序列的常數),並且必須是指向同一個String類範例。
字串拼接操作:常數與常數的拼接結果在常數池,原理是編譯期優化,變數拼接的原理是StringBuilder
如果拼接符號的前後出現了變數,則相當於在堆空間中new String(),具體的內容爲拼接的結果
而呼叫intern方法,則會判斷字串常數池中是否存在字串值,如果存在則返回常數池中的值,否者就在常數池中建立
s1 + s2的執行細節
StringBuilder s = new StringBuilder();
s.append(s1);
s.append(s2);
s.toString(); -> 類似於new String(「ab」);
左右兩邊如果是變數的話,就是需要new StringBuilder進行拼接,但是如果使用的是final修飾,則是從常數池中獲取。所以說拼接符號左右兩邊都是字串常數或常數參照 則仍然使用編譯器優化。也就是說被final修飾的變數,將會變成常數,類和方法將不能被繼承、
public static void test4() {
final String s1 = 「a」;
final String s2 = 「b」;
String s3 = 「ab」;
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
intern()的使用:intern是一個native方法
new String(「ab」)會建立幾個物件:兩個物件,一個物件是:new關鍵字在堆空間中建立,另一個物件:字串常數池中的物件,看位元組碼ldc就知道是2個物件
new String(「a」) + new String(「b」) 會建立幾個物件:建立了6個物件,
物件1:new StringBuilder();
物件2:new String(「a」);
物件3:常數池的 a;
物件4:new String(「b」);
物件5:常數池的 b
物件6:toString中會建立一個 new String(「ab」) 呼叫toString方法,不會在常數池中生成ab
總結string的intern()的使用:
JDK1.6中,將這個字串物件嘗試放入串池。
如果串池中有,則並不會放入。返回已有的串池中的物件的地址
如果沒有,會把此物件複製一份,放入串池,並返回串池中的物件地址
JDK1.7起,將這個字串物件嘗試放入串池。
如果串池中有,則並不會放入。返回已有的串池中的物件的地址
如果沒有,則會把物件的參照地址複製一份,放入串池,並返回串池中的參照地址
intern():節省空間
G1中的String去重操作:這裏說的重複,指的是在堆中的數據,而不是常數池中的,因爲常數池中的本身就不會重複
許多大規模的Java應用的瓶頸在於記憶體,Java堆中存活的數據集合差不多25%是string物件,jdk8中不再用char[]轉換爲byte[]陣列
垃圾回收概述
垃圾是指在執行程式中沒有任何指針指向的物件,這個物件就是需要被回收的垃圾。
不及時對記憶體中的垃圾進行清理,那麼,這些垃圾物件所佔的記憶體空間會一直保留到應用程式的結束,被保留的空間無法被其它物件使用,甚至可能導致記憶體溢位。
記憶體泄露:本身不被使用,但無法進行垃圾回收
爲什麼需要GC:不進行垃圾回收,記憶體遲早都會被消耗完,JVM將整理出的記憶體分配給新的物件,應付的業務越來越龐大、複雜,使用者越來越多,沒有GC就不能保證應用程式的正常進行
在早期的C/C++時代,垃圾回收基本上是手工進行的。開發人員可以使用new關鍵字進行記憶體申請,並使用delete關鍵字進行記憶體釋放
Java堆是垃圾收集器的工作重點
頻繁收集Young區,較少收集Old區,基本不收集Perm區(元空間)
垃圾回收相關演算法:
判斷物件存活一般有兩種方式:參照計數演算法和可達性分析演算法。
參照計數演算法:對每個物件儲存一個整型的參照計數器屬性。用於記錄物件被參照的情況,參照計數器有一個嚴重的問題,即無法處理回圈參照的情況。這是一條致命缺陷,導致在Java的垃圾回收器中沒有使用這類演算法。
可達性分析演算法(根搜尋演算法、追蹤性垃圾收集): 可以有效地解決在參照計數演算法中回圈參照的問題,防止記憶體漏失的發生。
可達性分析演算法是以根物件集合(GCRoots)爲起始點,按照從上至下的方式搜尋被根物件集合所連線的目標物件是否可達。參照鏈
GC Roots: 虛擬機器棧中參照的物件, 本地方法棧內JNI(通常說的本地方法)參照的物件方法區中類靜態屬性參照的物件,方法區中常數參照的物件, 所有被同步鎖synchronized持有的物件
總結一句話就是,除了堆空間外的一些結構,比如 虛擬機器棧、本地方法棧、方法區、字串常數池 等地方對堆空間進行參照的,都可以作爲GC Roots進行可達性分析
除了這些固定的GC Roots集合以外,根據使用者所選用的垃圾收集器以及當前回收的記憶體區域不同,還可以有其他物件「臨時性」地加入,共同構成完整GC Roots集合。比如:分代收集和區域性回收(PartialGC)。
由於Root採用棧方式存放變數和指針,所以如果一個指針,它儲存了堆記憶體裏面的物件,但是自己又不存放在堆記憶體裏面,那它就是一個Root。
物件的finalization機制 機製: 當垃圾回收器發現沒有參照指向一個物件,即:垃圾回收此物件之前,總會先呼叫這個物件的finalize()方法。
永遠不要主動呼叫某個物件的finalize()方法I應該交給垃圾回收機制 機製呼叫。
在finalize()時可能會導致物件復活。
finalize()方法的執行時間是沒有保障的,它完全由Gc執行緒決定,極端情況下,若不發生GC,則finalize()方法將沒有執行機會。,因爲優先順序比較低,即使主動呼叫該方法,也不會因此就直接進行回收
一個糟糕的finalize()會嚴重影響Gc的效能。
由於finalize()方法的存在,虛擬機器中的物件一般處於三種可能的狀態。可觸及,可復活的,不可觸及的
可觸及的:從根節點開始,可以到達這個物件。
可復活的:物件的所有參照都被釋放,但是物件有可能在finalize()中復活。
不可觸及的:物件的finalize()被呼叫,並且沒有復活,那麼就會進入不可觸及狀態。不可觸及的物件不可能被複活,因爲finalize()只會被呼叫一次
具體過程:
判定一個物件objA是否可回收,至少要經歷兩次標記過程:
如果物件objA到GC Roots沒有參照鏈,則進行第一次標記。
進行篩選,判斷此物件是否有必要執行finalize()方法,如果物件objA沒有重寫finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,則虛擬機器視爲「沒有必要執行」,objA被判定爲不可觸及的。如果物件objA重寫了finalize()方法,且還未執行過,那麼objA會被插入到F-Queue佇列中,由一個虛擬機器自動建立的、低優先順序的Finalizer執行緒觸發其finalize()方法執行。finalize()方法是物件逃脫死亡的最後機會,稍後GC會對F-Queue佇列中的物件進行第二次標記。如果objA在finalize()方法中與參照鏈上的任何一個物件建立了聯繫,那麼在第二次標記時,objA會被移出「即將回收」集合。之後,物件會再次出現沒有參照存在的情況。在這個情況下,finalize方法不會被再次呼叫,物件會直接變成不可觸及的狀態,也就是說,一個物件的finalize方法只會被呼叫一次。
清除階段:
當成功區分出記憶體中存活物件和死亡物件後,GC接下來的任務就是執行垃圾回收,釋放掉無用物件所佔用的記憶體空間,以便有足夠的可用記憶體空間爲新物件分配記憶體。目前在JVM中比較常見的三種垃圾收集演算法是: 標記一清除演算法(Mark-Sweep),複製演算法(copying),標記-壓縮演算法(Mark-Compact)
標記一清除演算法: 執行過程:當堆中的有效記憶體空間(available memory)被耗盡的時候,就會停止整個程式(也被稱爲stop the world),然後進行兩項工作,第一項則是標記,第二項則是清除。
標記:Collector從參照根節點開始遍歷,標記所有被參照的物件。一般是在物件的Header中記錄爲可達物件。標記的是參照的物件,不是垃圾!!
清除:Collector對堆記憶體從頭到尾進行線性的遍歷,如果發現某個物件在其Header中沒有標記爲可達物件,則將其回收
什麼是清除:所謂的清除並不是真的置空,而是把需要清除的物件地址儲存在空閒的地址列表裏。下次有新物件需要載入時,判斷垃圾的位置空間是否夠,如果夠,就存放覆蓋原有的地址。
關於空閒列表是在爲物件分配記憶體的時候 提過:
如果記憶體規整,採用指針碰撞的方式進行記憶體分配
如果記憶體不規整,虛擬機器需要維護一個列表,空閒列表分配
複製演算法:沒有標記和清除過程,實現簡單,執行高效,複製過去以後保證空間的連續性,不會出現「碎片」問題。
此演算法的缺點也是很明顯的,就是需要兩倍的記憶體空間。
對於G1這種分拆成爲大量region的GC,複製而不是移動,意味着GC需要維護region之間物件參照關係,不管是記憶體佔用或者時間開銷也不小
在新生代,對常規應用的垃圾回收,一次通常可以回收70% - 99% 的記憶體空間。回收性價比很高。所以現在的商業虛擬機器都是用這種收集演算法回收新生代。
標記-整理演算法:第一階段和標記清除演算法一樣,從根節點開始標記所有被參照物件,第二階段將所有的存活物件壓縮到記憶體的一端,按順序排放。之後,清理邊界外所有的空間。
標記-壓縮演算法的最終效果等同於標記-清除演算法執行完成後,再進行一次記憶體碎片整理,因此,也可以把它稱爲標記-清除-壓縮(Mark-Sweep-Compact)演算法。標記-清除演算法是一種非移動式的回收演算法,標記-壓縮是移動式的
分代收集演算法:目前幾乎所有的GC都採用分代收集演算法執行垃圾回收的
分代收集演算法,是基於這樣一個事實:不同的物件的生命週期是不一樣的。因此,不同生命週期的物件可以採取不同的收集方式,以便提高回收效率。一般是把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點使用不同的回收演算法,以提高垃圾回收的效率。
年輕代:複製演算法的回收整理,速度是最快的。複製演算法的效率只和當前存活物件大小有關,因此很適用於年輕代的回收。而複製演算法記憶體利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。
老年代:一般是由標記-清除或者是標記-清除與標記-整理的混合實現,以HotSpot中的CMS回收器爲例,CMS是基於Mark-Sweep實現的,對於物件的回收效率很高
Mark階段的開銷與存活物件的數量成正比。
Sweep階段的開銷與所管理區域的大小成正相關。
compact階段的開銷與存活物件的數據成正比。
增量收集演算法:在stop the World狀態下,應用程式所有的執行緒都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程式會被掛起很久,將嚴重影響使用者體驗或者系統的穩定性。
增量收集演算法的基礎仍是傳統的標記-清除和複製演算法。增量收集演算法通過對執行緒間衝突的妥善處理,允許垃圾收集執行緒以分階段的方式完成標記、清理或複製工作,執行緒切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
分割區演算法:爲了更好地控制GC產生的停頓時間,將一塊大的記憶體區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若幹個小區間,而不是整個堆空間,從而減少一次GC所產生的停頓。
垃圾回收相關概念:
通過system.gc()者Runtime.getRuntime().gc() 的呼叫,會顯式觸發FullGC,同時對老年代和新生代進行回收
system.gc() )呼叫附帶一個免責宣告,無法保證對垃圾收集器的呼叫。(不能確保立即生效)
記憶體溢位:javadoc中對outofMemoryError的解釋是,沒有空閒記憶體,並且垃圾收集器也無法提供更多記憶體。
沒有空閒記憶體的情況:說明Java虛擬機器的堆記憶體不夠。原因有二:
Java虛擬機器的堆記憶體設定不夠:可能存在記憶體漏失問題;也很有可能就是堆的大小不合理
程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被參照),老版本的oracle JDK,因爲永久代的大小是有限的,並且JVM對永久代垃圾回收(如,常數池回收、解除安裝不再需要的型別)非常不積極,所以當我們不斷新增新型別的時候,永久代出現OutOfMemoryError也非常多見,尤其是在執行時存在大量動態型別生成的場合;類似intern字串快取佔用太多空間,也會導致OOM問題。對應的異常資訊,會標記出來和永久代相關:「java.lang.OutOfMemoryError:PermGen space"。隨着元數據區的引入,方法區記憶體已經不再那麼窘迫,所以相應的ooM有所改觀,出現ooM,異常資訊則變成了:「java.lang.OutofMemoryError:Metaspace"。直接記憶體不足,也會導致OOM。
在拋出OutofMemoryError之前,通常垃圾收集器會被觸發,盡其所能去清理出空間,也不是在任何情況下垃圾收集器都會被觸發的,JVM可以判斷出垃圾收集並不能解決這個問題,所以直接拋出OutofMemoryError。
記憶體漏失: 嚴格來說,只有物件不會再被程式用到了,但是GC又不能回收他們的情況,才叫記憶體漏失。
寬泛意義上的「記憶體漏失」: 生命週期變很長但無需長生命週期的物件,方法內變數設定爲成員變數或者static,web程式中物件儲存至對談級別本質無需
舉例
單例模式:單例的生命週期和應用程式是一樣長的,所以單例程式中,如果持有對外部物件的參照的話,那麼這個外部物件是不能被回收的,則會導致記憶體漏失的產生。
一些提供close的資源未關閉導致記憶體漏失:數據庫連線(dataSourse.getConnection() ),網路連線(socket)和io連線必須手動close,否則是不能被回收的
Stop The World:指的是GC事件發生過程中,會產生應用程式的停頓。停頓產生時整個應用程式執行緒都會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱爲STW。可達性分析演算法中列舉根節點(GC Roots)會導致所有Java執行執行緒停頓。開發中不要用system.gc() 會導致stop-the-world的發生。
垃圾回收的並行與併發
併發:併發不是真正意義上的「同時進行」,只是CPU把一個時間段劃分成幾個時間片段(時間區間),然後在這幾個時間區間之間來回切換,由於CPU處理的速度非常快,只要時間間隔處理得當,即可讓使用者感覺是多個應用程式同時在進行。
並行:當系統有一個以上CPU時,當一個CPU執行一個進程時,另一個CPU可以執行另一個進程,兩個進程互不搶佔CPU資源,可以同時進行,我們稱之爲並行(Paralle1)
併發和並行對比
併發,指的是多個事情,在同一時間段內同時發生了。
並行,指的是多個事情,在同一時間點上同時發生了。
併發的多個任務之間是互相搶佔資源的。並行的多個任務之間是不互相搶佔資源的。
只有在多CPU或者一個CPU多核的情況中,纔會發生並行。否則,看似同時發生的事情,其實都是併發執行的。
併發和並行,在談論垃圾收集器的上下文語境中,它們可以解釋如下:
並行(Paralle1):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍處於等待狀態。
併發(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行的,可能會交替執行),垃圾回收執行緒在執行時不會停頓使用者程式的執行。使用者程式在繼續執行,而垃圾收集程式執行緒執行於另一個CPU上;
安全點:程式執行時並非在所有地方都能停頓下來開始GC,只有在特定的位置才能 纔能停頓下來開始GC,這些位置稱爲「安全點
如何在cc發生時,檢查所有執行緒都跑到最近的安全點停頓下來呢:主動式中斷:設定一箇中斷標誌,各個執行緒執行到Safe Point的時候主動輪詢這個標誌,如果中斷標誌爲真,則將自己進行中斷掛起。(有輪詢的機制 機製)
安全區域:執行緒處於sleep-狀態或Blocked 狀態,這時候執行緒無法響應JVM的中斷請求,把Safe Region看做是被擴充套件了的Safepoint。
參照:強參照、軟參照、弱參照、虛參照。這4種參照強度依次逐漸減弱
強參照:object obj=new Object 只要強參照關係還存在,垃圾收集器就永遠不會回收掉被參照的物件
強參照的物件是可觸及的,垃圾收集器就永遠不會回收掉被參照的物件。強參照是造成Java記憶體漏失的主要原因之一。虛擬機器寧願拋出OOM異常,也不會回收強參照所指向物件。
軟參照:沒有足夠的記憶體,纔會拋出記憶體流出異常
記憶體不足即回收,軟參照通常用來實現記憶體敏感的快取。比如:快取記憶體就有用到軟參照。如果還有空閒記憶體,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同時,不會耗盡記憶體。
弱參照:當垃圾收集器工作時,無論記憶體空間是否足夠,都會回收掉被弱參照關聯的物件。 發現即回收,WeakHashMap用來儲存圖片資訊,可以在記憶體不足的時候,及時回收,避免了OOM
虛參照:爲一個物件設定虛參照關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知(物件回收的跟蹤)
第一次嘗試獲取虛參照的值,發現無法獲取的,這是因爲虛參照是無法直接獲取物件的值,然後進行第一次gc,因爲會呼叫finalize方法,將物件復活了,所以物件沒有被回收,但是呼叫第二次gc操作的時候,因爲finalize方法只能執行一次,所以就觸發了GC操作,將物件回收了,同時將會觸發第二個操作就是 將回收的值存入到參照佇列中。
終端子參照:用於實現物件的finalize() 方法
三級快取:記憶體–本地–網路
Java不同版本新特性
語法層面:Lambda表達式、switch、自動拆箱裝箱、enum
API層面:Stream API、新的日期時間、Optional、String、集合框架
底層優化:JVM優化、GC的變化、元空間、靜態域、字串常數池位置變化
垃圾收集器分類
按執行緒數分(垃圾回收執行緒數),可以分爲序列垃圾回收器和並行垃圾回收器。
按照工作模式分,可以分爲併發式垃圾回收器和獨佔式垃圾回收器。
按碎片處理方式分,可分爲壓縮武垃圾回收器(指針碰撞)和非壓縮式垃圾回收器(空閒列表)。
按工作的記憶體區間分,又可分爲年輕代垃圾回收器和老年代垃圾回收器。
評估GC的效能指標:
吞吐量:執行使用者程式碼的時間佔總執行時間的比例(總執行時間 = 程式的執行時間 + 記憶體回收的時間)
垃圾收集開銷:吞吐量的補數,垃圾收集所用時間與總執行時間的比例。
暫停時間:執行垃圾收集時,程式的工作執行緒被暫停的時間。
收集頻率:相對於應用程式的執行,收集操作發生的頻率。
記憶體佔用:Java堆區所佔的記憶體大小。
快速:一個物件從誕生到被回收所經歷的時間。
主要抓住兩點:吞吐量、暫停時間
吞吐量=執行使用者程式碼時間 /(執行使用者程式碼時間+垃圾收集時間)
暫停時間」是指一個時間段內應用程式執行緒暫停,讓Gc執行緒執行的狀態
吞吐量vs暫停時間:現在標準:在最大吞吐量優先的情況下,降低停頓時間
7種經典的垃圾收集器
序列回收器:Serial、Serial old
並行回收器:ParNew、Parallel Scavenge、Parallel old
併發回收器:CMS、G1
Parallel回收器:吞吐量優先
和ParNew收集器不同,ParallelScavenge收集器的目標則是達到一個可控制的吞吐量(Throughput),它也被稱爲吞吐量優先的垃圾收集器。
自適應調節策略也是Paralle1 Scavenge與ParNew一個重要區別。
高吞吐量則可以高效率地利用CPU時間,儘快完成程式的運算任務,主要適合在後台運算而不需要太多互動的任務。執行批次處理、訂單處理、工資支付、科學計算的應用程式。
JDK8:parallel回收器
JDK9:G1 GC
CMS回收器:低延遲 這款收集器是HotSpot虛擬機器中第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集執行緒與使用者執行緒同時工作。
CMS的垃圾收集演算法採用標記-清除演算法,並且也會"stop-the-world"
CMS整個過程比之前的收集器要複雜,整個過程分爲4個主要階段,即初始標記階段、併發標記階段、重新標記階段和併發清除階段。
初始標記(Initial-Mark)階段:在這個階段中,程式中所有的工作執行緒都將會因爲「stop-the-world」機制 機製而出現短暫的暫停,這個階段的主要任務僅僅只是標記出GCRoots能直接關聯到的物件。一旦標記完成之後就會恢復之前被暫停的所有應用執行緒。由於直接關聯物件比較小,所以這裏的速度非常快。
併發標記(Concurrent-Mark)階段:從Gc Roots的直接關聯物件開始遍歷整個物件圖的過程,這個過程耗時較長但是不需要停頓使用者執行緒,可以與垃圾收集執行緒一起併發執行。
重新標記(Remark)階段:由於在併發標記階段中,程式的工作執行緒會和垃圾收集執行緒同時執行或者交叉執行,因此爲了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短。
併發清除(Concurrent-Sweep)階段:此階段清理刪除掉標記階段判斷的已經死亡的物件,釋放記憶體空間。由於不需要移動存活物件,所以這個階段也是可以與使用者執行緒同時併發的
CMS收集器的垃圾收集演算法採用的是標記清除演算法
CMS爲什麼不使用標記整理演算法?
答案其實很簡答,因爲當併發清除的時候,用Compact整理記憶體的話,原來的使用者執行緒使用的記憶體還怎麼用呢?要保證使用者執行緒能繼續執行,前提的它執行的資源不受影響嘛。Mark Compact更適合「stop the world」 這種場景下使用
優點:併發收集、低延遲
缺點:會產生記憶體碎片,導致併發清除後,使用者執行緒可用的空間不足。在無法分配大物件的情況下,不得不提前觸發FullGC。
CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致使用者停頓,但是會因爲佔用了一部分執行緒而導致應用程式變慢,總吞吐量會降低。
CMS收集器無法處理浮動垃圾。併發標記階段產生新的垃圾物件
如果你想要最小化地使用記憶體和並行開銷,請選Serial GC;
如果你想要最大化應用程式的吞吐量,請選Parallel GC;
如果你想要最小化GC的中斷或停頓時間,請選CMs GC。
G1回收器:區域化分代式
官方給G1設定的目標是在延遲可控的情況下獲得儘可能高的吞吐量,所以才擔當起「全功能收集器」的重任與期望。
業務越來越龐大、複雜,使用者越來越多,沒有GC就不能保證應用程式正常進行,而經常造成STW的GC又跟不上實際的需求,所以纔會不斷地嘗試對GC進行優化
G1 GC有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後台維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。這種方式的側重點在於回收垃圾最大量的區間(Region),G1一個名字:垃圾優先(Garbage First)
G1(Garbage-First)是一款面向伺服器端應用的垃圾收集器,主要針對配備多核CPU及大容量記憶體的機器,以極高概率滿足GC停頓時間的同時,還兼具高吞吐量的效能特徵。
G1垃圾收集器的優點
並行與併發
並行性:G1在回收期間,可以有多個GC執行緒同時工作,有效利用多覈計算能力。此時使用者執行緒STW
併發性:G1擁有與應用程式交替執行的能力,部分工作可以和應用程式同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程式的情況
分代收集
從分代上看,G1依然屬於分代型垃圾回收器,它會區分年輕代和老年代,年輕代依然有Eden區和Survivor區。但從堆的結構上看,它不要求整個Eden區、年輕代或者老年代都是連續的,也不再堅持固定大小和固定數量。
將堆空間分爲若幹個區域(Region),這些區域中包含了邏輯上的年輕代和老年代。
和之前的各類回收器不同,它同時兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代;
空間整合
CMS:「標記-清除」演算法、記憶體碎片、若幹次Gc後進行一次碎片整理
G1將記憶體劃分爲一個個的region。記憶體的回收是以region作爲基本單位的。Region之間是複製演算法,但整體上實際可看作是標記-壓縮(Mark-Compact)演算法,兩種演算法都可以避免記憶體碎片。這種特性有利於程式長時間執行,分配大物件時不會因爲無法找到連續記憶體空間而提前觸發下一次GC。尤其是當Java堆非常大的時候,G1的優勢更加明顯。
可預測的停頓時間模型(即:軟實時soft real-time)
G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
G1垃圾收集器的缺點
相較於CMS,G1還不具備全方位、壓倒性優勢。比如在使用者程式執行過程中,G1無論是爲了垃圾收集產生的記憶體佔用(Footprint)還是程式執行時的額外執行負載(overload)都要比CMS要高。
從經驗上來說,在小記憶體應用上CMS的表現大概率會優於G1,而G1在大記憶體應用上則發揮其優勢。平衡點在6-8GB之間。
G1收集器的常見操作步驟:
G1的設計原則就是簡化JVM效能調優,開發人員只需要簡單的三步即可完成調優:
第一步:開啓G1垃圾收集器,第二步:設定堆的最大記憶體,第三步:設定最大的停頓時間
G1收集器的適用場景
面向伺服器端應用,針對具有大記憶體、多處理器的機器。(在普通大小的堆裡表現並不驚喜)最主要的應用是需要低GC延遲,並具有大堆的應用程式提供解決方案
分割區Region:化整爲零
一個region有可能屬於Eden,Survivor或者old/Tenured記憶體區域。但是一個region只可能屬於一個角色。G1垃圾收集器還增加了一種新的記憶體區域,叫做Humongous記憶體區域, 主要用於儲存大物件
G1GC的垃圾回收過程主要包括如下三個環節:年輕代GC(Young GC)、老年代併發標記過程(Concurrent Marking)、混合回收(Mixed GC)
單執行緒、獨佔式、高強度的Fu11GC還是繼續存在的
young gc->young gc+concurrent mark->Mixed GC順序,進行垃圾回收。
應用程式分配記憶體,當年輕代的Eden區用盡時開始年輕代回收過程;G1的年輕代收集階段是一個並行的獨佔式收集器。在年輕代回收期,G1GC暫停所有應用程式執行緒,啓動多執行緒執行年輕代回收。然後從年輕代區間移動存活物件到Survivor區間或者老年區間,也有可能是兩個區間都會涉及。
當堆記憶體使用達到一定值(預設45%)時,開始老年代併發標記過程。
標記完成馬上開始混合回收過程。對於一個混合回收期,G1GC從老年區間移動存活物件到空閒區間,這些空閒區間也就成爲了老年代的一部分。和年輕代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整個老年代被回收,一次只需要掃描/回收一小部分老年代的Region就可以了。同時,這個老年代Region是和年輕代一起被回收的。
Remembered Set(記憶集):一個物件被不同區域參照的問題
G1回收過程-年輕代GC:YGC時,首先G1停止應用程式的執行(stop-The-Wor1d),G1建立回收集(Collection Set),回收集是指需要被回收的記憶體分段的集合,年輕代回收過程的回收集包含年輕代Eden區和Survivor區所有的記憶體分段。
回收過程:掃描根、更新RSet(dirty card queue)、處理RSet、複製物件、處理參照
G1回收過程-併發標記過程
G1回收過程 - 混合回收
Java垃圾收集器的設定對於JVM優化來說是一個很重要的選擇,選擇合適的垃圾收集器可以讓JVM的效能有一個很大的提升。怎麼選擇垃圾收集器?
優先調整堆的大小讓JVM自適應完成。
如果記憶體小於100M,使用序列收集器
如果是單核、單機程式,並且沒有停頓時間的要求,序列收集器
如果是多CPU、需要高吞吐量、允許停頓時間超過1秒,選擇並行或者JVM自己選擇
如果是多CPU、追求低停頓時間,需快速響應(比如延遲不能超過1秒,如網際網路應用),使用併發收集器
官方推薦G1,效能高。現在網際網路的專案,基本都是使用G1。
GC日誌分析
-XX:+PrintGc輸出GC日誌。類似:-verbose:gc
-XX:+PrintGcDetails輸出Gc的詳細日誌
-XX:+PrintGcTimestamps 輸出Gc的時間戳(以基準時間的形式)
-XX:+PrintGCDatestamps 輸出Gc的時間戳(以日期的形式,如2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC在進行Gc的前後列印出堆的資訊
-Xloggc:…/logs/gc.1og日誌檔案的輸出路徑
革命性的ZGC:在儘可能對吞吐量影響不大的前提下,實現在任意堆記憶體大小下都可以把垃圾收集的停頗時間限制在十毫秒以內的低延遲
ZGC的工作過程可以分爲4個階段:併發標記 - 併發預備重分配 - 併發重分配 - 併發重對映 等。
常用的日誌分析工具有:GCViewer、GCEasy、