Java記憶體區域
一:JVM類加載機制詳解
首先通過編譯器把 Java 代碼轉換成位元組碼,類加載器(ClassLoader)再把位元組碼加載到記憶體中,將其放在運行時資料區(Runtime data area)的方法區內,而位元組碼檔案只是 JVM 的一套指令集規范,并不能直接交給底層作業系統去執行,因此需要特定的命令決議器執行引擎(Execution Engine),將位元組碼翻譯成底層系統指令,再交由 CPU 去執行,而這個程序中需要呼叫其他語言的本地庫介面(Native Interface)來實作整個程式的功能,
一 類的生命周期
類的生命周期包括這幾個部分,加載、連接、初始化、使用和卸載,其中前三部是類的加載的程序 
- 加載,查找并加載類的二進制資料,在Java堆中也創建一個java.lang.Class類的物件
- 連接,連接又包含三塊內容:驗證、準備、初始化, 1、驗證,檔案格式、元資料、位元組碼、符號參考驗證; 2、準備,為類的靜態變數分配記憶體,并將其初始化為默認值; 3、決議,把類中的符號參考轉換為直接參考
- 初始化,為類的靜態變數賦予正確的初始值
- 使用,new出物件程式中使用
- 卸載,執行垃圾回收
二 類加載器

類加載器負責加載所有的類,其為所有被載入記憶體中的類生成一個java.lang.Class實體物件,一旦一個類被加載如JVM中,同一個類就不會被再次載入了,正如一個物件有一個唯一的標識一樣,一個載入JVM的類也有一個唯一的標識,在Java中,一個類用其全限定類名(包括包名和類名)作為標識;但在JVM中,一個類用其全限定類名和其類加載器作為其唯一標識,例如,如果在pg的包中有一個名為Person的類,被類加載器ClassLoader的實體kl負責加載,則該Person類對應的Class物件在JVM中表示為(Person.pg.kl),這意味著兩個類加載器加載的同名類:(Person.pg.kl)和(Person.pg.kl2)是不同的、它們所加載的類也是完全不同、互不兼容的
1、根類加載器(Bootstrap ClassLoader)
這個類將負責把<JAVA_HOME>\lib\目錄中的,或者-Xbootclasspath引數指定的目錄所指定的路徑中的,并且是虛擬機識別的的類別庫加載到虛擬機記憶體中,如rt.jar,識別僅按照檔案名識別,如果名字不符合,即使在這個目錄下,也不會被加載,啟動類加載器無法被java程式直接引,用戶如果在撰寫自定義的類加載器時,如果需要把加載請求委托給引導類加載器,那么直接用null代替即可,
2、擴展類加載器(Extension ClassLoader)
這個類加載器由sun.misc.Launcher $ExtClassLoader實作,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類別庫,
3、應用程式加載類(Application ClassLoader)
這個類加載器是由sun.misc.Launcher $App-ClassLoader實作,該加載器是由ClassLoader的getSystemClassLoader()方法回傳,所以一般稱它為系統類加載器,一般它加載用戶類路徑(ClassPath)所指定的類別庫,開發者一般直接使用這個類加載器,如果沒有定義自己的類加載器,那么這個應用程式加載類就是程式中默認的類加載器,
案例:手寫一個類加載器
1、撰寫一個java類 使用javac編譯生成class檔案

2、撰寫自定義類加載器
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path){
this.path = path;
}
//用于尋找類檔案
@Override
public Class findClass(String name){
byte[] b =loadClassData(name);
return defineClass(name,b,0,b.length);
}
public byte[] loadClassData(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1){
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
out.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
public static void main(String[] args)
throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyClassLoader classloader = new MyClassLoader("E:/project/");
Class c = classloader.loadClass("Test");
System.out.println(c.getClassLoader());
System.out.println(c.getClassLoader().getParent());
System.out.println(c.getClassLoader().getParent().getParent());
c.newInstance();
}
}
3、測驗結果

