1. 程式人生 > 其它 >Android 開發必備的知識點——JVM基礎【轉】

Android 開發必備的知識點——JVM基礎【轉】

image

1.JVM與作業系統的關係

Java Virtual Machine

JVM 全稱 Java Virtual Machine,也就是我們耳熟能詳的 Java 虛擬機器。它能識別 .class字尾的檔案,並且能夠解析它的指令,最終呼叫作業系統上的函式,完成我們想要的操作。

翻譯

Java 程式不一樣,使用 javac 編譯成 .class 檔案之後,還需要使用 Java 命令去主動執行它,作業系統並不認識這些 .class 檔案。所以JVM就是一個翻譯。

image

從圖中可以看到,有了 JVM 這個抽象層之後,Java 就可以實現跨平臺了。JVM 只需要保證能夠正確執行 .class 檔案,就可以執行在諸如 Linux、Windows、MacOS 等平臺上了。

從跨平臺到跨語言

跨平臺: 我們寫的這個類Person這個類,在不同的作業系統上(Linux、Windows、MacOS 等平臺)執行,效果是一樣,這個就是JVM的跨平臺性。

為了實現跨平臺型,不同作業系統有不同的JDK的版本www.oracle.com/java/techno…

image

跨語言: JVM只識別字節碼,所以JVM其實跟語言是解耦的,也就是沒有直接關聯,並不是它翻譯Java檔案,而是識別class檔案,這個一般稱之為位元組碼。還有像Groovy 、Kotlin、Jruby等等語言,它們其實也是編譯成位元組碼,所以也可以在JVM上面跑,這個就是JVM的跨語言特徵。

2.JVM、JRE、JDK的關係

JVM只是一個翻譯,把Class翻譯成機器識別的程式碼,但是需要注意,JVM 不會自己生成程式碼,需要大家編寫程式碼,同時需要很多依賴類庫,這個時候就需要用到JRE。

JRE是什麼,它除了包含JVM之外,提供了很多的類庫(就是我們說的jar包,它可以提供一些即插即用的功能,比如讀取或者操作檔案,連線網路,使用I/O等等之類的)這些東西就是JRE提供的基礎類庫。JVM 標準加上實現的一大堆基礎類庫,就組成了 Java 的執行時環境,也就是我們常說的 JRE(Java Runtime Environment)。

但對於程式設計師來說,JRE還不夠。我寫完要編譯程式碼,還需要除錯程式碼,還需要打包程式碼、有時候還需要反編譯程式碼。所以我們會使用JDK,因為JDK還提供了一些非常好用的小工具,比如 javac(編譯程式碼)、java、jar (打包程式碼)、javap(反編譯<反彙編>)等。這個就是JDK。

具體可以文件可以通過官網去下載:www.oracle.com/java/techno…

image

3.JVM整體

一個 Java 程式,首先經過 javac 編譯成 .class 檔案,然後 JVM 將其載入到方法區,執行引擎將會執行這些位元組碼。執行時,會翻譯成作業系統相關的函式。JVM 作為 .class 檔案的翻譯存在,輸入位元組碼,呼叫作業系統函式。

過程如下:Java 檔案->編譯器>位元組碼->JVM->機器碼。

解釋執行與GIT:

image

我們所說的 JVM,狹義上指的就 HotSpot(因為JVM有很多版本,但是使用最多的是HotSpot)。如非特殊說明,我們都以 HotSpot 為準。Java 之所以成為跨平臺,就是由於 JVM 的存在。Java 的位元組碼,是溝通 Java 語言與 JVM 的橋樑,同時也是溝通 JVM 與作業系統的橋樑。

4.執行時資料區域

Java 引以為豪的就是它的自動記憶體管理機制。相比於 C++的手動記憶體管理、複雜難以理解的指標等,Java 程式寫起來就方便的多。

在 Java 中,JVM 記憶體主要分為堆、程式計數器、方法區、虛擬機器棧和本地方法棧。

image

程式計數器

較小的記憶體空間,當前執行緒執行的位元組碼的行號指示器;各執行緒之間獨立儲存,互不影響。

程式計數器是一塊很小的記憶體空間,主要用來記錄各個執行緒執行的位元組碼的地址,例如,分支、迴圈、跳轉、異常、執行緒恢復等都依賴於計數器。

由於 Java 是多執行緒語言,當執行的執行緒數量超過 CPU 核數時,執行緒之間會根據時間片輪詢爭奪 CPU 資源。如果一個執行緒的時間片用完了,或者是其它原因導致這個執行緒的 CPU 資源被提前搶奪,那麼這個退出的執行緒就需要單獨的一個程式計數器,來記錄下一條執行的指令。

程式計數器也是JVM中唯一不會OOM(OutOfMemory)的記憶體區域

image

虛擬機器棧

棧是什麼樣的資料結構?先進後出(FILO)的資料結構,

虛擬機器棧在JVM執行過程中儲存當前執行緒執行方法所需的資料,指令、返回地址

Java 虛擬機器棧是基於執行緒的。哪怕你只有一個 main() 方法,也是以執行緒的方式執行的。線上程的生命週期中,參與計算的資料會頻繁地入棧和出棧,棧的生命週期是和執行緒一樣的。

棧裡的每條資料,就是棧幀。在每個 Java 方法被呼叫的時候,都會建立一個棧幀,併入棧。一旦完成相應的呼叫,則出棧。所有的棧幀都出棧後,執行緒也就結束了。

棧的大小預設為1M,可用引數 –Xss調整大小,例如-Xss256k

每個棧幀,都包含四個區域 :(區域性變量表、運算元棧、動態連線、返回地址)

  • 區域性變量表: 顧名思義就是區域性變數的表,用於存放我們的區域性變數的。首先它是一個32位的長度,主要存放我們的Java的八大基礎資料型別,一般32位就可以存放下,如果是64位的就使用高低位佔用兩個也可以存放下,如果是區域性的一些物件,比如我們的Object物件,我們只需要存放它的一個引用地址即可。
  • 操作資料棧: 存放我們方法執行的運算元的,它就是一個棧,先進後出的棧結構,運算元棧,就是用來操作的,操作的的元素可以是任意的java資料型別,所以我們知道一個方法剛剛開始的時候,這個方法的運算元棧就是空的,運算元棧執行方法就是JVM一直執行入棧/出棧的操作
  • 動態連線: Java語言特性多型(需要類執行時才能確定具體的方法)。
  • 返回地址: 正常返回(呼叫程式計數器中的地址作為返回)、異常的話(通過異常處理器表<非棧幀中的>來確定)

