本篇文章給大家帶來了關于java的相關知識,其中主要整理了JVM的相關問題,包括了JVM內存區域劃分、JVM類加載機制、VM的垃圾回收等等內容,下面一起來看一下,希望對大家有幫助。
推薦學習:《java視頻教程》
一.JVM內存區域劃分
JVM為什么要劃分出這些區域呢?JVM內存是從操作系統里面申請過來的,而JVM就根據功能需求將這些劃分成了一些小的模塊,這樣一塊大的場地就可以劃分成一些小的模塊,然后每個模塊就負責自己的功能就可以了,那接下來看看這些區域的功能到底是什么呢!
1.程序計數器
程序計數器是內存中最小的區域,這里面主要保存了下一條要執行的指令的地址在哪里(指令就是字節碼,一般程序要運行,JVM就需要把字節碼加載出來放到內存中,然后程序再把一條一條的指令從內存中取出來放到CPU上去執行,所以必須要記住當前執行到哪一條指令,以及下一條在哪里,因為CPU不是只給一個進程提供服務的,是給所有的進程都提供服務,是并發式的執行程序的,又因為操作系統是以線程為單位進行調度執行的,所以每個線程都要有自己的執行位置,也就是每一個線程都需要有一個程序計數器來記錄位置!)
2.棧
棧里面存放的主要是局部變量和方法調用信息,只要涉及到新方法的調用,就會有"入棧"的操作,每執行完成一個方法,就會有"出棧"的操作,而且棧也是每個線程都有一份的
因此對于遞歸來說,一定要控制好遞歸條件,否則很有可能會出現棧溢出(StackOverflowException)異常的!
3.堆
堆是內存中空間最大的區域,而且堆是每個進程只有一份的,進程中的多個線程公用一個堆,里面主要存放著new出來的對象以及對象的成員變量,例如String s = new String()如果在方法里面這里的s就是局部變量是在棧上的,如果這個s是成員變量,就是在堆上的,而后面new String()是對象的本體,對象是在堆上的,這是容易混淆的地方,另外堆還有一個重要的點就是關于垃圾回收問題,這個后面再詳細介紹!
4.方法區
方法區中存放的是"類對象",平常所寫的.java代碼經過編譯器翻譯過后就會變成.class(二進制字節碼),然后.class就會被加載到內存中,也就被JVM構造成了類對象(加載的過程就是稱為"類加載"),而這些類對象就會存放到方法區中,這里面就具體描述了類長啥樣(類的名字,類的成員及其成員名成員類型,類的方法及其方法名方法類型,以及一些指令…另外類對象里面還存放了一個很重要的東西,就是靜態成員,一般被static修飾的成員就成為了類屬性,而普通的方法被稱為實例屬性,這是有很大差別的)!
上面所介紹的是JVM中比較常見的區域,而一些JVM的內存區域劃分不一定是符合實際情況的,JVM在實現的過程中區域的劃分是不盡相同的,不同的廠商不同版本的JVM都是有可能存在差異的,不過對于我們普通的程序員而講,只要不是去實現JVM,那么就不需要了解那么深刻,講上面的幾個常見的區域加以了解就可以了!
二.JVM類加載機制
類加載其實是設計一個運行時環境的一個重要的功核心功能,這是非常重量級的,因此我這里也就簡單介紹一下!
上述就是類加載的具體過程,最后面的Using和Unloading就是使用的過程就不介紹了,就介紹一下前面的三個大的步驟:
1.Loading(加載)
在loading階段就會先找到對應的.class文件,然后打開并讀取(根據字節流).class文件,同時初步生成一個類對象,這個和完成的類加載(class Loading)是不相同的,不要弄混淆了!
class文件的具體格式(如果要實現一個Java編譯器就得按照這樣的格式來構造,實現JVM就得按照這個格式來進行加載!):
觀察這個格式就可以看到.class文件就把.java文件中的核心信息都表述進去了,只不過組織格式上發生了轉變,所以loading環節就會把讀取到的信息,初步填寫到類對象中
2.Linking(連接)
連接一般就是建立好多個實體之間的聯系
2.1.Verification(驗證)
Verification就是一個校驗的過程,主要就是驗證讀到的內容是不是和規范中規定的格式完全匹配,如果發現讀到的數據格式不符合規范,就會類加載失敗,并且拋出異常!
2.2.Preparation(準備)
Preparation階段是正式為定義的變量(靜態變量,就是static修飾的變量)分配內存并設置類變量初始值的階段,就會給每個靜態變量分配內存,并且設置為0值!
2.3.Resolution(解析)
Resolution階段是Java虛擬機將常量池內的符號引用替換為直接引用的過程,也就是初始化常量的過程,.class文件中常量是集中放置的,每個常量會有一個編號,而在.class文件中的結構體里初始情況就只是記錄的編號,然后就可以根據這個編號找到對應的內容,再填充到類對象中!
3.Initialization(初始化)
Initialization階段就是真正的對類對象進行初始化(根據寫的代碼),尤其是針對靜態成員
4.典型的面試題
class A { public A(){ System.out.println("A的構造方法"); } { System.out.println("A的構造代碼塊"); } static { System.out.println("A的靜態代碼塊"); }}class B extends A{ public B(){ System.out.println("B的構造方法"); } { System.out.println("B的構造代碼塊"); } static { System.out.println("B的靜態代碼塊"); }}public class Test extends B{ public static void main(String[] args) { new Test(); new Test(); }}
可以自己先嘗試寫一下輸出的結果
做這樣的題就需要把握幾個大的原則:
-
類加載階段就會進行靜態代碼塊的執行,要想創建實例,勢必要先進行類加載
-
靜態代碼塊只是類加載階段執行一次,其他階段都不會再執行
-
構造方法和構造代碼塊每次實例化都會執行,而且構造代碼塊會在構造方法前面執行~~
-
父類執行在前,子類執行在后!
-
程序是從main開始執行的,main的Test的方法,因此要執行main就需要先加載Test類
-
只有涉及到這個類了,類里面的東西才會被加載
輸出結果: A的靜態代碼塊 B的靜態代碼塊 A的構造代碼塊 A的構造方法 B的構造代碼塊 B的構造方法 A的構造代碼塊 A的構造方法 B的構造代碼塊 B的構造方法
5.雙親委派模型
這個東西是類加載中的一個環節,處于Loading階段(比較靠前的部分),雙親委派模型描述的就是JVM中的類加載器,如何根據類的全限定名(java.lang.String)找到.class文件的過程。這里的類加載器是JVM專門提供的對象,主要負責進行類加載,所以找文件的過程也是由類加載器來負責的,.class文件可能放置的位置有很多,有的要放到JDK目錄里面,有的放到項目目錄里面,還有的在其他特定的位置里面,因此JVM提供了多個類加載器,每個類加載器負責一個片區,而默認的類加載器主要有3個:
-
BootStrapClassLoader:負責加載標準庫中的類(String,ArrayList,Random,Scanner…)
-
ExtensionClassLoader:負責加載JDK擴展的類(現在很少用到)
-
ApplicationClassLoader:負責加載當前項目目錄中的類
-
另外程序員還可以自定義類加載器,來加載其他目錄中的類,Tomcat就自定義了類加載器,用來專門加載webapps里面的.class
雙親委派模型就描述了這個找目錄的過程,也就是上述類加載器是如何配合的
考慮找一下java.lang.String:
-
程序啟動,就會先進入ApplicationClassLoader類加載器
-
ApplicationClassLoader類加載器就會檢查下,它的父加載器是否已經加載過了,如果沒有,就調用父 類加載器ExtensionClassLoader
-
ExtensionClassLoader類加載器就會檢查下,它的父加載器是否已經加載過了,如果沒有,就調用父 類加載器BootStrapClassLoader
-
BootStrapClassLoader類加載器也會檢查下,它的父加載器是否已經加載過了,然后發現沒有父親,于是就掃描自己負責的目錄
-
然后java.lang.String這個類就在標準庫中能找到,然后后續就由BootStrapClassLoader加載器負責后續的加載過程,查找環節就結束了!
考慮找一下自己寫的Test類:
-
程序啟動,就會先進入ApplicationClassLoader類加載器
-
ApplicationClassLoader類加載器就會檢查下,它的父加載器是否已經加載過了,如果沒有,就調用父 類加載器ExtensionClassLoader
-
ExtensionClassLoader類加載器就會檢查下,它的父加載器是否已經加載過了,如果沒有,就調用父 類加載器BootStrapClassLoader
-
BootStrapClassLoader類加載器也會檢查下,它的父加載器是否已經加載過了,然后發現沒有父親,于是就掃描自己負責的目錄,沒掃描到,就會回到子加載器中繼續掃描
-
ExtensionClassLoader掃描自己負責的目錄,也沒有掃描到,再回到子加載器中繼續掃描
-
ApplicationClassLoader也掃描自己負責的目錄,自己寫的類就在自己的項目目錄下,因此就能找到,然后后續的類加載就由ApplicationClassLoad完成,此時查找目錄的環節就結束了~~(另外如果ApplicationClassLoader也沒有找到們就會拋出ClassNotFoundException異常)
這一套查找規則就稱為雙親委派模型,那為啥JVM要這樣設計呢,理由就是一旦程序員自己寫的類和全限定類名重復了,也能夠成功加載標準庫中的類,而不是自己寫的類!!!
另外如果是自定義的類加載器,要不要遵守這個雙親委派模型呢?
答案是可以遵守也可以不遵守,主要看需求,例如Tomcat加載webapp中的類,就沒有遵守,因為遵守了上面的類加載器也是不可能找到的!
三.JVM的垃圾回收
JVM中的垃圾回收機制(GC),一般在寫代碼的時候,經常就會涉及到申請內存,例如創建一個變量,new一個對象,調用一個方法,加載類…而申請內存的時機一般是明確的(需要保存某個或某些數據就需要申請內存),但是釋放內存的時機,卻是不那么清楚的,釋放的早了也不行(如果還是要使用的,結果已經被釋放了這就讓其無內存可用了,就讓這些數據"無處可去"),釋放的晚了也不行(釋放晚了,大量的囤積很有可能讓可用內存逐漸變少,很有可能會出現內存泄漏問題,就是無內存可以使用),因此內存的釋放要恰到好處才好!
而垃圾回收的本職是靠運行時環境額外做了很多的工作來完成釋放內存操作的,這讓程序員的心智負擔大大降低了,但是垃圾回收也是有劣勢的:①消耗額外的開銷(消耗資源耕