三 雙親委派機制
雙親委派機制作業程序:
如果一個類加載器收到了類加載器的請求.它首先不會自己去嘗試加載這個類.而是把這個請求委派給父加載器去完成.每個層次的類加載器都是如此.因此所有的加載請求最終都會傳送到Bootstrap類加載器(啟動類加載器)中.只有父類加載反饋自己無法加載這個請求(它的搜索范圍中沒有找到所需的類)時.子加載器才會嘗試自己去加載,
雙親委派模型的優點:
java類隨著它的加載器一起具備了一種帶有優先級的層次關系,例如類java.lang.Object,它存放在rt.jart之中.無論哪一個類加載器都要加載這個類.最終都是雙親委派模型最頂端的Bootstrap類加載器去加載.因此Object類在程式的各種類加載器環境中都是同一個類.相反.如果沒有使用雙親委派模型.由各個類加載器自行去加載的話.如果用戶撰寫了一個稱為“java.lang.Object”的類.并存放在程式的ClassPath中.那系統中將會出現多個不同的Object類.java型別體系中最基礎的行為也就無法保證.應用程式也將會一片混亂.
例如 當jvm要加載Test.class的時候,
(1)首先會到自定義加載器中查找,看是否已經加載過,如果已經加載過,則回傳位元組碼,
(2)如果自定義加載器沒有加載過,則詢問上一層加載器(即AppClassLoader)是否已經加載過Test.class,
(3)如果沒有加載過,則詢問上一層加載器(ExtClassLoader)是否已經加載過,
(4)如果沒有加載過,則繼續詢問上一層加載(BoopStrap ClassLoader)是否已經加載過,
(5)如果BoopStrap ClassLoader依然沒有加載過,則到自己指定類加載路徑下("sun.boot.class.path")查看是否有Test.class位元組碼,有則回傳,沒有通
知下一層加載器ExtClassLoader到自己指定的類加載路徑下(java.ext.dirs)查看,
(6)依次類推,最后到自定義類加載器指定的路徑還沒有找到Test.class位元組碼,則拋出例外ClassNotFoundException

ClassLoader的原始碼來看看雙親委派模式
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name); //檢查該類是否加載過了
if (c == null) {//沒加載過的情況
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果自定義的類加載器的parent不為null,就呼叫parent的loadClass進行加載類
c = parent.loadClass(name, false);
} else {
//否則就去找bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
二:JVM 記憶體模型組成部分
根據JVM規范共分為虛擬機堆疊,堆,方法區,程式計數器,本地方法堆疊五個部分

1、虛擬機堆疊
虛擬機堆疊是執行緒私有的記憶體空間,它和 Java 執行緒一起創建,當創建一個執行緒時,會在虛擬機堆疊中申請一個執行緒堆疊,用來保存方法的區域變數、運算元堆疊、動態鏈接方法和回傳地址等資訊,并參與方法的呼叫和回傳,每一個方法的呼叫都伴隨著堆疊幀的入堆疊操作,方法的回傳則是堆疊幀的出堆疊操作,可以這么理解,虛擬機堆疊針對當前 Java 應用中所有執行緒,都有一個其相應的執行緒堆疊,每一個執行緒堆疊都互相獨立、互不影響,里面存盤了該執行緒中獨有的資訊,
2、方法區
方法區與java堆一樣,是各個執行緒所共享的,它用來存盤已被虛擬機加載的類資訊、常量、靜態變數、即時編譯后的代碼等資料,方法區是jvm提出的規范,而永久代就是方法區的具體實作,java虛擬機對方法區的限制非常寬松,可以像堆一樣不需要連續的記憶體可可選擇的固定大小外,還可以選擇不識閑垃圾收集,相對而言,垃圾收集行為在這邊區域是比較少出現的,在方法區會報出 永久代記憶體溢位的錯誤,而java1.8為了解決這個問題,就提出了meta space(元空間)的概念,就是為了解決永久代記憶體溢位的情況,一般來說,在不指定 meta space大小的情況下,虛擬機方法區記憶體大小就是宿主主機的記憶體大小
3、程式計數器
程式計數器(Program Counter Register)是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器,在虛擬機的概念模型里(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實作),位元組碼解釋器作業時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、回圈、跳轉、例外處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成,由于Java 虛擬機的多執行緒是通過執行緒輪流切換并分配處理器執行時間的方式來實作的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內核)只會執行一條執行緒中的指令,因此,為了執行緒切換后能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間的計數器互不影響,獨立存盤,我們稱這類記憶體區域為“執行緒私有”的記憶體,如果執行緒正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機位元組碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則為空(Undefined),此記憶體區域是唯一一個在Java 虛擬機規范中沒有規定任何OutOfMemoryError 情況的區域
4、本地方法堆疊
與虛擬機堆疊發揮的作用非常類似,他們之間的區別是虛擬機堆疊為虛擬機執行java方法服務,而本地方法堆疊則為虛擬機使用到的native方法服務,與虛擬機堆疊一樣,本地方法堆疊也會拋出StackOverflowError,OutOfMemorryError例外,
5、堆
堆是 JVM 記憶體中最大的一塊記憶體空間,該記憶體被所有執行緒共享,幾乎所有物件和陣列都被分配到了堆記憶體中,堆被劃分為新生代和老年代,新生代又被進一步劃分為 Eden 區和 Survivor 區,最后 Survivor 由 From Survivor 和 To Survivor 組成,隨著 Java 版本的更新,其內容又有了一些新的變化:在 Java6 版本中,永久代在非堆記憶體區;到了 Java7 版本,永久代的靜態變數和運行時常量池被合并到了堆中;而到了 Java8,永久代被 元空間 (處于本地記憶體)取代了,運行時常量池是位于元空間中,string的實體是放在堆記憶體中
三:元空間(metaspace)