棧幀執行對記憶體區域的影響

位元組碼助記碼解釋地址:cloud.tencent.com/developer/a…

image image

在JVM中,基於解釋執行的這種方式是基於棧的引擎,這個說的棧,就是運算元棧。

本地方法棧

本地方法棧跟 Java 虛擬機器棧的功能類似,Java 虛擬機器棧用於管理 Java 函式的呼叫,而本地方法棧則用於管理本地方法的呼叫。但本地方法並不是用 Java 實現的,而是由 C 語言實現的。

本地方法棧是和虛擬機器棧非常相似的一個區域,它服務的物件是 native 方法。你甚至可以認為虛擬機器棧和本地方法棧是同一個區域。

虛擬機器規範無強制規定,各版本虛擬機器自由實現 ,HotSpot直接把本地方法棧和虛擬機器棧合二為一 。

執行緒共享的區域:方法去、堆

方法區/永久代

很多開發者都習慣將方法區稱為“永久代”,其實這兩者並不是等價的。

HotSpot 虛擬機器使用永久代來實現方法區,但在其它虛擬機器中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一說。因此,方法區只是 JVM 中規範的一部分,可以說,在 HotSpot 虛擬機器中,設計人員使用了永久代來實現了 JVM 規範的方法區。

方法區主要是用來存放已被虛擬機器載入的類相關資訊,包括類資訊、靜態變數、常量、執行時常量池、字串常量池

JVM 在執行某個類的時候,必須先載入。在載入類 (載入、驗證、準備、解析、初始化)的時候,JVM 會先載入 class 檔案,而在 class 檔案中除了有類的版本、欄位、方法和介面等描述資訊外,還有一項資訊是常量池 (Constant Pool Table),用於存放編譯期間生成的各種字面量和符號引用。

字面量包括字串 (String a=“b”)、基本型別的常量 (final 修飾的變數),符號引用則包括類和方法的全限定名(例如 String 這個類,它的全限定名就是 Java/lang/String)、欄位的名稱和描述符以及方法的名稱和描述符。

而當類載入到記憶體中後,JVM 就會將 class 檔案常量池 中的內容存放到執行時的常量池中;在解析階段,JVM 會把符號引用替換為直接引用(物件的索引值)。

例如,類中的一個字串常量在 class 檔案中時,存放在 class 檔案常量池中的;在 JVM 載入完類之後,JVM 會將這個字串常量放到執行時常量池中,並在解析階段,指定該字串物件的索引值。執行時常量池是全域性共享的,多個類共用一個執行時常量池,class 檔案中常量池多個相同的字串在執行時常量池只會存在一份。

方法區與堆空間類似,也是一個共享記憶體區,所以方法區是執行緒共享的 。假如兩個執行緒都試圖訪問方法區中的同一個類資訊,而這個類還沒有裝入 JVM,那麼此時就只允許一個執行緒去載入它,另一個執行緒必須等待。在 HotSpot 虛擬機器、Java7 版本中已經將永久代的靜態變數和執行時常量池轉移到了堆中,其餘部分則儲存在 JVM 的非堆記憶體中,而 Java8 版本已經將方法區中實現的永久代去掉了,並用元空間(class metadata)代替了之前的永久代,並且元空間的儲存位置是本地

