1 前言
在講java創(chuàng)建之前聪黎,我們先來了解下Java虛擬機(jī)內(nèi)存組成,當(dāng)Java虛擬機(jī)啟動(dòng)后备恤,會(huì)將系統(tǒng)分配給JVM的空間邏輯上劃分為堆稿饰、虛擬機(jī)棧、本地方法棧烘跺、方法區(qū)湘纵、程序計(jì)數(shù)器五個(gè)部分脂崔,如下圖所示:
堆:放置new出來的對象滤淳、數(shù)組
虛擬機(jī)棧:線程運(yùn)行前,會(huì)給其分配一個(gè)線程椘鲎螅空間脖咐,線程中每個(gè)方法執(zhí)行都會(huì)生成一個(gè)棧幀放入線程棧中,棧幀里面包含局部變量表汇歹、操作數(shù)棧屁擅、動(dòng)態(tài)連接和方法出口四部分。
????????局部變量表:存儲方法中的局部變量
????????操作數(shù)棧:用于賦值或者計(jì)算的數(shù)據(jù)
????????動(dòng)態(tài)鏈接:方法執(zhí)行的入口地址
????????方法出口:返回調(diào)用方法的地址
本地方法棧:與虛擬機(jī)棧類似产弹,是調(diào)用非java方法的棧
方法區(qū):存儲類元信息派歌、常量池
程序計(jì)數(shù)器:指向線程正在運(yùn)行的位置
2 Java對象創(chuàng)建
new一個(gè)對象的過程如上圖所示,依次執(zhí)行類加載檢查痰哨、分配內(nèi)存胶果、初始化零值、設(shè)置對象頭和執(zhí)行clinit五步斤斧。上述五步的作用分別如下:
類加載檢查:檢查對象對應(yīng)的class文件是否已被加載
分配內(nèi)存:在堆上或棧上分配內(nèi)存存儲對象
初始化零值:將分配的內(nèi)存賦零值
設(shè)置對象頭:在對象頭中設(shè)置對象運(yùn)行相關(guān)信息早抠、類指針、數(shù)組長度(是數(shù)組才有)
執(zhí)行clinit:賦值并執(zhí)行構(gòu)造函數(shù)
下面我們來詳細(xì)分析下每一步里面都分別做了什么撬讽。
2.1? 類加載檢查
創(chuàng)建一個(gè)對象之前蕊连,肯定需要知道該對象對應(yīng)類的相關(guān)信息,比如內(nèi)存要分配多少游昼、對象屬性賦值為多少甘苍。這些信息都存儲在編譯后的class文件中,所以首先需要將對象的class文件加載進(jìn)JVM內(nèi)存烘豌,當(dāng)創(chuàng)建該類的對象時(shí)载庭,需要什么信息就去對應(yīng)內(nèi)存中獲取。把class文件加載進(jìn)內(nèi)存的過程叫做JVM類加載,其中就涉及兩個(gè)問題昧捷,第一是誰來加載闲昭,第二是具體如何加載。下面我們就來理下這兩個(gè)問題靡挥。
2.1.1 誰來加載
這就要從運(yùn)行java程序開始講了序矩,現(xiàn)有MyMath.class,執(zhí)行java MyMath后跋破,大體過程如下圖所示簸淀。
執(zhí)行java MyMath后,java.exe會(huì)調(diào)用底層jvm.dll創(chuàng)建Java虛擬機(jī)和引導(dǎo)類加載器實(shí)例毒返,然后底層C++代碼會(huì)調(diào)用Java代碼創(chuàng)建JVM啟動(dòng)器實(shí)例Launcher租幕,其中會(huì)創(chuàng)建擴(kuò)展類加載器和應(yīng)用類加載器,在創(chuàng)建這兩個(gè)類加載器時(shí)拧簸,會(huì)將擴(kuò)展類加載器的父加載器賦值為引導(dǎo)類加載器(實(shí)際賦值為null劲绪,引導(dǎo)類加載器是在C++底層生成的,JVM里面獲取不到)盆赤,應(yīng)用類加載器的父加載器賦值為擴(kuò)展類加載器贾富。之后會(huì)獲取對應(yīng)的類加載器去加載class文件,一般該類加載器為應(yīng)用類加載器牺六,也可自定義類加載器加載類颤枪。具體代碼如下圖所示:
目前可以看到,JVM啟動(dòng)后會(huì)產(chǎn)生中三個(gè)類加載器淑际,分別是引導(dǎo)類加載器畏纲、擴(kuò)展類加載器、應(yīng)用類加載器春缕,這三個(gè)類加載器作用為:
????????引導(dǎo)類加載器:加載/JAVA_HOME/bin目錄下的類庫
????????擴(kuò)展類加載器:加載/JAVA_HOME/bin/ext目錄下的類庫
????????應(yīng)用類加載器:加載類路徑目錄下的類庫
可以看出這三個(gè)類加載器的分別加載不同的類庫盗胀,為什么JVM這樣設(shè)計(jì)呢?主要是基于安全的考慮淡溯,不允許隨意修改核心類庫读整,還有就是共用類庫加載一次就行,無需多次加載咱娶。為了實(shí)現(xiàn)上述效果米间,JVM類加載器還設(shè)計(jì)了雙親委派機(jī)制,具體流程如下圖所示:
當(dāng)加載一個(gè)類時(shí)膘侮,應(yīng)用類加載器會(huì)先判斷該類是否已被加載屈糊,如被加載則返回。如未被加載琼了,應(yīng)用類加載器不會(huì)直接加載而是委托父加載器去加載逻锐,直到啟動(dòng)類加載器夫晌。當(dāng)啟動(dòng)類加載器在其路徑下未找到該類文件,則交由其子類加載器去目錄下加載昧诱,直至加載成功晓淀。若最后應(yīng)用類加載器在其目錄下也為找到該類文件,則拋出異常盏档。
各類加載器分別加載不同目錄類庫和雙親委派機(jī)制解決了安全和重復(fù)加載的問題凶掰,但是隨著程序越來越復(fù)雜,會(huì)出現(xiàn)下面的場景蜈亩,tomcat部署多個(gè)應(yīng)用時(shí)懦窘,應(yīng)用可能會(huì)使用同一個(gè)類庫的不同版本。如果還是用上述三個(gè)類加載器和雙親委派機(jī)制稚配,一個(gè)類只能加載一次畅涂,最后會(huì)導(dǎo)致應(yīng)用不能正常使用。如果要滿足道川,就需要自定義類加載器和打破雙親委派機(jī)制(不向上委派就算打破)午衰。從雙親委派的代碼可以看到,打破雙親委派機(jī)制需要重寫loadClass()愤惰,自定義類加載器重寫findClass()即可苇经。Tomcat打破雙親委派的過程可以看下最后補(bǔ)充內(nèi)容,這里就不詳細(xì)講了宦言。找到了類被誰加載,下面就來講講具體加載過程商模。
2.1.2 如何加載
加載過程如下圖所示:
JVM完整的類加載需要經(jīng)歷加載奠旺、驗(yàn)證、準(zhǔn)備畸裳、解析舀射、初始化国撵、使用、卸載七個(gè)過程忿晕,其中驗(yàn)證、準(zhǔn)備银受、解析又稱為連接過程践盼。這幾個(gè)過程的作用分別如下:
? ??加載:找到class文件,并將其轉(zhuǎn)化為二進(jìn)制字符流宾巍,加載進(jìn)JVM虛擬機(jī)內(nèi)存中咕幻,
? ??驗(yàn)證:檢查二機(jī)制字符流是否符合JVM規(guī)范
? ??準(zhǔn)備:給靜態(tài)變量、常量分配內(nèi)存顶霞,靜態(tài)變量賦零值肄程,常量直接賦值
? ??解析:將符號引用轉(zhuǎn)化為直接引用
? ??初始化:給靜態(tài)變量賦值
class文件加載進(jìn)JVM內(nèi)存后,類元信息放在方法區(qū),會(huì)在堆內(nèi)生成一個(gè)類元指針蓝厌,指向方法區(qū)中的類元信息玄叠,是程序找到類信息的入口。目前為止拓提,類已經(jīng)加載進(jìn)JVM對應(yīng)內(nèi)存了诸典,那創(chuàng)建Java對象的第一步校驗(yàn)就通過了,下面就開始分配內(nèi)存了崎苗。
2.2 分配內(nèi)存
一般來說狐粱,對象的內(nèi)存都會(huì)分配在堆上,但為了減少垃圾回收的壓力胆数,JVM中的實(shí)際分配內(nèi)存如下圖所示:
new一個(gè)對象時(shí)肌蜻,會(huì)先判斷是否能進(jìn)行棧上分配,主要依賴于逃逸分析和標(biāo)量替換必尼,就是先判斷該對象是否會(huì)逃逸出當(dāng)前作用域蒋搜,被其他對象引用,如果不會(huì)逃逸出當(dāng)前作用域判莉,就會(huì)考慮棧上分配豆挽,如果此時(shí)棧上剩余內(nèi)存不夠就在堆上分配。如果棧內(nèi)存足夠券盅,但不連續(xù)帮哈,就會(huì)將對象進(jìn)行標(biāo)量替換,分解為不可再分的標(biāo)量锰镀,將其放在棧上的各個(gè)地方娘侍,會(huì)標(biāo)記哪些變量屬于同一個(gè)對象。以上都是考慮到該對象不會(huì)逃逸泳炉,那就會(huì)隨著出棧直接銷毀憾筏,減少GC壓力。但是不管是在棧上分配還是堆上分配內(nèi)存花鹅,都涉及如何具體分配以及避免并發(fā)的問題氧腰。目前JVM有指針碰撞和空閑列表兩種分配方式:
? ??指針碰撞:內(nèi)存分配規(guī)整,未分配內(nèi)存和已分配內(nèi)存中間有一個(gè)指針刨肃,該指針指向未分配內(nèi)存地址
? ??空閑列表:內(nèi)存分配不規(guī)整古拴,維護(hù)一個(gè)列表,存儲空閑內(nèi)存的地址
為解決分配過程中存在并發(fā)的問題之景,一般使用以下兩種方式:
? ??CAS+重試:通過該種方式將分配操作原子性
? ??TLAB:采用這種方式時(shí)斤富,線程啟動(dòng)時(shí)在堆上專門分配一塊內(nèi)存給線程存儲對象
2.3 初始化零值
將分配給對象的空間用零值將之前的數(shù)據(jù)覆蓋掉
2.4 設(shè)置對象頭
對象由對象頭、實(shí)例數(shù)據(jù)锻狗、對齊填充三部分組成的满力,前面已經(jīng)將實(shí)例數(shù)據(jù)的內(nèi)存空間賦為了零值焕参,現(xiàn)在就剩下對象頭了,對象頭包含信息如下圖所示:
對象頭=markword+Kclass指針+數(shù)組長度油额,具體里面包含的信息如圖上所示叠纷。其中注意Kclass指針是JVM虛擬機(jī)訪問類元信息的入口。我們自己寫的程序是無法使用到這個(gè)指針的潦嘶。我們使用反射用到的類元指針涩嚣,是加載類時(shí)生成的那個(gè)〉嘟可通過下面程序查看對象組成
```
package com.dailystudy.jvm;
import org.openjdk.jol.info.ClassLayout;
/***
* 計(jì)算對象大小
*/
public class JOLSample {
? ? //運(yùn)行需要加載jol-core.jar包
? ? //-XX:+UseCompressedOops 默認(rèn)開啟指針壓縮所有指針
? ? //-XX:+UseCompressedClassPointers 默認(rèn)開啟的只壓縮對象頭里的類型指針Klass Pointer
? ? //Ooops Ordinary Object Pointers
? ? public static class A{
? ? ? ? int id;
? ? ? ? String name;
? ? ? ? byte b;
? ? ? ? Object o;
? ? }
? ? public static void main(String[] args) {
? ? ? ? ClassLayout layout = ClassLayout.parseInstance(new Object());
? ? ? ? System.out.println(layout.toPrintable());
? ? ? ? System.out.println("-----------------------------");
? ? ? ? ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
? ? ? ? System.out.println(layout1.toPrintable());
? ? ? ? System.out.println("-----------------------------");
? ? ? ? ClassLayout layout2 = ClassLayout.parseInstance(new A());
? ? ? ? System.out.println(layout2.toPrintable());
? ? }
}
```
2.5 執(zhí)行clinit
對對象靜態(tài)數(shù)據(jù)進(jìn)行賦值航厚,并執(zhí)行構(gòu)造函數(shù),至此锰蓬,對象就生成完成了幔睬。后續(xù)程序就可以使用它進(jìn)行相應(yīng)操作,那對象無需使用的時(shí)候芹扭,如何銷毀它回收之前分配的內(nèi)存呢麻顶?
3 對象回收
我們都知道Java會(huì)幫我們自動(dòng)回收不用的內(nèi)存,而無需我們像C++一樣手動(dòng)釋放舱卡,那JVM到底是怎么做的呢辅肾?我們先來了解下JVM內(nèi)存回收大體機(jī)制,之前講到JVM內(nèi)存邏輯上會(huì)分為堆轮锥、虛擬機(jī)棧矫钓、本地方法棧、方法區(qū)交胚、程序計(jì)數(shù)器五個(gè)部分份汗,對于內(nèi)存回收來說,只會(huì)回收堆和方法區(qū)蝴簇,因?yàn)槎牙锩娣胖玫氖谴罅縩ew出來的對象,當(dāng)其不再使用時(shí)匆帚,就可以回收掉其內(nèi)存了熬词。方法區(qū)放的時(shí)大量的類元信息,當(dāng)一個(gè)類無需使用的時(shí)候吸重,也可將其進(jìn)行卸載回收空間互拾。JVM的設(shè)計(jì)者,基于經(jīng)驗(yàn)即程序生成的對象總是朝生夕死嚎幸,將堆內(nèi)存分為了老年代和年輕代颜矿,其比例一般為1:2,根據(jù)各自的特點(diǎn)分別采用不同的垃圾回收算法嫉晶。JVM堆內(nèi)存回收規(guī)則大概如下圖所示:
可以看到年輕代又分為了Eden區(qū)骑疆、S1區(qū)田篇、S2區(qū),其比例默認(rèn)是8:1:1箍铭。最開始對象一般都放置在Eden或者S1區(qū)(大對象除外泊柬,大對象會(huì)直接放入老年代),當(dāng)Eden和S1區(qū)放滿了后诈火,會(huì)觸發(fā)MinorGC兽赁,該次GC會(huì)回收掉Eden和S1區(qū)的垃圾對象,將存活對象移動(dòng)到S2區(qū)冷守,并將其存活對象分代年齡加1刀崖。然后新進(jìn)來的對象就都放置在Eden和S2區(qū),當(dāng)其滿了觸發(fā)MinorGC拍摇,也會(huì)跟著之前一樣回收垃圾對象亮钦,將存活對象放置在S1區(qū)同時(shí)分代年齡加一。當(dāng)分代年齡大于15時(shí)授翻,會(huì)將其從年輕代賦值到老年代或悲,當(dāng)老年代放滿后,會(huì)觸發(fā)FullGC堪唐,清理年輕代巡语、老年代、方法區(qū)的內(nèi)存(其中還涉及對象分代年齡判斷淮菠、老年代空間擔(dān)保機(jī)制)男公。
觸發(fā)MinorGC和FullGC會(huì)將所有用戶線程暫停,即產(chǎn)生STW現(xiàn)象合陵。一般MinorGC耗時(shí)較短枢赔,F(xiàn)ullGC還是較長。當(dāng)用戶使用應(yīng)用程序時(shí)拥知,STW會(huì)讓客戶產(chǎn)生卡頓的感覺踏拜,對于實(shí)時(shí)性較高的系統(tǒng),是無法忍受的低剔。所以減少FullGC的次數(shù)速梗,降低MinorGC頻次就成為了JVM調(diào)優(yōu)的重要目標(biāo)。對于方法區(qū)的回收要求較高襟齿,我這邊就簡單列一下姻锁,一般來說類卸載不會(huì)經(jīng)常發(fā)生。
該類所有的實(shí)例對象都已經(jīng)被回收猜欺,也就是Java堆中不存在該類的任何實(shí)例
加載該類的ClassLoader已經(jīng)被回收
該類對應(yīng)的java.lang.Class對象沒有在任何地方引用位隶,無法在任何地方通過反射訪問該類的方法
4 補(bǔ)充Tomcat打破雙親委派機(jī)制
tomcat是一個(gè)web容器,它需要解決什么問題呢开皿?
1 一個(gè)web容器可能需要部署不同的應(yīng)用涧黄,不同的應(yīng)用可能會(huì)依賴同一個(gè)類庫的不同版本篮昧,不能保證同一個(gè)類在同一個(gè)服務(wù)器中只有一份,因此要保證每個(gè)應(yīng)用程序的類庫都是獨(dú)立的弓熏,保證相互隔離恋谭。
2 部署在同一個(gè)web容器中相同的版本的類庫,只需加載一份共享
3 web容器的類庫與程序的類庫隔離開來
4 web支持JSP熱修改
第一個(gè)問題:雙親委派機(jī)制下同一個(gè)類只能加載一份挽鞠,所以需要打破雙親委派疚颊,使用web類加載器加載自己所需的類庫版本,無需向上委托信认。
第二個(gè)問題:默認(rèn)的類加載器機(jī)制可以實(shí)現(xiàn)
第三個(gè)問題:與第一個(gè)問題一樣
第四個(gè)問題:每個(gè)JSP文件就有一個(gè)類加載器材义,有一個(gè)線程,監(jiān)聽文件修改嫁赏,然后清空當(dāng)前類加載器其掂,賦值新的classLoader
目前Tomcat的實(shí)現(xiàn)機(jī)制如下:
如上圖,橙色部分還是和原來一樣潦蝇,采用雙親委派機(jī)制款熬,黃色部分是tomcat第一部分自定義的類加載器,這部分主要加載tomcat包中的類攘乒,這一部分依然采用的是雙親委派機(jī)制贤牛,而綠色部分是tomcat第二部分自定義類加載器,正是這一部分则酝,打破了類的雙親委派機(jī)制殉簸。
tomcat給每個(gè)web應(yīng)用創(chuàng)建一個(gè)類加載實(shí)例WebAppClassLoader,這個(gè)類中重寫了loadClass方法沽讹,讓其先加載當(dāng)前應(yīng)用目錄下的類般卑,如果找不到才向上委托。對于多個(gè)WEB可以共用的類爽雄,就可以放在同一目錄下蝠检,讓SharedClassLoader去加載。