一、Java內存布局
1环形、Java內部布局全貌
JVM包含兩個子系統(tǒng)和兩個組件:
- 兩個子系統(tǒng)為Class loader(類裝載)总寻、Execution engine(執(zhí)行引擎);
- 兩個組件為Runtime data area(運行時數(shù)據(jù)區(qū))犹褒、Native Interface(本地接口)抵窒。
各組件的功能大致如下:
- Class loader(類裝載):根據(jù)給定的全限定名類名(如:java.lang.Object)來裝載class文件到Runtime data area中的method area。
- Execution engine(執(zhí)行引擎):執(zhí)行classes中的指令叠骑。
- Native Interface(本地接口):與native libraries交互李皇,是其它編程語言交互的接口。
- Runtime data area(運行時數(shù)據(jù)區(qū)域):這就是我們常說的JVM的內存宙枷。
2掉房、Java內存模型工作機制
- 首先利用IDE集成開發(fā)工具編寫Java源代碼,源文件的后綴為.java慰丛;
- 再利用編譯器(javac命令)將源代碼編譯成字節(jié)碼文件卓囚,字節(jié)碼文件的后綴名為.class;
- 運行字節(jié)碼的工作是由解釋器(java命令)來完成的诅病。
- 接下來類加載器又將這些.class文件加載到JVM中
- 將類的.class文件中的二進制數(shù)據(jù)讀入到內存中哪亿,將其放在運行時數(shù)據(jù)區(qū)的方法區(qū)內粥烁,然后在堆區(qū)創(chuàng)建一個 java.lang.Class對象,用來封裝類在方法區(qū)內的數(shù)據(jù)結構蝇棉。
3讨阻、JVM 運行時數(shù)據(jù)區(qū)
Java 虛擬機在執(zhí)行 Java 程序的過程中會把它管理的內存劃分成若干個不同的數(shù)據(jù)區(qū)域。JDK. 1.8 和之前的版本略有不同篡殷,下面會介紹到变勇。
JDK 1.8 之前:
JDK 1.8 :
不同虛擬機的運行時數(shù)據(jù)區(qū)可能略微有所不同,但都會遵從 Java 虛擬機規(guī)范贴唇, Java 虛擬機規(guī)范規(guī)定的區(qū)域分為以下 5 個部分搀绣,其中線程私有程序計數(shù)器、虛擬機棧戳气、本地方法棧链患;線程共享堆、方法區(qū)瓶您、直接內存麻捻。
(1)程序計數(shù)器(Program Counter Register)
程序計數(shù)器是線程私有的屬性,其主要有兩個作用:
- 字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令呀袱,從而實現(xiàn)代碼的流程控制贸毕,如:順序執(zhí)行、選擇夜赵、循環(huán)明棍、異常處理。
- 在多線程的情況下寇僧,程序計數(shù)器用于記錄當前線程執(zhí)行的位置摊腋,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了
注意:程序計數(shù)器是唯一一個不會出現(xiàn) OutOfMemoryError 的內存區(qū)域,它的生命周期隨著線程的創(chuàng)建而創(chuàng)建嘁傀,隨著線程的結束而死亡兴蒸。
(2)Java 虛擬機棧(Java Virtual Machine Stacks)
與程序計數(shù)器一樣,Java 虛擬機棧也是線程私有的细办,Java 內存可以粗糙的區(qū)分為堆內存(Heap)和棧內存 (Stack),其中棧就是現(xiàn)在說的虛擬機棧橙凳,或者說是虛擬機棧中局部變量表部分。 Java虛擬機棧中存放局部變量表笑撞、操作數(shù)棧岛啸、動態(tài)鏈接、方法出口信息娃殖。
Java 虛擬機棧會出現(xiàn)兩種錯誤:StackOverFlowError 和 OutOfMemoryError值戳。
- StackOverFlowError: 若 Java 虛擬機棧的內存大小不允許動態(tài)擴展,那么當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候炉爆,就拋出 StackOverFlowError 錯誤。
- OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態(tài)擴展,且當線程請求棧時內存用完了芬首,無法再動態(tài)擴展了赴捞,此時拋出 OutOfMemoryError 錯誤。
(3)本地方法棧(Native Method Stack)
和虛擬機棧所發(fā)揮的作用非常相似郁稍,區(qū)別是: 虛擬機棧為虛擬機執(zhí)行 Java 方法 (也就是字節(jié)碼)服務赦政,而本地方法棧則為虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一耀怜。
方法執(zhí)行完畢后相應的棧幀也會出棧并釋放內存空間恢着,也會出現(xiàn) StackOverFlowError 和OutOfMemoryError 兩種錯誤。
(4)Java 堆(Java Heap)
Java 虛擬機所管理的內存中最大的一塊财破,線程共享掰派,此內存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數(shù)組都在這里分配內存左痢。Java 堆是垃圾收集器管理的主要區(qū)域靡羡,因此也被稱作GC 堆(Garbage Collected Heap)。
堆這里最容易出現(xiàn)的就是 OutOfMemoryError 錯誤俊性,并且出現(xiàn)這種錯誤之后的表現(xiàn)形式還會有幾種略步,比如:
-
OutOfMemoryError: GC Overhead Limit Exceeded
: 當JVM花太多時間執(zhí)行垃圾回收并且只能回收很少的堆空間時,就會發(fā)生此錯誤定页。 -
java.lang.OutOfMemoryError: Java heap space
:假如在創(chuàng)建新的對象時, 堆內存中的空間不足以存放新創(chuàng)建的對象, 就會引發(fā)java.lang.OutOfMemoryError: Java heap space
錯誤趟薄。(和本機物理內存無關,和你配置的內存大小有關典徊!)
(5)方法區(qū)(Methed Area)
線程共享的一塊區(qū)域竟趾,它用于存儲已被虛擬機加載的類信息、常量宫峦、靜態(tài)變量岔帽、即時編譯器編譯后的代碼等數(shù)據(jù)。
JDK 8 版本之后方法區(qū)(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了)导绷,取而代之是元空間犀勒,元空間使用的是直接內存。
(6)運行時常量池(Runtime Constant Pool)
用于存放編譯期生成的各種字面量和符號引用妥曲,當常量池無法再申請到內存時會拋出 OutOfMemoryError 錯誤贾费。
JDK1.7之前運行時常量池邏輯包含字符串常量池存放在方法區(qū), 此時hotspot虛擬機對方法區(qū)的實現(xiàn)為永久代
JDK1.7 字符串常量池被從方法區(qū)拿到了堆中, 這里沒有提到運行時常量池,也就是說字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區(qū), 也就是hotspot中的永久代 。
JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆, 運行時常量池還在方法區(qū), 只不過方法區(qū)的實現(xiàn)從永久代變成了元空間(Metaspace)
(7)直接內存(Direct Memory)
直接內存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分檐盟,也不是虛擬機規(guī)范中定義的內存區(qū)域褂萧,但是這部分內存也被頻繁地使用。而且也可能導致 OutOfMemoryError 錯誤出現(xiàn)葵萎。
JDK1.4 中新加入的 NIO(New Input/Output) 類导犹,引入了一種基于通道(Channel) 與緩存區(qū)(Buffer) 的 I/O 方式唱凯,它可以直接使用 Native 函數(shù)庫直接分配堆外內存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作谎痢。這樣就能在一些場景中顯著提高性能磕昼,因為避免了在 Java 堆和 Native 堆之間來回復制數(shù)據(jù)。
本機直接內存的分配不會受到 Java 堆的限制节猿,但是票从,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。
(8)補充:堆和棧的區(qū)別
- 物理地址:
- 堆的物理地址分配對對象是不連續(xù)的滨嘱。因此性能慢些峰鄙。
- 棧使用的是數(shù)據(jù)結構中的棧,先進后出的原則太雨,物理地址分配是連續(xù)的吟榴。所以性能快。
- 內存分配:
- 堆因為是不連續(xù)的躺彬,所以分配的內存是在
運行期
確認的煤墙,因此大小不固定。一般堆大小遠遠大于棧宪拥。 - 棧是連續(xù)的仿野,所以分配的內存大小要在
編譯期
就確認,大小是固定的她君。
- 堆因為是不連續(xù)的躺彬,所以分配的內存是在
- 存放的內容:
- 堆存放的是對象的實例和數(shù)組脚作。因此該區(qū)更關注的是數(shù)據(jù)的存儲
- 棧存放:局部變量,操作數(shù)棧缔刹,返回結果球涛。該區(qū)更關注的是程序方法的執(zhí)行。
- 靜態(tài)變量放在方法區(qū)校镐,靜態(tài)的對象還是放在堆亿扁。
- 程序的可見度:
- 堆對于整個應用程序都是共享、可見的鸟廓。
- 棧只對于線程是可見的从祝。所以也是線程私有。他的生命周期和線程相同引谜。
4牍陌、JVM中對象的創(chuàng)建過程
下圖便是 Java 對象的創(chuàng)建過程,圖后會詳細說明每一步的作用:
(1)類加載檢查
虛擬機遇到一條 new 指令時员咽,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到這個類的符號引用毒涧,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過贝室。如果沒有契讲,那必須先執(zhí)行相應的類加載過程仿吞。
(2)分配內存
在類加載檢查通過后,接下來虛擬機將為新生對象分配內存怀泊。對象所需的內存大小在類加載完成后便可確定茫藏,為對象分配空間的任務等同于把一塊確定大小的內存從 Java 堆中劃分出來误趴。
1)內存分配方式
分配方式有 “指針碰撞” 和 “空閑列表” 兩種霹琼,選擇那種分配方式由 Java 堆是否規(guī)整決定,而 Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定凉当。
2)內存分配并發(fā)問題
對象的創(chuàng)建在虛擬機中是一個非常頻繁的行為枣申,哪怕只是修改一個指針所指向的位置,在并發(fā)情況下也是不安全的看杭,可能出現(xiàn)正在給對象 A 分配內存忠藤,指針還沒來得及修改,對象 B 又同時使用了原來的指針來分配內存的情況楼雹。解決這個問題有兩種方案:
- 對分配內存空間的動作進行同步處理(采用 CAS + 失敗重試來保障更新操作的原子性)模孩;
- 把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在 Java 堆中預先分配一小塊內存贮缅,稱為本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)榨咐。哪個線程要分配內存,就在哪個線程的 TLAB 上分配谴供。只有 TLAB 用完并分配新的 TLAB 時块茁,才需要同步鎖。通過-XX:+/-UserTLAB參數(shù)來設定虛擬機是否使用TLAB桂肌。
(3)初始化零值
內存分配完成后数焊,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用崎场,程序能訪問到這些字段的數(shù)據(jù)類型所對應的零值佩耳。
(4)設置對象頭
初始化零值完成之后,虛擬機要對對象進行必要的設置谭跨,例如這個對象是那個類的實例干厚、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼饺蚊、對象的 GC 分代年齡等信息萍诱。 這些信息存放在對象頭中。 另外污呼,根據(jù)虛擬機當前運行狀態(tài)的不同裕坊,如是否啟用偏向鎖等,對象頭會有不同的設置方式燕酷。
(5)執(zhí)行初始化init方法
在上面工作都完成之后籍凝,從虛擬機的視角來看周瞎,一個新的對象已經產生了,但從 Java 程序的視角來看饵蒂,對象創(chuàng)建才剛開始声诸,在上面工作都完成之后,從虛擬機的視角來看退盯,一個新的對象已經產生了彼乌,但從 Java 程序的視角來看,對象創(chuàng)建才剛開始渊迁, 方法還沒有執(zhí)行慰照,所有的字段都還為零食绿。所以一般來說氮块,執(zhí)行 new 指令之后會接著執(zhí)行方法灾螃,把對象按照程序員的意愿進行初始化饱溢,這樣一個真正可用的對象才算完全產生出來狰住。 方法驼卖,把對象按照程序員的意愿進行初始化民假,這樣一個真正可用的對象才算完全產生出來意鲸。
5耕漱、對象的內存布局
在 Hotspot 虛擬機中算色,對象在內存中的布局可以分為 3 塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充孤个。
-
對象頭:對象頭分為
Mark Word
和Class Metadata Addresss
兩個部分剃允,Mark Word
存儲對象的hashCode、鎖信息或者分代年齡GC等標志等信息齐鲤。Class Metadata Addresss
存放指向類元數(shù)據(jù)的指針斥废,JVM通過這個指針確定該對象是那個類的實列。 - 實例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息给郊,包括父類的屬性信息牡肉,如果是數(shù)組的實例部分還包括數(shù)組的長度,這部分內存按4字節(jié)對齊
- 對齊填充:由于虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍淆九。填充數(shù)據(jù)不是必須存在的统锤,僅僅是為了字節(jié)對齊,這點了解即可炭庙。
Java對象內存布局中最重要的一塊應該就是對象頭中的Mark Word
部分了饲窿,他涉及到了hash值、鎖狀態(tài)焕蹄、分代年齡等許多非常重要的內容逾雄,下面就來詳細捋一下:
這部分主要用來存儲對象自身的運行時數(shù)據(jù),如hashCode、GC分代年齡等鸦泳。mark word
的位長度為JVM的一個Word大小银锻,也就是說32位JVM的Mark word
為32位,64位JVM為64位做鹰。為了讓一個字大小存儲更多的信息击纬,JVM將字的最低兩個位設置為標記位,不同標記位下的Mark Word示意如下:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
上面分別給出了32位和64位JVM中markword的區(qū)別钾麸,大致可以發(fā)現(xiàn)除了位數(shù)更振,基本上都是一樣了。
這里以32位JVM為例喂走,64位的情況下是一樣的殃饿,32位JVM一個字是32為谋作,64位JVM一個字是64為芋肠,JVM均用一個字的大小記錄當前Mark Word中的信息。
(1)Normal無所狀態(tài)
identity_hashcode
: 25位的對象標識Hash碼遵蚜,采用延遲加載技術帖池。調用方法System.identityHashCode()
計算,并會將結果寫到該對象頭中吭净。當對象被鎖定時睡汹,該值會移動到管程Monitor中。也就是說當對象加鎖之后寂殉,前25位將不再是對象的hashCode囚巴。age:
4位的Java對象年齡。在GC中友扰,如果對象在Survivor區(qū)復制一次彤叉,年齡增加1。當對象達到設定的閾值時村怪,將會晉升到老年代秽浇。默認情況下,并行GC的年齡閾值為15甚负,并發(fā)GC的年齡閾值為6柬焕。由于age只有4位,所以最大值為15梭域,這就是-XX:MaxTenuringThreshold
選項最大值為15的原因斑举。biased_lock:
對象是否啟用偏向鎖標記,只占1個二進制位病涨。為1時表示對象啟用偏向鎖富玷,為0時表示對象沒有偏向鎖。-
lock:
2位的鎖狀態(tài)標記位,由于希望用盡可能少的二進制位表示盡可能多的信息凌彬,所以設置了lock標記沸柔。該標記的值不同,整個mark word表示的含義不同铲敛。biased_lock lock· 狀態(tài) 0 01 無鎖 1 01 偏向鎖 0 00 輕量級鎖 0 10 重量級鎖 0 11 GC標記
(2)Biased偏向鎖狀態(tài)
-
thread:
23位表示持有偏向鎖的線程ID褐澎。 -
epoch:
2位,偏向時間戳,達到一定數(shù)量之后就會升級為輕量級鎖伐蒋。 -
age
工三、biased_loc
、lock
跟無鎖的狀態(tài)一致先鱼。
(3)Lightweight Locked輕量級鎖狀態(tài)
-
ptr_to_lock_record:
30位指向棧中鎖記錄的指針俭正。 - 剩下的兩位用于標志當前的鎖狀態(tài)
(4)Heavyweight Locked重量級鎖狀態(tài)
ptr_to_heavyweight_monitor:
30位指向管程Monitor的指針。剩下的兩位用于標志當前的鎖狀態(tài)
6焙畔、對象訪問定位
Java
程序需要通過 JVM
棧上的引用訪問堆中的具體對象掸读。對象的訪問方式取決于 JVM
虛擬機的實現(xiàn)。目前主流的訪問方式有 句柄 和 直接指針 兩種方式宏多。
指針: 指向對象儿惫,代表一個對象在內存中的起始地址。
句柄: 可以理解為指向指針的指針伸但,維護著對象的指針肾请。句柄不直接指向對象,而是指向對象的指針(句柄不發(fā)生變化更胖,指向固定內存地址)铛铁,再由對象的指針指向對象的真實內存地址。
(1)句柄訪問
Java
堆中劃分出一塊內存來作為句柄池却妨,引用中存儲對象的句柄地址饵逐,而句柄中包含了對象實例數(shù)據(jù)與對象類型數(shù)據(jù)各自的具體地址信息,具體構造如下圖所示:
優(yōu)勢:引用中存儲的是穩(wěn)定的句柄地址管呵,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針梳毙,而引用本身不需要修改。
(2)直接指針
如果使用直接指針訪問捐下,引用 中存儲的直接就是對象地址账锹,那么Java
堆對象內部的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關信息。
優(yōu)勢:速度更快坷襟,節(jié)省了一次指針定位的時間開銷奸柬。由于對象的訪問在Java
中非常頻繁,因此這類開銷積少成多后也是非秤こ蹋可觀的執(zhí)行成本廓奕。HotSpot 中采用的就是這種方式。
二、Java垃圾回收
垃圾回收主要就是防止JVM溢出而存在的一套JVM自動回收垃圾機制桌粉,主要需要弄明白下面幾個問題:
1蒸绩、內存如何分配和回收的
(1)Java內存分配模型
Java 的自動內存管理主要是針對對象內存的回收和對象內存的分配。同時铃肯,Java 自動內存管理最核心的功能是 堆 內存中對象的分配與回收患亿。
Java 堆是垃圾收集器管理的主要區(qū)域,因此也被稱作GC 堆(Garbage Collected Heap)押逼。從垃圾回收的角度步藕,由于現(xiàn)在收集器基本都采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代:再細致一點有:Eden 空間挑格、From Survivor咙冗、To Survivor 空間等。進一步劃分的目的是更好地回收內存漂彤,或者更快地分配內存雾消。
堆空間的基本結構:
(2)Java堆內存分配策略
- 在堆中,如果待分配的對象所需內存大于eden區(qū)大小显歧,那么將直接送入老年代仪或。
- 如果eden區(qū)可以放下,會經歷一下的分配過程:
- 對象都會首先在 Eden 區(qū)域分配
- 在一次新生代垃圾回收后士骤,如果對象還存活,則會進入 s1("To")蕾域,并且對象的年齡還會加 1(初始為0)
- 當它的年齡增加到一定程度(默認為 15 歲)拷肌,就會被晉升到老年代中。
- 經過這次GC后旨巷,Eden區(qū)和"From"區(qū)已經被清空巨缘。這個時候,"From"和"To"會交換他們的角色采呐,也就是新的"To"就是上次GC前的“From”若锁,新的"From"就是上次GC前的"To"(下文再詳細闡述)
(3)新生代內存中,為什么要有Survivor區(qū)域
- 如果沒有Survivor斧吐,Eden區(qū)每進行一次Minor GC(發(fā)生在新生代的垃圾回收)又固,存活的對象就會被送到老年代。老年代很快被填滿煤率,觸發(fā)Major GC(因為Major GC一般伴隨著Minor GC仰冠,也可以看做觸發(fā)了Full GC)。老年代的內存空間遠大于新生代蝶糯,進行一次Full GC消耗的時間比Minor GC長得多洋只。你也許會問,執(zhí)行時間長有什么壞處?頻發(fā)的Full GC消耗的時間是非呈缎椋可觀的肢扯,這一點會影響大型程序的執(zhí)行和響應速度,更不要說某些連接會因為超時發(fā)生連接錯誤了担锤。
- 從上面可以看出來鹃彻,Survivor區(qū)域帶來的最大的優(yōu)勢就是防止老年代被很快填滿,從而增大老年代垃圾回收時間上的浪費
好妻献,那我們來想想在沒有Survivor的情況下蛛株,有沒有什么解決辦法,可以避免上述情況:
- 增加老年代空間 育拨,更多存活對象才能填滿老年代谨履。降低Full GC頻率 隨著老年代空間加大,一旦發(fā)生Full GC熬丧,執(zhí)行所需要的時間更長
- 減少老年代空間 Full GC所需時間減少 老年代很快被存活對象填滿笋粟,F(xiàn)ull GC頻率增加。顯而易見析蝴,沒有Survivor的話害捕,上述兩種解決方案都不能從根本上解決問題。
(4)為什么要設置兩個Survivor區(qū)
這個問題也就是復制算法的原理闷畸,堆中新生代采用的就是復制算法尝盼,下面來看一下它的魅力:
設置兩個Survivor區(qū)最大的好處就是解決了碎片化,我們來分析一下:
1)首先使用單個Survivor區(qū)
剛剛新建的對象在Eden中佑菩,一旦Eden滿了盾沫,觸發(fā)一次Minor GC,Eden中的存活對象就會被移動到Survivor區(qū)殿漠。這樣繼續(xù)循環(huán)下去赴精,下一次Eden滿了的時候,問題來了绞幌,此時進行Minor GC蕾哟,Eden和Survivor各有一些存活對象,如果此時把Eden區(qū)的存活對象硬放到Survivor區(qū)莲蜘,很明顯這兩部分對象所占有的內存是不連續(xù)的谭确,也就導致了內存碎片化。
碎片化帶來的風險是極大的菇夸,嚴重影響JAVA程序的性能琼富。堆空間被散布的對象占據(jù)不連續(xù)的內存,最直接的結果就是庄新,堆中沒有足夠大的連續(xù)內存空間鞠眉,接下去如果程序需要給一個內存需求很大的對象分配內存薯鼠,就會造成很可怕的結果。下面用圖簡單說明一下只有一個Survivor會出現(xiàn)什么樣的情況
- 第一次GC發(fā)生之前械蹋,Survivor區(qū)為空出皇, GC過后, Eden清空哗戈, Survivor填上
- 第二次GC的時候郊艘,Survivor區(qū)域中有部分被標記清除,Eden又加入了一些新的元素唯咬, 那么繼續(xù)清空如下纱注,可以看到此輪GC過后,Survivor區(qū)因為此輪也清楚了兩個紅點胆胰,所以產生了兩個碎片狞贱,而Eden區(qū)新加入的四個綠點緊跟著之前的Survivor加入
- 第三次GC的時候,同樣的Eden區(qū)域和Survivor區(qū)域都產生了要回收的對象蜀涨,看看這會出現(xiàn)了什么樣的情況
經過上面的GC瞎嬉,可以看到最終再Survivor區(qū)出現(xiàn)了大量的碎片,那么向解決這個問題的最好的方式就是使用兩個Survivor區(qū)厚柳。
2)使用兩個Survivor區(qū)
咱們再來看看氧枣,用兩個Survivor會出現(xiàn)什么樣的情況。
-
首先第一次GC别垮,將Eden區(qū)紅色全部干掉便监,綠色全部扔到from里面區(qū)
雙No2
- 這里簡單看一下第二次GC是怎么進行操作的,首先Eden區(qū)和from區(qū)這個時候又出現(xiàn)了許多紅點(帶清理的對象)宰闰,這次JVM的操作就不是直接再Survivor區(qū)域上將對象干掉茬贵,這次他會收先將from區(qū)域里面的紅點全部干點,然后剩余的綠點順位進入to區(qū)域移袍,eden區(qū)域同樣按這個原理清空, 放入to區(qū)域之后老充,再將from區(qū)域和to區(qū)域交換葡盗,始終保證to區(qū)域是空閑的狀態(tài),這樣就可以非常完美的解決碎片化問題啡浊。
2觅够、哪些垃圾需要回收
堆中幾乎放著所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)巷嚣。
(1)怎么判斷對象已死亡
判斷對象是否已經死亡通常有兩個方法喘先,引用計數(shù)法和可達性分析算法
1)引用計數(shù)法
給對象中添加一個引用計數(shù)器,每當有一個地方引用它廷粒,計數(shù)器就加 1窘拯;當引用失效红且,計數(shù)器就減 1;任何時候計數(shù)器為 0 的對象就是不可能再被使用的涤姊。
這個方法實現(xiàn)簡單暇番,效率高,但是目前主流的虛擬機中并沒有選擇這個算法來管理內存思喊,其最主要的原因是它很難解決對象之間相互循環(huán)引用的問題壁酬。
2)可達性分析算法
這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節(jié)點開始向下搜索恨课,節(jié)點所走過的路徑稱為引用鏈舆乔,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的剂公。
(2)四種引用是怎么進行垃圾回收的
JDK1.2 以后希俩,Java 對引用的概念進行了擴充,將引用分為強引用诬留、軟引用斜纪、弱引用、虛引用四種(引用強度逐漸減弱)
1)強引用(StrongReference)
有強引用的對象文兑,垃圾回收器絕不會回收它盒刚,當內存空間不足,Java 虛擬機寧愿拋出 OutOfMemoryError 錯誤绿贞,使程序異常終止因块,也不會靠隨意回收具有強引用的對象來解決內存不足問題。
2)軟引用(SoftReference)
如果內存空間足夠籍铁,垃圾回收器就不會回收它涡上,如果內存空間不足了,就會回收這些對象的內存拒名。常用作于告訴緩存
3)弱引用(WeakReference)
在垃圾回收器線程掃描它所管轄的內存區(qū)域的過程中吩愧,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內存空間足夠與否增显,都會回收它的內存雁佳。
4)虛引用(PhantomReference)
與其他幾種引用都不同,虛引用并不會決定對象的生命周期同云。如果一個對象僅持有虛引用糖权,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收炸站。虛引用的用途是在 gc 時返回一個通知星澳。
特別注意,在程序設計中一般很少使用弱引用與虛引用旱易,使用軟引用的情況較多禁偎,這是因為軟引用可以加速 JVM 對垃圾內存的回收速度腿堤,可以維護系統(tǒng)的運行安全,防止內存溢出(OutOfMemory)等問題的產生届垫。
(3)如何判斷一個常量是廢棄常量
假如在常量池中存在字符串 "abc"释液,如果當前沒有任何 String 對象引用該字符串常量的話,就說明常量 "abc" 就是廢棄常量装处,如果這時發(fā)生內存回收的話而且有必要的話误债,"abc" 就會被系統(tǒng)清理出常量池。
(4)如何判斷一個類是無用的類
類需要同時滿足下面 3 個條件才能算是 “無用的類” :
- 該類所有的實例都已經被回收妄迁,也就是 Java 堆中不存在該類的任何實例寝蹈。
- 加載該類的 ClassLoader 已經被回收。
- 該類對應的 java.lang.Class 對象沒有在任何地方被引用登淘,無法在任何地方通過反射訪問該類的方法箫老。
虛擬機可以對滿足上述 3 個條件的無用類進行回收,這里說的僅僅是“可以”黔州,而并不是和對象一樣不使用了就會必然被回收耍鬓。
3、什么時候回收
(1)分代垃圾回收器工作流程
分代回收器有兩個分區(qū):老生代和新生代流妻,新生代默認的空間占比總空間的 1/3牲蜀,老生代的默認占比是 2/3。
新生代使用的是復制算法绅这,新生代里有 3 個分區(qū):Eden涣达、To Survivor、From Survivor证薇,它們的默認占比是 8:1:1度苔,它的執(zhí)行流程如下:
- 把 Eden + From Survivor 存活的對象放入 To Survivor 區(qū);
- 清空 Eden 和 From Survivor 分區(qū)浑度;
- From Survivor 和 To Survivor 分區(qū)交換寇窑,F(xiàn)rom Survivor 變 To Survivor,To Survivor 變 From Survivor箩张。
每次在 From Survivor 到 To Survivor 移動時都存活的對象疗认,年齡就 +1,當年齡到達 15(默認配置是 15)時伏钠,升級為老生代。大對象也會直接進入老生代谨设。
老生代當空間占用到達某個值之后就會觸發(fā)全局垃圾收回熟掂,一般使用標記整理的執(zhí)行算法。以上這些循環(huán)往復就構成了整個分代垃圾回收的整體執(zhí)行流程扎拣。
(2)Minor GC和Major GC內存回收策略
多數(shù)情況赴肚,對象都在新生代 Eden 區(qū)分配素跺。當 Eden 區(qū)分配沒有足夠的空間進行分配時,虛擬機將會發(fā)起一次 Minor GC誉券。如果本次 GC 后還是沒有足夠的空間指厌,則將啟用分配擔保機制在老年代中分配內存。
這里我們提到 Minor GC踊跟,如果你仔細觀察過 GC 日常踩验,通常我們還能從日志中發(fā)現(xiàn) Major GC/Full GC。
- Minor GC 是指發(fā)生在新生代的 GC商玫,因為 Java 對象大多都是朝生夕死箕憾,所有 Minor GC 非常頻繁,一般回收速度也非橙快袭异;
- Major GC/Full GC 是指發(fā)生在老年代的 GC,出現(xiàn)了 Major GC 通常會伴隨至少一次 Minor GC炬藤。Major GC 的速度通常會比 Minor GC 慢 10 倍以上胡岔。
(3)不可達的對象并非“非死不可”
- 即使在可達性分析法中不可達的對象,也并非是“非死不可”的缺脉,這時候它們暫時處于“緩刑階段”短荐,要真正宣告一個對象死亡,至少要經歷兩次標記過程细睡;
- 可達性分析法中不可達的對象被第一次標記并且進行一次篩選谷羞,篩選的條件是此對象是否有必要執(zhí)行 finalize 方法。
- 當對象沒有覆蓋 finalize 方法溜徙,或 finalize 方法已經被虛擬機調用過時湃缎,虛擬機將這兩種情況視為沒有必要執(zhí)行。
- 被判定為需要執(zhí)行的對象將會被放在一個隊列中進行第二次標記蠢壹,除非這個對象與引用鏈上的任何一個對象建立關聯(lián)嗓违,否則就會被真的回收。
(4)可以主動通知虛擬機進行垃圾回收嗎
可以图贸。程序員可以手動執(zhí)行System.gc()蹂季,通知GC運行,但是Java語言規(guī)范并不保證GC一定會執(zhí)行疏日。
4偿洁、如何回收
(1)垃圾收集算法
1)標記清除算法
該算法分為“標記”和“清除”階段:首先比較出所有需要回收的對象,在標記完成后統(tǒng)一回收掉所有被標記的對象沟优。它是最基礎的收集算法涕滋,后續(xù)的算法都是對其不足進行改進得到。這種垃圾收集算法會帶來兩個明顯的問題:
- 效率問題
- 空間問題(標記清除后會產生大量不連續(xù)的碎片)
2)復制算法
為了解決效率問題挠阁,“復制”收集算法出現(xiàn)了宾肺。它可以將內存分為大小相同的兩塊溯饵,每次使用其中的一塊。當這一塊的內存使用完后锨用,就將還存活的對象復制到另一塊去丰刊,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區(qū)間的一半進行回收增拥。
3)標記整理算法
根據(jù)老年代的特點提出的一種標記算法啄巧,標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象回收跪者,而是讓所有存活的對象向一端移動棵帽,然后直接清理掉端邊界以外的內存。
4)分代收集算法
虛擬機的垃圾收集都采用分代收集算法渣玲,這種算法沒有什么新的思想逗概,只是根據(jù)對象存活周期的不同將內存分為幾塊。一般將 java 堆分為新生代和老年代忘衍,這樣我們就可以根據(jù)各個年代的特點選擇合適的垃圾收集算法逾苫。
- 在新生代使用復制算法
- 在老年代使用標記整理算法
(2)垃圾收集器
如果說收集算法是內存回收的方法論枚钓,那么垃圾收集器就是內存回收的具體實現(xiàn)。
下圖展示了7種作用于不同分代的收集器星掰,其中用于回收新生代的收集器包括Serial、PraNew嫩舟、Parallel Scavenge家厌,回收老年代的收集器包括Serial Old播玖、Parallel Old、CMS蜀踏,還有用于回收整個Java堆的G1收集器果覆。不同收集器之間的連線表示它們可以搭配使用随静。
-
Serial收集器(復制算法): 新生代單線程收集器燎猛,標記和清理都是單線程重绷,優(yōu)點是簡單高效昭卓;
Serial 收集器
-
ParNew收集器 (復制算法): 新生代收并行集器候醒,實際上是Serial收集器的多線程版本倒淫,在多核CPU環(huán)境下有著比Serial更好的表現(xiàn)敌土;
ParNew 收集器
- Parallel Scavenge收集器 (復制算法): 新生代并行收集器返干,追求高吞吐量矩欠,高效利用 CPU癌淮。吞吐量 = 用戶線程時間/(用戶線程時間+GC線程時間)该默,高吞吐量可以高效率的利用CPU時間栓袖,盡快完成程序的運算任務裹刮,適合后臺應用等對交互相應要求不高的場景捧弃;
Serial Old收集器 (標記-整理算法): 老年代單線程收集器违霞,Serial收集器的老年代版本买鸽;
Parallel Old收集器 (標記-整理算法): 老年代并行收集器眼五,吞吐量優(yōu)先看幼,Parallel Scavenge收集器的老年代版本诵姜;
CMS(Concurrent Mark Sweep)收集器(標記-清除算法): 老年代并行收集器茅诱,它非常符合在注重用戶體驗的應用上使用翎卓,以獲取最短回收停頓時間為目標的收集器失暴,具有高并發(fā)逗扒、低停頓的特點矩肩,追求最短GC回收停頓時間黍檩。
G1(Garbage First)收集器 (標記-整理算法): Java堆并行收集器刽酱,G1收集器是JDK1.7提供的一個新收集器棵里,G1收集器基于“標記-整理”算法實現(xiàn)殿怜,也就是說不會產生內存碎片稳捆。此外乔夯,G1收集器不同于之前的收集器的一個重要特點是:G1回收的范圍是整個Java堆(包括新生代,老年代)甲脏,而前六種收集器回收的范圍僅限于新生代或老年代块请。
因為CMS和G1收集器相對比較特殊墩新,下面單獨介紹一下他們
(3)CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器海渊。它非常符合在注重用戶體驗的應用上使用臣疑。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的并發(fā)收集器讯沈,它第一次實現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時工作缺狠。
從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種 “標記-清除”算法實現(xiàn)的驮樊,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些囚衔。整個過程分為四個步驟:
- 初始標記: 暫停所有的其他線程练湿,并記錄下直接與 root 相連的對象肥哎,速度很快 篡诽;
- 并發(fā)標記: 同時開啟 GC 和用戶線程朱浴,用一個閉包結構去記錄可達對象翰蠢。但在這個階段結束梁沧,這個閉包結構并不能保證包含當前所有的可達對象趁尼。因為用戶線程可能會不斷的更新引用域酥泞,所以 GC 線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發(fā)生引用更新的地方悯姊。
- 重新標記: 重新標記階段就是為了修正并發(fā)標記期間因為用戶程序繼續(xù)運行而導致標記產生變動的那一部分對象的標記記錄悯许,這個階段的停頓時間一般會比初始標記階段的時間稍長先壕,遠遠比并發(fā)標記階段時間短
-
并發(fā)清除: 開啟用戶線程垃僚,同時 GC 線程開始對未標記的區(qū)域做清掃栽燕。
CMS 垃圾收集器
從它的名字就可以看出它是一款優(yōu)秀的垃圾收集器,主要優(yōu)點:并發(fā)收集付秕、低停頓。但是它有下面三個明顯的缺點:
- 對 CPU 資源敏感亮元;
- 無法處理浮動垃圾爆捞;
- 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。
(4)G1收集器
G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征.
被視為 JDK1.7 中 HotSpot 虛擬機的一個重要進化特征成肘。它具備一下特點:
- 并行與并發(fā):G1 能充分利用 CPU双霍、多核環(huán)境下的硬件優(yōu)勢洒闸,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執(zhí)行的 GC 動作深纲,G1 收集器仍然可以通過并發(fā)的方式讓 java 程序繼續(xù)執(zhí)行囤萤。
- 分代收集:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆澄惊,但是還是保留了分代的概念掸驱。
- 空間整合:與 CMS 的“標記--清理”算法不同毕贼,G1 從整體來看是基于“標記整理”算法實現(xiàn)的收集器;從局部上來看是基于“復制”算法實現(xiàn)的待秃。
- 可預測的停頓:這是 G1 相對于 CMS 的另一個大優(yōu)勢章郁,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外培廓,還能建立可預測的停頓時間模型医舆,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內蔬将。
G1 收集器的運作大致分為以下幾個步驟:
- 初始標記
- 并發(fā)標記
- 最終標記
- 篩選回收
G1 收集器在后臺維護了一個優(yōu)先列表,每次根據(jù)允許的收集時間毙石,優(yōu)先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)滞时。這種使用 Region 劃分內存空間以及有優(yōu)先級的區(qū)域回收方式坪稽,保證了 G1 收集器在有限時間內可以盡可能高的收集效率(把內存化整為零)窒百。
三、類的生命周期
1渤滞、類完整的生命周期
一個類的完整生命周期如下:
Class 文件需要加載到虛擬機中之后才能運行和使用,那么虛擬機是如何加載這些 Class 文件呢?
系統(tǒng)加載 Class 類型的文件主要三步:加載->連接->初始化论咏。連接過程又可分為三步:驗證->準備->解析。
下面開始一步一步分析類加載的過程:
2养涮、加載階段
類的加載過程主要完成三件事:
- 通過全類名獲取定義此類的二進制字節(jié)流(獲取.class文件的字節(jié)流)
- 將字節(jié)流上面所代表的靜態(tài)存儲結構轉換為方法區(qū)的運行時數(shù)據(jù)結構
- 在內存中生成一個代表該類的Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口
值得注意的是悄谐,加載階段和連接階段的部分內容是交叉進行的爬舰,加載階段尚未結束坪仇,連接階段可能就已經開始了椅文。
3、驗證階段
驗證階段主要就是對文件格式,元數(shù)據(jù)林说,字節(jié)碼以及符號應用的一些驗證腿箩,個人感覺跟編譯檢查一樣的工作
4珠移、準確階段
準備階段是正式為類變量分配內存并設置類變量初始值的階段钧惧,這些內存都將在方法區(qū)中分配。對于該階段有以下幾點需要注意:
這時候進行內存分配的僅包括類變量(static)乾颁,而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中巴席。
這里所設置的初始值"通常情況"下是數(shù)據(jù)類型默認的零值(如0荧库、0L分衫、null、false等)邀桑,比如我們定義了
public static int value=111
壁畸,那么 value 變量在準備階段的初始值就是 0 而不是111(初始化階段才會賦值)。特殊情況:比如給 value 變量加上了 fianl 關鍵字public static final int value=111
令杈,那么準備階段 value 的值就被賦值為 111。
5给赞、解析階段
虛擬機將常量池中的符號引用替換成直接引用的過程残邀。符號引用就理解為一個標示,而在直接引用直接指向內存中的地址芥挣;
符號引號和直接引用的區(qū)別:
符號引用(Symbolic References):
- 符號引用以一組符號來描述所引用的目標驱闷,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可空免。例如空另,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info扼菠、CONSTANT_Methodref_info等類型的常量出現(xiàn)摄杂。
- 符號引用與虛擬機的內存布局無關,引用的目標并不一定加載到內存中循榆。在Java中析恢,一個java類將會編譯成一個class文件。在編譯時秧饮,java類并不知道所引用的類的實際地址映挂,因此只能使用符號引用來代替。比如org.simple.People類引用了org.simple.Language類盗尸,在編譯時People類并不知道Language類的實際內存地址柑船,因此只能使用符號org.simple.Language(假設是這個,當然實際中是由類似于CONSTANT_Class_info的常量來表示的)來表示Language類的地址泼各。
- 各種虛擬機實現(xiàn)的內存布局可能有所不同鞍时,但是它們能接受的符號引用都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規(guī)范的Class文件格式中历恐。
直接引用(Direct References):
直接指向目標的指針(比如寸癌,指向“類型”【Class對象】、類變量弱贼、類方法的直接引用可能是指向方法區(qū)的指針)
相對偏移量(比如蒸苇,指向實例變量、實例方法的直接引用都是偏移量)
一個能間接定位到目標的句柄
直接引用是和虛擬機的布局相關的吮旅,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同溪烤。如果有了直接引用,那引用的目標必定已經被加載入內存中了庇勃。
6檬嘀、初始化階段
對靜態(tài)變量和靜態(tài)代碼塊執(zhí)行初始化工作。
初始化是類加載的最后一步责嚷,也是真正執(zhí)行類中定義的 Java 程序代碼(字節(jié)碼)鸳兽,初始化階段是執(zhí)行類構造器 方法的過程。對于構造方法的調用罕拂,虛擬機會自己確保其在多線程環(huán)境中的安全性揍异。因為構造方法是帶鎖線程安全,所以在多線程環(huán)境下進行類初始化的話可能會引起死鎖爆班,并且這種死鎖很難被發(fā)現(xiàn)衷掷。
對于初始化階段,虛擬機嚴格規(guī)范了有且只有5種情況下柿菩,必須對類進行初始化(只有主動去使用類才會初始化類):
(1)當遇到 new 戚嗅、 getstatic、putstatic或invokestatic 這4條直接碼指令時,比如 new 一個類懦胞,讀取一個靜態(tài)字段(未被 final 修飾)替久、或調用一個類的靜態(tài)方法時。
- 當jvm執(zhí)行new指令時會初始化類医瘫。即當程序創(chuàng)建一個類的實例對象侣肄。
- 當jvm執(zhí)行getstatic指令時會初始化類。即程序訪問類的靜態(tài)變量(不是靜態(tài)常量醇份,常量會被加載到運行時常量池)稼锅。
- 當jvm執(zhí)行putstatic指令時會初始化類。即程序給類的靜態(tài)變量賦值僚纷。
- 當jvm執(zhí)行invokestatic指令時會初始化類矩距。即程序調用類的靜態(tài)方法。
(2)使用 java.lang.reflect
包的方法對類進行反射調用時如Class.forname("..."),newInstance()等等怖竭。 锥债,如果類沒初始化,需要觸發(fā)其初始化痊臭。
(3)初始化一個類哮肚,如果其父類還未初始化,則先觸發(fā)該父類的初始化广匙。
(4)當虛擬機啟動時允趟,用戶需要定義一個要執(zhí)行的主類 (包含 main 方法的那個類),虛擬機會先初始化這個類鸦致。
(5)MethodHandle和VarHandle可以看作是輕量級的反射調用機制潮剪,而要想使用這2個調用, 就必須先使用findStaticVarHandle來初始化要調用的類分唾。
7抗碰、卸載過程
卸載類即該類的Class對象被GC。
卸載類需要滿足3個要求:
- 該類的所有的實例對象都已被GC绽乔,也就是說堆不存在該類的實例對象弧蝇。
- 該類沒有在其他任何地方被引用
- 該類的類加載器的實例已被GC
所以,在JVM生命周期類折砸,由jvm自帶的類加載器加載的類是不會被卸載的看疗。但是由我們自定義的類加載器加載的類是可能被卸載的。
只要想通一點就好了鞍爱,jdk自帶的BootstrapClassLoader,PlatformClassLoader,AppClassLoader負責加載jdk提供的類,所以它們(類加載器的實例)肯定不會被回收专酗。而我們自定義的類加載器的實例是可以被回收的睹逃,所以使用我們自定義加載器加載的類是可以被卸載掉的。
四、類加載器
1沉填、有哪幾種類加載器
JVM 中內置了三個重要的 ClassLoader疗隶,除了 BootstrapClassLoader 其他類加載器均由 Java 實現(xiàn)且全部繼承自java.lang.ClassLoader
:
-
BootstrapClassLoader(啟動類加載器) :最頂層的加載類,由C++實現(xiàn)翼闹,負責加載
%JAVA_HOME%/lib
目錄下的jar包和類或者或被-Xbootclasspath
參數(shù)指定的路徑中的所有類斑鼻。 -
ExtensionClassLoader(擴展類加載器) :主要負責加載目錄
%JRE_HOME%/lib/ext
目錄下的jar包和類,或被java.ext.dirs
系統(tǒng)變量所指定的路徑下的jar包猎荠。 - AppClassLoader(應用程序類加載器) :面向我們用戶的加載器坚弱,負責加載當前應用classpath下的所有jar包和類。
- 用戶自定義類加載器:通過繼承 java.lang.ClassLoader類的方式實現(xiàn)关摇。
2荒叶、什么是雙親委派模型
每一個類都有一個對應它的類加載器。系統(tǒng)中的 ClassLoder 在協(xié)同工作的時候會默認使用 雙親委派模型 输虱。
- 即在類加載的時候些楣,系統(tǒng)會首先判斷當前類是否被加載過。已經被加載的類會直接返回宪睹,否則才會嘗試加載愁茁。
- 加載的時候,首先會把該請求委派該父類加載器的
loadClass()
處理亭病,因此所有的請求最終都應該傳送到頂層的啟動類加載器BootstrapClassLoader
中鹅很。 - 當父類加載器無法處理時,才由自己來處理命贴。當父類加載器為null時道宅,會使用啟動類加載器
BootstrapClassLoader
作為父類加載器。
雙親委派模型
概括的說胸蛛,雙親委派模型就是如果一個類加載器收到了類加載的請求污茵,它首先不會自己去加載這個類,而是把這個請求委派給父類加載器去完成葬项,每一層的類加載器都是如此泞当,這樣所有的加載請求都會被傳送到頂層的啟動類加載器中,只有當父加載無法完成加載請求(它的搜索范圍中沒找到所需的類)時民珍,子加載器才會嘗試去加載類襟士。
3、雙親委派模型的好處
- 雙親委派模型保證了Java程序的穩(wěn)定運行嚷量,可以避免類的重復加載(JVM 區(qū)分不同類的方式不僅僅根據(jù)類名陋桂,相同的類文件被不同的類加載器加載產生的是兩個不同的類),
-
也保證了 Java 的核心 API 不被篡改蝶溶。如果沒有使用雙親委派模型嗜历,而是每個類加載器加載自己的話就會出現(xiàn)一些問題宣渗,比如我們編寫一個稱為
java.lang.Object
類的話,那么程序運行的時候梨州,系統(tǒng)就會出現(xiàn)多個不同的Object
類痕囱。
五、JVM調優(yōu)
待補充
參考:
https://blog.csdn.net/ThinkWon/article/details/104390752
https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/jvm/Java內存區(qū)域.md
https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/jvm/類加載過程.md