元空間大小引數:

  • jdk1.7及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
  • jdk1.8以後(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
  • jdk1.8以後大小就只受本機總記憶體的限制(如果不設定引數的話)

JVM引數參考:docs.oracle.com/javase/8/do…

Java8 為什麼使用元空間替代永久代,這樣做有什麼好處呢?

官方給出的解釋是:

移除永久代是為了融合 HotSpot JVM 與 JRockit VM 而做出的努力,因為 JRockit 沒有永久代,所以不需要配置永久代。

永久代記憶體經常不夠用或發生記憶體溢位,丟擲異常 java.lang.OutOfMemoryError: PermGen。這是因為在 JDK1.7 版本中,指定的 PermGen 區大小為 8M,由於 PermGen 中類的元資料資訊在每次 FullGC 的時候都可能被收集,回收率都偏低,成績很難令人滿意;還有,為 PermGen 分配多大的空間很難確定,PermSize 的大小依賴於很多因素,比如,JVM 載入的 class 總數、常量池的大小和方法的大小等。

堆是 JVM 上最大的記憶體區域,我們申請的幾乎所有的物件 ,都是在這裡儲存的。我們常說的垃圾回收,操作的物件就是堆。

堆空間一般是程式啟動時,就申請了,但是並不一定會全部使用。

隨著物件的頻繁建立,堆空間佔用的越來越多,就需要不定期的對不再使用的物件進行回收。這個在 Java 中,就叫作 GC(Garbage Collection)。

那一個物件建立的時候,到底是在堆上分配,還是在棧上分配呢?這和兩個方面有關:物件的型別和在 Java 類中存在的位置。

Java 的物件可以分為基本資料型別和普通物件。

對於普通物件來說,JVM 會首先在堆上建立物件,然後在其他地方使用的其實是它的引用。比如,把這個引用儲存在虛擬機器棧的區域性變量表中。

對於基本資料型別來說(byte、short、int、long、float、double、char),有兩種情況。當你在方法體內聲明瞭基本資料型別的物件,它就會在棧上直接分配。其他情況,都是在堆上分配。

堆大小引數:

引數解釋
-Xms 堆的最小值
-Xmx 堆的最大值
-Xmn 新生代的大小
-XX:NewSize 新生代最小值
-XX:MaxNewSize 生代最大值

例如- Xmx256m

5.直接記憶體

不是虛擬機器執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體區域;

  • 如果使用了NIO,這塊區域會被頻繁使用,在java堆內可以用directByteBuffer物件直接引用並操作;
  • 這塊記憶體不受java堆大小限制,但受本機總記憶體的限制,可以通過-XX:MaxDirectMemorySize來設定(預設與堆記憶體最大值一樣),所以也會出現OOM異常。
image

6.從底層深入理解執行時資料區

開啟HSDB工具

Jdk1.8啟動JHSDB的時候必須將sawindbg.dll複製到對應目錄的jre下

image

C:\Program Files\Java\jdk1.8.0_101\lib

執行 java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB

image

當我們通過 Java 執行以上程式碼時,JVM 的整個處理過程如下:

  1. JVM 向作業系統申請記憶體,JVM 第一步就是通過配置引數或者預設配置引數向作業系統申請記憶體空間。
  2. JVM 獲得記憶體空間後,會根據配置引數分配堆、棧以及方法區的記憶體大小。
  3. 完成上一個步驟後, JVM 首先會執行構造器,編譯器會在.java 檔案被編譯成.class 檔案時,收集所有類的初始化程式碼,包括靜態變數賦值語句、靜態程式碼塊、靜態方法,靜態變數和常量放入方法區
  4. 執行方法。啟動 main 執行緒,執行 main 方法,開始執行第一行程式碼。此時堆記憶體中會建立一個 Teacher 物件,物件引用 student 就存放在棧中。

執行其他方法時,具體的操作:棧幀執行對記憶體區域的影響。棧幀執行對記憶體區域的影響

image image

深入辨析堆和棧

功能

  • 以棧幀的方式儲存方法呼叫的過程,並存儲方法呼叫過程中基本資料型別的變數(int、short、long、byte、float、double、boolean、char等)以及物件的引用變數,其記憶體分配在棧上,變量出了作用域就會自動釋放;
  • 而堆記憶體用來儲存Java中的物件。無論是成員變數,區域性變數,還是類變數,它們指向的物件都儲存在堆記憶體中;

執行緒獨享還是共享

  • 棧記憶體歸屬於單個執行緒,每個執行緒都會有一個棧記憶體,其儲存的變數只能在其所屬執行緒中可見,即棧記憶體可以理解成執行緒的私有記憶體。
  • 堆記憶體中的物件對所有執行緒可見。堆記憶體中的物件可以被所有執行緒訪問。

空間大小

  • 棧的記憶體要遠遠小於堆記憶體,棧的深度是有限制的,可能發生StackOverFlowError問題。

7.記憶體溢位

棧溢位

引數:-Xss1m, 具體預設值需要檢視官網:docs.oracle.com/javase/8/do…

image

HotSpot版本中棧的大小是固定的,是不支援拓展的。

java.lang.StackOverflowError 一般的方法呼叫是很難出現的,如果出現了可能會是無限遞迴。

虛擬機器棧帶給我們的啟示:方法的執行因為要打包成棧楨,所以天生要比實現同樣功能的迴圈慢,所以樹的遍歷演算法中:遞迴和非遞迴(迴圈來實現)都有存在的意義。遞迴程式碼簡潔,非遞迴程式碼複雜但是速度較快。

OutOfMemoryError:不斷建立執行緒,JVM申請棧記憶體,機器沒有足夠的記憶體。(一般演示不出,演示出來機器也死了)

堆溢位

記憶體溢位:申請記憶體空間,超出最大堆記憶體空間。

如果是記憶體溢位,則通過 調大 -Xms,-Xmx引數。

如果不是記憶體洩漏,就是說記憶體中的物件卻是都是必須存活的,那麼久應該檢查JVM的堆引數設定,與機器的記憶體對比,看是否還有可以調整的空間,再從程式碼上檢查是否存在某些物件生命週期過長、持有狀態時間過長、儲存結構設計不合理等情況,儘量減少程式執行時的記憶體消耗。

方法區溢位

  1. 執行時常量池溢位
  2. 方法區中儲存的Class物件沒有被及時回收掉或者Class資訊佔用的記憶體超過了我們配置。

注意Class要被回收,條件比較苛刻(僅僅是可以,不代表必然,因為還有一些引數可以進行控制):

  1. 該類所有的例項都已經被回收,也就是堆中不存在該類的任何例項。
  2. 載入該類的ClassLoader已經被回收。
  3. 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
image

程式碼示例:

cglib是一個強大的,高效能,高質量的Code生成類庫,它可以在執行期擴充套件Java類與實現Java介面。

CGLIB包的底層是通過使用一個小而快的位元組碼處理框架ASM,來轉換位元組碼並生成新的類。除了CGLIB包,指令碼語言例如Groovy和BeanShell,也是使用ASM來生成java的位元組碼。當然不鼓勵直接使用ASM,因為它要求你必須對JVM內部結構包括class檔案的格式和指令集都很熟悉。

本機直接記憶體溢位

直接記憶體的容量可以通過MaxDirectMemorySize來設定(預設與堆記憶體最大值一樣),所以也會出現OOM異常;

由直接記憶體導致的記憶體溢位,一個比較明顯的特徵是在HeapDump檔案中不會看見有什麼明顯的異常情況,如果發生了OOM,同時Dump檔案很小,可以考慮重點排查下直接記憶體方面的原因。

8.虛擬機器優化技術

編譯優化技術——方法內聯

方法內聯的優化行為,就是把目標方法的程式碼原封不動的“複製”到呼叫的方法中,避免真實的方法呼叫而已。

image

棧的優化技術——棧幀之間資料的共享

在一般的模型中,兩個不同的棧幀的記憶體區域是獨立的,但是大部分的JVM在實現中會進行一些優化,使得兩個棧幀出現一部分重疊。(主要體現在方法中有引數傳遞的情況),讓下面棧幀的運算元棧和上面棧幀的部分區域性變數重疊在一起,這樣做不但節約了一部分空間,更加重要的是在進行方法呼叫時就可以直接公用一部分資料,無需進行額外的引數複製傳遞了。

image

使用HSDB工具檢視棧空間一樣可以看到。

image

9.虛擬機器中的物件

image

物件的分配

虛擬機器遇到一條new指令時,首先檢查是否被類載入器載入,如果沒有,那必須先執行相應的類載入過程。

類載入就是把class載入到JVM的執行時資料區的過程(類載入後面有專門的專題講)。

1.檢查載入

首先檢查這個指令的引數是否能在常量池中定位到一個類的符號引用(符號引用: 符號引用以一組符號來描述所引用的目標),並且檢查類是否已經被載入、解析和初始化過。

2.分配記憶體

接下來虛擬機器將為新生物件分配記憶體。為物件分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃分出來。

指標碰撞

如果Java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間那邊挪動一段與物件大小相等的距離,這種分配方式稱為指標碰撞

image
空閒列表

如果Java堆中的記憶體並不是規整的,已使用的記憶體和空閒的記憶體相互交錯,那就沒有辦法簡單地進行指標碰撞了,虛擬機器就必須維護一個列表,記錄上哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式稱為 空閒列表

image

選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

如果是Serial、ParNew等帶有壓縮的整理的垃圾回收器的話,系統採用的是指標碰撞,既簡單又高效。

如果是使用CMS這種不帶壓縮(整理)的垃圾回收器的話,理論上只能採用較複雜的空閒列表。

併發安全

除如何劃分可用空間之外,還有另外一個需要考慮的問題是物件建立在虛擬機器中是非常頻繁的行為,即使是僅僅修改一個指標所指向的位置,在併發情況下也並不是執行緒安全的,可能出現正在給物件A分配記憶體,指標還沒來得及修改,物件B又同時使用了原來的指標來分配記憶體的情況。

CAS機制

解決這個問題有兩種方案,一種是對分配記憶體空間的動作進行同步處理——實際上虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性;

分配緩衝

另一種是把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊私有記憶體,也就是本地執行緒分配緩衝(Thread Local Allocation Buffer,TLAB),JVM線上程初始化時,同時也會申請一塊指定大小的記憶體,只給當前執行緒使用,這樣每個執行緒都單獨擁有一個Buffer,如果需要分配記憶體,就在自己的Buffer上分配,這樣就不存在競爭的情況,可以大大提升分配效率,當Buffer容量不夠的時候,再重新從Eden區域申請一塊繼續使用。

TLAB的目的是在為新物件分配記憶體空間時,讓每個Java應用執行緒能在使用自己專屬的分配指標來分配空間,減少同步開銷。

TLAB只是讓每個執行緒有私有的分配指標,但底下存物件的記憶體空間還是給所有執行緒訪問的,只是其它執行緒無法在這個區域分配而已。當一個TLAB用滿(分配指標top撞上分配極限end了),就新申請一個TLAB。

引數:

-XX:+UseTLAB

允許在年輕代空間中使用執行緒本地分配塊(TLAB)。預設情況下啟用此選項。要禁用TLAB,請指定-XX:-UseTLAB。

docs.oracle.com/javase/8/do…

image

3.記憶體空間初始化

(注意不是構造方法)記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(如int值為0,boolean值為false等等)。這一步操作保證了物件的例項欄位在Java程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。

4.設定

接下來,虛擬機器要對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的元資料資訊(Java classes在Java hotspot VM內部表示為類元資料)、物件的雜湊碼、物件的GC分代年齡等資訊。這些資訊存放在物件的物件頭之中。

5.物件初始化

在上面工作都完成之後,從虛擬機器的視角來看,一個新的物件已經產生了,但從Java程式的視角來看,物件建立才剛剛開始,所有的欄位都還為零值。所以,一般來說,執行new指令之後會接著把物件按照程式設計師的意願進行初始化(構造方法),這樣一個真正可用的物件才算完全產生出來。

物件的記憶體佈局

image

在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3塊區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)

