Java對象創(chuàng)建回收全過程

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去加載。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挚瘟,一起剝皮案震驚了整個(gè)濱河市蝇率,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刽沾,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件排拷,死亡現(xiàn)場離奇詭異侧漓,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)监氢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門布蔗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來藤违,“玉大人,你說我怎么就攤上這事纵揍《倨梗” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵泽谨,是天一觀的道長璧榄。 經(jīng)常有香客問我,道長吧雹,這世上最難降的妖魔是什么骨杂? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮雄卷,結(jié)果婚禮上搓蚪,老公的妹妹穿的比我還像新娘。我一直安慰自己丁鹉,他們只是感情好妒潭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揣钦,像睡著了一般雳灾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拂盯,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天佑女,我揣著相機(jī)與錄音,去河邊找鬼谈竿。 笑死团驱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的空凸。 我是一名探鬼主播嚎花,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼呀洲!你這毒婦竟也來了紊选?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤道逗,失蹤者是張志新(化名)和其女友劉穎兵罢,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滓窍,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡卖词,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吏夯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片此蜈。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡即横,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出裆赵,到底是詐尸還是另有隱情东囚,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布战授,位于F島的核電站页藻,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏陈醒。R本人自食惡果不足惜惕橙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钉跷。 院中可真熱鬧弥鹦,春花似錦、人聲如沸爷辙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膝晾。三九已至栓始,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間血当,已是汗流浹背幻赚。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留臊旭,地道東北人落恼。 一個(gè)月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像离熏,于是被迫代替她去往敵國和親佳谦。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容