1、方法區與永久代,元空間之間的關系
方法區是一種規范,不同的虛擬機廠商可以基于規范做出不同的實作,永久代和元空間就是出于不同jdk版本的實作,說白了,方法區就像是一個介面,永久代與元空間分別是兩個不同的實作類而已,只不過永久代是這個介面最初的實作類,后來這個介面一直進行變更,直到最后徹底廢棄這個實作類,由新實作類——元空間進行替代,
2、永久代
PermGen space的全稱是Permanent Generation space,是指記憶體的永久保存區域,說說為什么會記憶體益出:這一部分用于存放Class和Meta的資訊,Class在被 Load的時候被放入PermGen space區域,它和和存放Instance的Heap區域不同,所以如果你的APP會LOAD很多CLASS的話,就很可能出現PermGen space錯誤,這種錯誤常見在web服務器對JSP進行pre compile的時候,
2.1、 持久代的大小
- 它的上限是MaxPermSize,默認是64M
- Java堆中的連續區域 : 如果存盤在非連續的堆空間中的話,要定位出持久代到新物件的參考非常復雜并且耗時,卡表(card table),是一種記憶集(Remembered Set),它用來記錄某個記憶體代中普通物件指標(oops)的修改,
- 持久代用完后,會拋出OutOfMemoryError "PermGen space"例外,解決方案:應用程式清理參考來觸發類卸載;增加MaxPermSize的大小,
- 需要多大的持久代空間取決于類的數量,方法的大小,以及常量池的大小
2.2、為什么移除持久代
- 它的大小是在啟動時固定好的——很難進行調優,-XX:MaxPermSize,設定成多少好呢?
- HotSpot的內部型別也是Java物件:它可能會在Full GC中被移動,同時它對應用不透明,且是非強型別的,難以跟蹤除錯,還需要存盤元資料的元資料資訊(meta-metadata),
- 簡化Full GC:每一個回收器有專門的元資料迭代器,
- 可以在GC不進行暫停的情況下并發地釋放類資料,
- 使得原來受限于持久代的一些改進未來有可能實作
永久代最終被移除,運行時常量池存在于記憶體的元空間中,字串常量移至Java Heap, PermSize 和 MaxPermSize 會被忽略并給出警告
3、metaspace元空間
元空間的本質和永久代類似,都是對JVM規范中方法區的實作,不過元空間與永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地記憶體,因此,默認情況下,元空間的大小僅受本地記憶體限制,但可以通過以下引數來指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行型別卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時,適當提高該值,
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的,
除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空間容量的百分比,減少為分配空間所導致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空間容量的百分比,減少為釋放空間所導致的垃圾收集
4、元空間替換永久代的原因
1、之前不管是不是需要,JVM都會吃掉那塊空間……如果設定得太小,JVM會死掉;如果設定得太大,這塊記憶體就被JVM浪費了,理論上說,現在你完全可以不關注這個,因為JVM會在運行時自動調校為“合適的大小”;
2、提高Full GC的性能,在Full GC期間,Metadata到Metadata pointers之間不需要掃描了;
3、隱患就是如果程式存在記憶體泄露,不停的擴展metaspace的空間,會導致機器的記憶體不足,所以還是要有必要的除錯和監控,
四:佇列和堆疊有什么區別
1、佇列
佇列是一種特殊的線性表,特殊之處在于它只允許在表的前端(front)進行洗掉操作,而在表的后端(rear)進行插入操作,和堆疊一樣,佇列是一種操作受限制的線性表,進行插入操作的端稱為隊尾,進行洗掉操作的端稱為隊頭,佇列采用的FIFO(first in first out),新元素(等待進入佇列的元素)總是被插入到鏈表的尾部,而讀取的時候總是從鏈表的頭部開始讀取,每次讀取一個元素,釋放一個元素,所謂的動態創建,動態釋放,因而也不存在溢位等問題,由于鏈表由結構體間接而成,遍歷也方便,(先進先出)
2、堆疊
堆疊(stack)又名堆疊,它是一種運算受限的線性表,其限制是僅允許在表的一端進行插入和洗掉運算,這一端被稱為堆疊頂,相對地,把另一端稱為堆疊底,堆疊就是一個桶,后放進去的先拿出來,它下面本來有的東西要等它出來之后才能出來(先進后出),堆疊(Stack)是作業系統在建立某個行程時或者執行緒(在支持多執行緒的作業系統中是執行緒)為這個執行緒建立的存盤區域,該區域具有FIFO的特性,在編譯的時候可以指定需要的Stack的大小,
兜兜轉轉大半輩子,才明白保持初心,無非就是自由自在地活著,
至于其他種種紛擾,當我們想明白、肯放下的時候,自然能找到與自己、與他人和解的辦法,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/281238.html
標籤:java
上一篇:java基本語法