物件頭包括兩部分資訊,第一部分用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。

物件頭的另外一部分是型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

如果物件是一個java陣列,那麼在物件頭中還有一塊用於記錄陣列長度的資料。

第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。由於HotSpot VM的自動記憶體管理系統要求對物件的大小必須是8位元組的整數倍。當物件其他資料部分沒有對齊時,就需要通過對齊填充來補全。

image

物件的訪問定位

建立物件是為了使用物件,我們的Java程式需要通過棧上的reference資料來操作堆上的具體物件。目前主流的訪問方式有使用控制代碼和直接指標兩種。

控制代碼

如果使用控制代碼訪問的話,那麼Java堆中將會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊。

直接指標

如果使用直接指標訪問, reference中儲存的直接就是物件地址。

這兩種物件訪問方式各有優勢,使用控制代碼來訪問的最大好處就是reference中儲存的是穩定的控制代碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制代碼中的例項資料指標,而reference本身不需要修改。

使用直接指標訪問方式的最大好處就是速度更快,它節省了一次指標定位的時間開銷,由於物件的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。

對Sun HotSpot而言,它是使用直接指標訪問方式進行物件訪問的。

image

10.判斷物件的存活

在堆裡面存放著幾乎所有的物件例項,垃圾回收器在對對進行回收前,要做的事情就是確定這些物件中哪些還是“存活”著,哪些已經“死去”(死去代表著不可能再被任何途徑使用得物件了)

引用計數法

在物件中新增一個引用計數器,每當有一個地方引用它,計數器就加1,當引用失效時,計數器減1.

Python在用,但主流虛擬機器沒有使用,因為存在物件相互引用的情況,這個時候需要引入額外的機制來處理,這樣做影響效率,

image image

在程式碼中看到,只保留相互引用的物件還是被回收掉了,說明JVM中採用的不是引用計數法。

可達性分析

面試時重要的知識點,牢記)

來判定物件是否存活的。這個演算法的基本思路就是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。

image

作為GC Roots的物件包括下面幾種:

  1. 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
  2. 方法區中類靜態屬性引用的物件。
  3. 方法區中常量引用的物件。
  4. 本地方法棧中JNI(即一般說的Native方法)引用的物件。
  5. JVM的內部引用(class物件、異常物件NullPointException、OutofMemoryError,系統類載入器)。
  6. 所有被同步鎖(synchronized關鍵)持有的物件。
  7. JVM內部的JMXBean、JVMTI中註冊的回撥、原生代碼快取等
  8. JVM實現中的“臨時性”物件,跨代引用的物件(在使用分代模型回收只回收部分代時)

以上的回收都是物件,類的回收條件:

注意Class要被回收,條件比較苛刻,必須同時滿足以下的條件(僅僅是可以,不代表必然,因為還有一些引數可以進行控制):

  1. 該類所有的例項都已經被回收,也就是堆中不存在該類的任何例項。
  2. 載入該類的ClassLoader已經被回收。
  3. 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
  4. 引數控制:
image

還有一個廢棄的常量,這個是物件的回收非常相似,比如:假如有一個字串“king”進入常量池。

Finalize方法

即使通過可達性分析判斷不可達的物件,也不是“非死不可”,它還會處於“緩刑”階段,真正要宣告一個物件死亡,需要經過兩次標記過程,一次是沒有找到與GCRoots的引用鏈,它將被第一次標記。隨後進行一次篩選(如果物件覆蓋了finalize),我們可以在finalize中去拯救。

程式碼演示:

image

執行結果

image

可以看到,物件可以被拯救一次(finalize執行第一次,但是不會執行第二次)

程式碼改一下,再來一次。

image

執行結果

image

物件沒有被拯救,這個就是finalize方法執行緩慢,還沒有完成拯救,垃圾回收器就已經回收掉了。

所以建議大家儘量不要使用finalize,因為這個方法太不可靠。在生產中你很難控制方法的執行或者物件的呼叫順序,建議大家忘了finalize方法!因為在finalize方法能做的工作,java中有更好的,比如try-finally或者其他方式可以做得更好

11.物件的引用型別

強引用

一般的Object obj = new Object() ,就屬於強引用。在任何情況下,只有有強引用關聯(與根可達)還在,垃圾回收器就永遠不會回收掉被引用的物件。

軟引用 SoftReference

一些有用但是並非必需,用軟引用關聯的物件,系統將要發生記憶體溢位(OuyOfMemory)之前,這些物件就會被回收(如果這次回收後還是沒有足夠的空間,才會丟擲記憶體溢位)。參見程式碼:

VM引數 -Xms10m -Xmx10m -XX:+PrintGC

image

執行結果

例如,一個程式用來處理使用者提供的圖片。如果將所有圖片讀入記憶體,這樣雖然可以很快的開啟圖片,但記憶體空間使用巨大,一些使用較少的圖片浪費記憶體空間,需要手動從記憶體中移除。如果每次開啟圖片都從磁碟檔案中讀取到記憶體再顯示出來,雖然記憶體佔用較少,但一些經常使用的圖片每次開啟都要訪問磁碟,代價巨大。這個時候就可以用軟引用構建快取。

弱引用 WeakReference

一些有用(程度比軟引用更低)但是並非必需,用弱引用關聯的物件,只能生存到下一次垃圾回收之前,GC發生時,不管記憶體夠不夠,都會被回收。

參看程式碼:

image

注意: 軟引用 SoftReference和弱引用 WeakReference,可以用在記憶體資源緊張的情況下以及建立不是很重要的資料快取。當系統記憶體不足的時候,快取中的內容是可以被釋放的。

實際運用(WeakHashMap、ThreadLocal)

虛引用 PhantomReference

幽靈引用,最弱(隨時會被回收掉)

垃圾回收的時候收到一個通知,就是為了監控垃圾回收器是否正常工作。

12.學習垃圾回收的意義

Java與C++等語言最大的技術區別:自動化的垃圾回收機制(GC)

為什麼要了解GC和記憶體分配策略

  1. 面試需要;
  2. GC對應用的效能是有影響的;
  3. 寫程式碼有好處

棧:棧中的生命週期是跟隨執行緒,所以一般不需要關注

堆:堆中的物件是垃圾回收的重點

方法區/元空間:這一塊也會發生垃圾回收,不過這塊的效率比較低,一般不是我們關注的重點

13.物件的分配策略

物件的分配原則

  • 物件優先在Eden分配
  • 空間分配擔保
  • 大物件直接進入老年代
  • 長期存活的物件進入老年代
  • 動態物件年齡判定
image

棧上分配

沒有逃逸

即方法中的物件沒有發生逃逸。

逃逸分析的原理: 分析物件動態作用域,當一個物件在方法中定義後,它可能被外部方法所引用,比如:呼叫引數傳遞到其他方法中,這種稱之為方法逃逸,甚至還有可能被外部執行緒訪問到,例如:賦值給其他執行緒中訪問的變數,這個稱之為執行緒逃逸

從不逃逸到方法逃逸到執行緒逃逸,稱之為物件由低到高的不同逃逸程度

如果確定一個物件不會逃逸出執行緒之外,那麼讓物件在棧上分配記憶體可以提高JVM的效率。

逃逸分析程式碼

public class EscapeAnalysisTest {
   public static void main(String[] args) throws Exception {
       long start = System.currentTimeMillis();
       for (int i = 0; i < 50000000; i++) {
           allocate();
      }
       System.out.println((System.currentTimeMillis() - start) + " ms");
       Thread.sleep(600000);
  }

   static void allocate() {
       MyObject myObject = new MyObject(2020, 2020.6);
  }

   static class MyObject {
       int a;
       double b;

       MyObject(int a, double b) {
           this.a = a;
           this.b = b;
      }
  }
}

這段程式碼在呼叫的過程中 myboject這個物件屬於全域性逃逸,JVM可以做棧上分配

然後通過開啟和關閉DoEscapeAnalysis開關觀察不同。

開啟逃逸分析(JVM預設開啟)

image

檢視執行速度

image

關閉逃逸分析

image

檢視執行速度

image

測試結果可見,開啟逃逸分析對程式碼的執行效能有很大的影響!那為什麼有這個影響?

逃逸分析

如果是逃逸分析出來的物件可以在棧上分配的話,那麼該物件的生命週期就跟隨執行緒了,就不需要垃圾回收,如果是頻繁的呼叫此方法則可以得到很大的效能提高。

採用了逃逸分析後,滿足逃逸的物件在棧上分配

image

沒有開啟逃逸分析,物件都在堆上分配,會頻繁觸發垃圾回收(垃圾回收會影響系統性能),導致程式碼執行慢

image

程式碼驗證

開啟GC列印日誌:-XX:+PrintGC

開啟逃逸分析

image

可以看到沒有GC日誌

關閉逃逸分析

image

可以看到關閉了逃逸分析,JVM在頻繁的進行垃圾回收(GC),正是這一塊的操作導致效能有較大的差別。

物件優先在Eden區分配

虛擬機器引數:

-Xms20m
-Xmx20m
-Xmn10m
-XX:+PrintGCDetails
-XX:+PrintGCDetails 列印垃圾回收日誌,程式退出時輸出當前記憶體的分配情況

注意:新生代初始時就有大小

大多數情況下,物件在新生代Eden區中分配。當Eden區沒有足夠空間分配時,虛擬機器將發起一次Minor GC。

大物件直接進入老年代

-Xms20m
-Xmx20m
-Xmn10m
-XX:+PrintGCDetails
-XX:PretenureSizeThreshold=4m
-XX:+UseSerialGC

PretenureSizeThreshold引數只對Serial和ParNew兩款收集器有效。

最典型的大物件是那種很長的字串以及陣列。這樣做的目的:1.避免大量記憶體複製,2.避擴音前進行垃圾回收,明明記憶體有空間進行分配。

長期存活物件進入老年區

如果物件在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並將物件年齡設為1,物件在Survivor區中每熬過一次 Minor GC,年齡就增加1,當它的年齡增加到一定程度(併發的垃圾回收器預設為15),CMS是6時,就會被晉升到老年代中。

-XX:MaxTenuringThreshold調整

物件年齡動態判定

為了能更好地適應不同程式的記憶體狀況,虛擬機器並不是永遠地要求物件的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡

空間分配擔保

在發生Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次Minor GC,儘管這次Minor GC是有風險的,如果擔保失敗則會進行一次Full GC;如果小於,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次Full GC。

本地執行緒分配緩衝(TLAB)

具體見章節分配緩衝

14.垃圾回收演算法

垃圾回收演算法的實現設計到大量的程式細節,並且每一個平臺的虛擬機器操作記憶體的方式都有不同,所以不需要去了解演算法的實現,我們重點講解分代收集理論和3種演算法的思想。

分代收集理論

當前商業虛擬機器的垃圾收集器,大多遵循“分代收集”的理論來進行設計,這個理論大體上是這麼描述的:

  1. 絕大部分的物件都是朝生夕死
  2. 熬過多次垃圾回收的物件就越難回收。

根據以上兩個理論,朝生夕死的物件放一個區域,難回收的物件放另外一個區域,這個就構成了新生代和老年代。

image

GC種類

市面上發生垃圾回收的叫法很多,我大體整理了一下:

  1. 新生代回收(Minor GC/Young GC):指只是進行新生代的回收。
  2. 老年代回收(Major GC/Old GC):指只是進行老年代的回收。目前只有CMS垃圾回收器會有這個單獨的收集老年代的行為。(Major GC定義是比較混亂,有說指是老年代,有的說是做整個堆的收集,這個需要你根據別人的場景來定,沒有固定的說法)
  3. 整堆收集(Full GC):收集整個Java堆和方法區(注意包含方法區)
image image

複製演算法(Copying)

將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半。

注意:記憶體移動是必須實打實的移動(複製),不能使用指標玩。

複製回收演算法適合於新生代,因為大部分物件朝生夕死,那麼複製過去的物件比較少,效率自然就高,另外一半的一次性清理是很快的。

image

特點

  • 實現簡單、執行高效
  • 記憶體複製、沒有記憶體碎片
  • 利用率只有一半

Appel式回收

一種更加優化的複製回收分代策略:具體做法是分配一塊較大的Eden區和兩塊較小的Survivor空間(你可以叫做From或者To,也可以叫做Survivor1和Survivor2)

image

專門研究表明,新生代中的物件98%是“朝生夕死”的,所以並不需要按照1:1的比例來劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor[1]。當回收時,將Eden和Survivor中還存活著的物件一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。

HotSpot虛擬機器預設Eden和Survivor的大小比例是8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),只有10%的記憶體會被“浪費”。當然,98%的物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於10%的物件存活,當Survivor空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保(Handle Promotion)

標記-清除演算法(Mark-Sweep)

演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。

回收效率不穩定,如果大部分物件是朝生夕死,那麼回收效率降低,因為需要大量標記物件和回收物件,對比複製回收效率很低。

它的主要不足空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

回收的時候如果需要回收的物件越多,需要做的標記和清除的工作越多,所以標記清除演算法適用於老年代。複製回收演算法適用於新生代。

特點:

  • 執行效率不穩定
  • 記憶體碎片導致提前GC
image

標記-整理演算法(Mark-Compact)

首先標記出所有需要回收的物件,在標記完成後,後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。標記整理演算法雖然沒有記憶體碎片,但是效率偏低。

我們看到標記整理與標記清除演算法的區別主要在於物件的移動。物件移動不單單會加重系統負擔,同時需要全程暫停使用者執行緒才能進行,同時所有引用物件的地方都需要更新。

特點:

image

所以看到,老年代採用的標記整理演算法與標記清除演算法,各有優點,各有缺點。

15.JVM中常見的垃圾收集器

分代收集的思想

在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。

而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”演算法來進行回收。

請記住下圖的垃圾收集器和之間的連線關係。 具體看官網JVM引數:docs.oracle.com/javase/8/do…

image image

並行:垃圾收集的多執行緒的同時進行。

併發:垃圾收集的多執行緒和應用的多執行緒同時進行。

注:吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+ 垃圾收集時間)

垃圾收集時間= 垃圾回收頻率 * 單次垃圾回收時間

16.垃圾回收器工作示意圖

Serial/Serial Old

最古老的,單執行緒,獨佔式,成熟,適合單CPU 伺服器

-XX:+UseSerialGC 新生代和老年代都用序列收集器
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old
複製程式碼

ParNew

和Serial基本沒區別,唯一的區別:多執行緒,多CPU的,停頓時間比Serial少

-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old

Parallel Scavenge(ParallerGC)/Parallel Old

關注吞吐量的垃圾收集器,高吞吐量則可以高效率地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。

所謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間),虛擬機器總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

image

Concurrent Mark Sweep (CMS)

image

收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符合這類應用的需求。

從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基於“標記—清除”演算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分為4個步驟,包括:

  • 初始標記 -短暫 ,僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快。

  • 併發標記 -和使用者的應用程式同時進行 ,進行GC Roots追蹤的過程,標記從GCRoots開始關聯的所有物件開始遍歷整個可達分析路徑的物件。這個時間比較長,所以採用併發處理(垃圾回收器執行緒和使用者執行緒同時工作)

  • 重新標記 -短暫 ,為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。

  • 併發清除-同時進行

由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,所以,從總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

-XX:+UseConcMarkSweepGC ,表示新生代使用ParNew,老年代的用CMS

CPU敏感: CMS對處理器資源敏感,畢竟採用了併發的收集、當處理核心數不足4個時,CMS對使用者的影響較大。

浮動垃圾: 由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。

由於浮動垃圾的存在,因此需要預留出一部分記憶體,意味著 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。

在1.6的版本中老年代空間使用率閾值(92%)

如果預留的記憶體不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機器將臨時啟用 Serial Old 來替代 CMS。

會產生空間碎片: 標記 - 清除演算法會導致產生不連續的空間碎片

總體來說,CMS是JVM推出了第一款併發垃圾收集器 ,所以還是非常有代表性。

但是最大的問題是CMS採用了標記清除演算法,所以會有記憶體碎片,當碎片較多時,給大物件的分配帶來很大的麻煩,為了解決這個問題,CMS提供一個引數:-XX:+UseCMSCompactAtFullCollection,一般是開啟的,如果分配不了大物件,就進行記憶體碎片的整理過程。

這個地方一般會使用Serial Old ,因為Serial Old是一個單執行緒,所以如果記憶體空間很大、且物件較多時,CMS發生這樣情況會很卡。

image

17.Stop The World現象

任何的GC收集器都會進行業務執行緒的暫停,這個就是STW,Stop The World,所以我們GC調優的目標就是儘可能的減少STW的時間和次數。

image

G1

image image

-XX:+UseG1GC

記憶體佈局: 在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。每一個區域可以通過引數-XX:G1HeapRegionSize=size 來設定。

image

Region中還有一塊特殊區域Humongous區域,專門用於儲存大物件,一般只要認為一個物件超過了Region容量的一般可認為是大物件,如果物件超級大,那麼使用連續的N個Humongous區域來儲存。

並行與併發 :G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程式繼續執行。

分代收集 :與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的舊物件以獲取更好的收集效果。

空間整合 :與CMS的“標記—清理”演算法不同,G1從整體來看是基於“標記—整理”演算法實現的收集器,從區域性(兩個Region之間)上來看是基於“複製”演算法實現的,但無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC。

追求停頓時間

-XX:MaxGCPauseMillis 指定目標的最大停頓時間,G1嘗試調整新生代和老年代的比例,堆大小,晉升年齡來達到這個目標時間。

-XX:ParallerGCThreads:設定GC的工作執行緒數量。

一般在G1和CMS中間選擇的話平衡點在6~8G,只有記憶體比較大G1才能發揮優勢。

18.池與String

常量池有很多概念,包括執行時常量池、class常量池、字串常量池。

虛擬機器規範只規定以上區域屬於方法區,並沒有規定虛擬機器廠商的實現。

嚴格來說是靜態常量池和執行時常量池 ,靜態常量池是存放字串字面量、符號引用以及類和方法的資訊,而執行時常量池存放的是執行時一些直接引用。 執行時常量池是在類載入完成之後,將靜態常量池中的符號引用值轉存到執行時常量池中,類在解析之後,將符號引用替換成直接引用。 這兩個常量池在JDK1.7版本之後,就移到堆記憶體中了,這裡指的是物理空間,而邏輯上還是屬於方法區(方法區是邏輯分割槽)。

字面量:

給基本型別變數賦值的方式就叫做字面量或者字面值

比如:int i=120; long j=10L;

符號引用 :包括類和方法的全限定名(例如 String 這個類,它的全限定名就是 Java/lang/String)、欄位的名稱和描述符以及方法的名稱和描述符。

直接引用 :具體物件的索引值。

String 物件是如何實現的?

瞭解了 String 物件的實現後,你有沒有發現在實現程式碼中 String 類被 final 關鍵字修飾了,而且變數 char 陣列也被 final 修飾了。我們知道類被 final 修飾代表該類不可繼承,而 char[]被 final+private 修飾,代表了 String 物件不可被更改。Java 實現的這個特性叫作 String 物件的不可變性,即 String 物件一旦建立成功,就不能再對它進行改變。

image

在 Java 中,通常有兩種建立字串物件的方式,

一種是通過字串常量的方式建立,如 String str=“abc”;

這種方式首先會檢查該物件是否在字串常量池中,如果在,就返回該物件引用,否則新的字串將在常量池中被建立。這種方式可以減少同一個值的字串物件的重複建立,節約記憶體。

另一種是字串變數通過 new 形式的建立,如 String str = new String(“abc”)。

這種方式,首先在編譯類檔案時,"abc"常量字串將會放入到常量結構中,在類載入時,“abc"將會在常量池中建立;其次,在呼叫 new 時,JVM 命令將會呼叫 String 的建構函式,同時引用常量池中的"abc” 字串,在堆記憶體中建立一個 String 物件;最後,str 將引用 String 物件。

如果呼叫 intern 方法,會去檢視字串常量池中是否有等於該物件的字串的引用,如果沒有會把首次遇到的字串的引用新增到常量池中;如果有,就返回常量池中的字串引用。(這個版本都是基於JDK1.7及以後版本)

19.常見面試題

JVM記憶體結構說一下!

開放式題目,具體可見章節 執行時資料區域

一般從兩個維度出發:執行緒私有和執行緒共享。到每一個記憶體區域的細節點。

image

Java 虛擬機器棧是基於執行緒的。哪怕你只有一個 main() 方法,也是以執行緒的方式執行的。線上程的生命週期中,參與計算的資料會頻繁地入棧和出棧,棧的生命週期是和執行緒一樣的。

棧裡的每條資料,就是棧幀。在每個 Java 方法被呼叫的時候,都會建立一個棧幀,併入棧。一旦完成相應的呼叫,則出棧。所有的棧幀都出棧後,執行緒也就結束了。每個棧幀,都包含四個區域:

  • 區域性變量表
  • 運算元棧
  • 動態連線
  • 返回地址

本地方法棧是和虛擬機器棧非常相似的一個區域,它服務的物件是 native 方法。

程式計數器是一塊較小的記憶體空間,它的作用可以看作是當前執行緒所執行的位元組碼的行號指示器。

堆是 JVM 上最大的記憶體區域,我們申請的幾乎所有的物件,都是在這裡儲存的。我們常說的垃圾回收,操作的物件就是堆。

方法區,這個區域儲存的內容,包括:類的資訊、常量池、方法資料、方法程式碼就可以了。

什麼情況下記憶體棧溢位?

java.lang.StackOverflowError 如果出現了可能會是無限遞迴。

OutOfMemoryError:不斷建立執行緒,JVM申請棧記憶體,機器沒有足夠的記憶體。

描述new一個物件的流程!

image

具體見章節物件的分配

Java物件會不會分配在棧中?

可以,如果這個物件不滿足逃逸分析,那麼虛擬機器在特定的情況下會走棧上分配。

如果判斷一個物件是否被回收,有哪些演算法,實際虛擬機器使用得最多的是什麼?

引用計數法和根可達性分析兩種,用得最多是根可達性分析。

GC收集演算法有哪些?他們的特點是什麼?

複製、標記清除、標記整理。複製速度快,但是要浪費空間,不會記憶體碎片。標記清除空間利用率高,但是有記憶體碎片。標記整理演算法沒有記憶體碎片,但是要移動物件,效能較低。三種演算法各有所長,各有所短。

JVM中一次完整的GC流程是怎樣的?物件如何晉級到老年代?

物件優先在新生代區中分配,若沒有足夠空間,Minor GC; 大物件(需要大量連續記憶體空間)直接進入老年態;長期存活的物件進入老年態。

如果物件在新生代出生並經過第一次MGC後仍然存活,年齡+1,若年齡超過一定限制(15),則被晉升到老年態。

Java中的幾種引用關係,他們的區別是什麼?

強引用

一般的Object obj = new Object() ,就屬於強引用。在任何情況下,只有有強引用關聯(與根可達)還在,垃圾回收器就永遠不會回收掉被引用的物件。

軟引用 SoftReference

一些有用但是並非必需,用軟引用關聯的物件,系統將要發生記憶體溢位(OuyOfMemory)之前,這些物件就會被回收(如果這次回收後還是沒有足夠的空間,才會丟擲記憶體溢位)。

弱引用 WeakReference

一些有用(程度比軟引用更低)但是並非必需,用弱引用關聯的物件,只能生存到下一次垃圾回收之前,GC發生時,不管記憶體夠不夠,都會被回收。

虛引用 PhantomReference

幽靈引用,最弱(隨時會被回收掉)

垃圾回收的時候收到一個通知,就是為了監控垃圾回收器是否正常工作。

final、finally、finalize的區別?

在java中,final可以用來修飾類,方法和變數(成員變數或區域性變數)

當用final修飾類的時,表明該類不能被其他類所繼承。當我們需要讓一個類永遠不被繼承,此時就可以用final修飾,但要注意:

final類中所有的成員方法都會隱式的定義為final方法。

使用final方法的原因主要有兩個:

(1) 把方法鎖定,以防止繼承類對其進行更改。

(2) 效率,在早期的java版本中,會將final方法轉為內嵌呼叫。但若方法過於龐大,可能在效能上不會有多大提升。因此在最近版本中,不需要final方法進行這些優化了。

final成員變量表示常量,只能被賦值一次,賦值後其值不再改變。

finally作為異常處理的一部分 ,它只能用在try/catch語句中,並且附帶一個語句塊,表示這段語句最終一定會被執行(不管有沒有丟擲異常),經常被用在需要釋放資源的情況下

Object中的Finalize方法

即使通過可達性分析判斷不可達的物件,也不是“非死不可”,它還會處於“緩刑”階段,真正要宣告一個物件死亡,需要經過兩次標記過程,一次是沒有找到與GCRoots的引用鏈,它將被第一次標記。隨後進行一次篩選(如果物件覆蓋了finalize),我們可以在finalize中去拯救。

所以建議大家儘量不要使用finalize,因為這個方法太不可靠。在生產中你很難控制方法的執行或者物件的呼叫順序,建議大家忘了finalize方法!因為在finalize方法能做的工作,java中有更好的,比如try-finally或者其他方式可以做得更好

String s = new String(“xxx”);建立了幾個物件?

2個

1、 在一開始字串"xxx"會在載入類時,在常量池中建立一個字串物件。

2、 呼叫 new時 會在堆記憶體中建立一個 String 物件,String 物件中的 char 陣列將會引用常量池中字串。

最後

筆者在面試前,從網上收集了一些 Android 開發相關的學習文件、面試題、Android 核心筆記等等文件,進行了複習,在此分享給大家,希望能幫助到大家學習提升,如有需要參考的可以直接去我 GitHub地址:https://github.com/733gh/Android-T3 訪問查閱。




作者:冬日毛毛雨
連結:https://www.jianshu.com/p/cd71540316c0


本文來自部落格園,作者:up~up,轉載請註明原文連結:https://www.cnblogs.com/soft-engineer/p/14985724.html