蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》

寫在開頭

從接觸 Java 開發到現在,大家對 Java 最直觀的印象是什么呢?是它宣傳的 “Write once, run anywhere”,還是目前看已經有些過于形式主義的語法呢?有沒有靜下心來仔細想過,對于 Java 到底了解到什么程度?
自從業以來,對于Java的那些紛紛擾擾的問題,我們或多或少都有些道不明,說不清的情緒,一直心有余悸,甚至困惑著我們的,可曾入夢,
是不是有著,不論查閱了多少遍的資料,以及翻閱了多少技術大咖的書籍,也未能解開心里那由來已久的疑惑,就像一個個未解之謎一般縈繞心扉,惶惶不可終日?
我一直都在問自己,一段Java代碼的中類,從撰寫到編譯,經過一系列的步驟加載到JVM,再到運行的程序,它究竟是如何運作和流轉的,其機制是什么?我們看到的結果究竟是如何呈現出來的,這其中發生了什么?
雖然,從學習Java之初,我們都會了解和記憶,以及在后來大家在提及的時候,大多數都是一句“我們應該都不陌生”,甚至“我相信大家都了然于心”之類話“蜻蜓點水”般輕描淡寫,
但是,如果真的要問一問的話,能詳細說道一二的,想必都會以“夏蟲不可語冰“的悲劇上演了吧!作為一名Java Develioer來說,正確了解和掌握這些原理和機制,早已經不是什么”不能說的秘密“,
帶著這些問題,今日我們便來扒一扒一個Java物件中的那些枝末細節,一個Java物件是如何被創建和執行的,我們又該如何理解和認識這些原理和機制,以及在日常開發作業中,我們需要注意些什么?
關健術語

本文用到的一些關鍵詞語以及常用術語,主要如下:
- 指標壓縮(CompressedOops) : 全稱為Compressed Ordinary Object Pointer,在HotSpot VM 64位(bit)虛擬機為了提升記憶體使用率而提出的指標壓縮技術,主要是指將Java程式中的所有物件參考指標壓縮一半,主要闡述的是一個指標大小占用一個字寬單位大小,即就是HotSpot VM 64位(bit)虛擬機的一個字寬單位大小是64bit,在實際作業時,原本的指標會壓縮成32bit,Oracle JDK從6 update 23開始在64位系統上開始支持開啟壓縮指標,在JDK1.7版本之后默認開啟,
- 指標碰撞(Bump the Pointer), 指的Java物件為分配堆記憶體的一種記憶體分配方式,其分配程序是把記憶體分為已分配記憶體和空間記憶體分別處于不同的一側,主要通過一個指標指向分界點區分,一般JVM為一個新物件分配記憶體的時候,把指標往往空閑記憶體區域移動指向相同物件大小的距離即可,一般適用于Serial和ParNew等不會產生記憶體碎片,且堆記憶體完整的收集器,
- 空閑串列(Clear Free List): 指的Java物件為分配堆記憶體的一種記憶體分配方式,其分配程序是把記憶體分為已分配記憶體和空間記憶體相互交錯,JVM通過維護一張記憶體串列記錄的可用空間記憶體塊,創建新物件需要分配堆記憶體時,從串列中尋找一個足夠大的記憶體塊分配給物件實體,同步更新串列記錄情況,當GC收集器發生GC時,把已回收的記憶體更新到記憶體串列,一般適用于CMS等會產生記憶體碎片,且堆記憶體不完整的收集器,
- 逃逸分析(Escape Analysis): 在編程語言的編譯優化原理中,分析指標動態范圍的方法稱之為逃逸分析,主要是判斷變數的作用域是否存在于其他記憶體堆疊或者執行緒中,當一個物件的指標被多個方法或執行緒參考時,我們稱這個指標發生了逃逸,其用來分析這種逃逸現象的方法,就稱之為逃逸分析,跟靜態代碼分析技術中的指標分析和外形分析類似,
- 標量替換(Scalar Replacement):主要是指使用標量替換聚合量(Java中的物件實體),把一個物件進行分解成一個個的標量進行逃逸分析,不可選的物件才能進行標量替換,標量主要是指不可分割的量,一般來說主要是基本資料型別和參考型別,
- 堆疊上分配(Allocation on Stack): 一般Java物件創建出來會在堆疊上進行記憶體分配,不是所有的物件都可以實作堆疊上分配,要想實作堆疊上分配,需要進行逃逸分析和標量替換,
基本概述

Java 本身是一種面向物件的語言,最顯著的特性有兩個方面,一是所謂的“書寫一次,到處運行”(Write once, run anywhere),能夠非常容易地獲得跨平臺能力;另外就是垃圾收集(GC, Garbage Collection),Java 通過垃圾收集器(Garbage Collector)回收分配記憶體,大部分情況下,程式員不需要自己操心記憶體的分配和回收,
我們日常會接觸到 JRE(Java Runtime Environment)或者 JDK(Java Development Kit), JRE,也就是 Java 運行環境,包含了 JVM 和 Java 類別庫,以及一些模塊等,而 JDK 可以看作是 JRE 的一個超集,提供了更多工具,比如編譯器、各種診斷工具等,
對于“Java 是解釋執行”這句話,這個說法不太準確,我們開發的 Java 的源代碼,首先通過 Javac 編譯成為位元組碼(bytecode),然后,在運行時,通過 Java 虛擬機(JVM)內嵌的解釋器將位元組碼轉換成為最終的機器碼,但是常見的 JVM,比如我們大多數情況使用的 Oracle JDK 提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)編譯器,也就是通常所說的動態編譯器,JIT 能夠在運行時將熱點代碼編譯成機器碼,這種情況下部分熱點代碼就屬于編譯執行,而不是解釋執行,
眾所周知,我們通常把 Java 分為編譯期和運行時,這里說的 Java 的編譯和 C/C++ 是有著不同的意義的,Javac 的編譯,編譯 Java 原始碼生成“.class”檔案里面實際是位元組碼,而不是可以直接執行的機器碼,Java 通過位元組碼和 Java 虛擬機(JVM)這種跨平臺的抽象,屏蔽了作業系統和硬體的細節,這也是實作“一次編譯,到處執行”的基礎,
1.Java原始碼分析
Java原始碼依據JDK提供的API來組織有效的代碼物體,一般都是通過呼叫API來編織和組成代碼的,

對于一段Java源代碼(Source Code)來說,要想正確被執行,需要先編譯通過,最后托管給所承載JVM,最終才被運行,
Java是一個主要思想是面向物件的,其中的Java的資料型別主要有基本資料型別和包裝型別別,其中:
- 基本資料型別(8大資料型別,其中void):byte、short、int、long、float、double、char、boolean、void
- 包裝型別別:Byte、Short、Integer、Long、Float、Double、Character、Boolean、Void
其中,資料型別主要是用來描述物件的基本特征和賦予功能屬性的一套語意分析規則,
一般來說Java原始碼的支持,會依據JDK提供的API來組織有效的代碼物體,對于源代碼的實作,通常我們都是通過呼叫API來編織和組成代碼的,
2.Java編譯機制
Java編譯機制主要可以分為編譯前端和編譯后端兩個階段,一般來說主要是指將源代碼翻譯為目標代碼的程序,稱為編譯程序,

編譯從一定意義上來說,根本上就是“翻譯”,指的計算機能否識別和認識,促成我們與計算機通信的作業機制,
Java整個編譯以及運行的程序相當繁瑣,總體來看主要有:詞法分析 --> 語法分析 --> 語意分析和中間代碼生成 --> 優化 --> 目標代碼生成,
具體來看,Java程式從源檔案創建到程式運行要經過兩大步驟,其中:
- 編譯前端:Java檔案會由編譯器編譯成class檔案(位元組碼檔案),會經過編譯原理簡單程序的前三步,屬于狹義的編譯程序,是將源代碼翻譯為中間代碼的程序,
- 編譯后端: 位元組碼由java虛擬機解釋運行,解釋執行即為目標代碼生成并執行,因此,Java程式既要編譯的同時也要經過JVM的解釋運行,屬于廣義的編譯程序,是將源代碼翻譯為機器代碼的程序,
從詳細分析來看,在編譯前端的階段,最重要的一個編譯器就是javac 編譯器, 在命令列執行javac命令,其實本質是運行了javac.exe這個應用,
而對于編譯后端的階段來說,最重要的是 運行期即時編譯器(JIT,Just in Time Compiler)和 靜態的提前編譯器(AOT,Ahead of Time Compiler),
特別指出,在Oracle JDK 9之前, Hotspot JVM 內置了兩個不同的 JIT compiler,其中:
- C1模式:屬于輕量級的Client編譯器,對應client 模式,編譯時間短,占用記憶體少,適用于對于啟動速度敏感的應用,比如普通 Java GUI 桌面應用,
- C2模式:屬于重量級的Server編譯器,對應 server 模式,執行效率高,大量編譯優化,它的優化是為長時間運行的服務器端應用設計的,適用于服務器,
但是,我們需要注意的是,默認是采用所謂的分層編譯(TieredCompilation),
在Oracle JDK 9之后,除了我們日常最常見的 Java 使用模式,其實還有一種新的編譯方式,即所謂的 AOT編譯,直接將位元組碼編譯成機器代碼,這樣就避免了 JIT 預熱等各方面的開銷,比如 Oracle JDK 9 就引入了實驗性的 AOT 特性,并且增加了新的 jaotc 工具,
3.Java類加載機制
Java類加載機制主要分為加載,驗證,準備,決議,初始化等5個階段,

當源代碼編譯完成之后,便是執行程序,其中需要一定的加載機制來幫助我們簡化流程,從Java HotSpot(TM)的執行模式上看,一般主要可以分為三種:
- 第一種:決議模式(Interpreted Mode)
Marklin:~ marklin$ java -Xint -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, interpreted mode)
Marklin:~ marklin$
- 第二種:編譯模式(Compiled Mode)
Marklin:~ marklin$ java -Xcomp -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, compiled mode)
Marklin:~ marklin$
- 第三種: 混合模式(Mixed Mode),主要是指編譯模式和決議模式的組合體
Marklin:~ marklin$ java -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode)
Marklin:~ marklin$
不論哪一種模式,只有在具體的使用場景上,Java HotSpot(TM)會依據系統環境自動選擇啟動引數,

在Java HotSpot(TM)中,JVM類加載機制分為五個部分:加載,驗證,準備,決議,初始化,其中:
- 加載:會在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的入口,
- 驗證: 確保Class檔案的位元組流中包含的資訊是否符合當前虛擬機的要求,并且不會危害虛擬機自身的安全,
- 準備: 正式為類變數分配記憶體并設定類變數的初始值階段,即在方法區中分配這些變數所使用的記憶體空間,
- 決議: 虛擬機將常量池中的符號參考替換為直接參考的程序,
- 初始化: 前面的類加載階段之后,除了在加載階段可以自定義類加載器以外,其它操作都由JVM主導,到了初始階段,才開始真正執行類中定義的Java程式代碼,
對于決議階段,我們需要理解符號參考和直接參考,其中:
- 符號參考: 符號參考與虛擬機實作的布局無關,參考的目標并不一定要已經加載到記憶體中,各種虛擬機實作的記憶體布局可以各不相同,但是它們能接受的符號參考必須是一致的,因為符號參考的字面量形式明確定義在Java虛擬機規范的Class檔案格式中,符號參考就是class檔案中主要包括CONSTANT_Class_info,CONSTANT_Field_info,CONSTANT_Method_info 等型別的常量,
- 直接參考: 是指向目標的指標,相對偏移量或是一個能間接定位到目標的句柄,如果有了直接參考,那參考的目標必定已經在記憶體中存在,
對于初始化階段來說,是執行類構造器 client方法的程序,其方法是由編譯器自動收集類中的類變數的賦值操作和靜態陳述句塊中的陳述句合并而成的,虛擬機會保證子類構造器 client方法執行之前,父類的類構造器 client方法已經執行完畢,如果一個類中沒有對靜態變數賦值也沒有靜態陳述句塊,那么編譯器可以不為這個類生成類構造器 client方法,
特別需要注意的是,以下幾種情況不會執行類初始化:
- 通過子類參考父類的靜態欄位,只會觸發父類的初始化,而不會觸發子類的初始化,
- 定義物件陣列,不會觸發該類的初始化,
- 常量在編譯期間會存入呼叫類的常量池中,本質上并沒有直接參考定義常量的類,不會觸發定義常量所在的類,
- 通過類名獲取Class物件,不會觸發類的初始化,
- 通過Class.forName加載指定類時,如果指定引數initialize為false時,也不會觸發類初始化,其實這個引數是告訴虛擬機,是否要對類進行初始化,
- 通過ClassLoader默認的loadClass方法,也不會觸發初始化動作,
在Java HotSpot(TM)虛擬機中,其加載動作放到JVM外部實作,以便讓應用程式決定如何獲取所需的類,主要提供了3種類加載器,其中:

- 啟動類加載器(Bootstrap ClassLoader):負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機認可(按檔案名識別,如rt.jar)的類,
- 擴展類加載器(Extension ClassLoader):負責加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變數指定路徑中的類別庫,
- 應用程式類加載器(Application ClassLoader): 負責加載用戶路徑(classpath)上的類別庫, JVM通過雙親委派模型進行類的加載,當然我們也可以通過繼承java.lang.ClassLoader實作自定義的類加載器,
當一個類收到了類加載請求,首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應該傳送到啟動類加載其中,只有當父類加載器反饋自己無法完成這個請求的時候,一般來說是指在它的加載路徑下沒有找到所需加載的Class,子類加載器才會嘗試自己去加載,

采用雙親委派的一個好處是比如加載位于rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都是委托給頂層的啟動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object物件,
由此可見,使用雙親委派之后,外部類想要替換系統JDK的類時,或者篡改其實作時,父類加載器已經加載過的,系統JDK子類加載器便不會再次加載,從而一定程度上防止了危險代碼的植入,
4.Java物件組成結構
Java物件(Object實體)結構主要包括物件頭、物件體和對齊位元組三部分,

在一個Java物件(Object Instance)中,主要包含物件頭(Object Header),物件體(Object Entry),以及對齊位元組(Byte Alignment)等內容,
換句話說,一個JAVA物件在記憶體中的存盤分布情況,其抽象成存盤結構,在Hotspot虛擬機中,物件在記憶體中的存盤布局分為 3 塊區域,其中:

- 物件頭(Object Header):物件頭部資訊,主要分為標記資訊欄位,類物件指標,以及陣列長度等三部分資訊,
- 物件體(Object Entry):物件體資訊,也叫作實體資料(Instance Data),主要包含物件的實體變數(成員變數),用于成員屬性值,包括父類的成員屬性值,這部分記憶體按4位元組對齊,
- 對齊位元組(Byte Alignment):也叫作填充對齊(Padding),其作用是用來保證Java物件所占記憶體位元組數為8的倍數HotSpot VM的記憶體管理要求物件起始地址必須是8位元組的整數倍,
一般來說,物件頭本身是填充對齊的參考指標是8的倍數,當物件的實體變數資料不是8的倍數時,便需要填充資料來保證8位元組的對齊,其中,對于物件頭來說:

- 標記資訊欄位(Mark Word): 主要存盤自身運行時的資料,例如GC標志位、哈希碼、鎖狀態等資訊, 用于表示物件的執行緒鎖狀態,另外還可以用來配合GC存放該物件的hashCode,
- 類物件指標(Class Pointer): 用于存放方法區Class物件的地址,虛擬機通過這個指標來確定這個物件是哪個類的實體,是指向方法區中Class資訊的指標,意味著該物件可隨時知道自己是哪個Class的實體,
- 陣列長度(Array Length): 如果物件是一個Java陣列,那么此欄位必須有,用于記錄陣列長度的資料;如果物件不是一個Java陣列,那么此欄位不存在,所以這是一個可選欄位,根據當前JVM的位數來決定,只有當本物件是一個陣列物件時才會有這個部分,
其次,對于物件體來說,用于保存物件屬性值,是物件的主體部分,占用的記憶體空間大小取決于物件的屬性數量和型別,
而對于對齊位元組來說,并不一定是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用,當物件實體資料部分沒有對齊(8位元組的整數倍)時,就需要通過對齊填充來補全,
特別指出,相對于物件結構中的欄位長度來說,其Mark Word、Class Pointer、Array Length欄位的長度都與JVM的位數息息相關,其中:
- 標記資訊欄位(Mark Word):欄位長度為JVM的一個Word(字)大小,也就是說32位JVM的Mark Word為32位,64位JVM的Mark Word為64位,
- 類物件指標(Class Pointer):欄位長度也為JVM的一個Word(字)大小,即32位JVM的Mark Word為32位,64位JVM的Mark Word為64位,
也就是說,在32位JVM虛擬機中,Mark Word和Class Pointer這兩部分都是32位的;在64位JVM虛擬機中,Mark Word和Class Pointer這兩部分都是64位的,
對于物件指標而言,如果JVM中的物件數量過多,使用64位的指標將浪費大量記憶體,通過簡單統計,64位JVM將會比32位JVM多耗費50%的記憶體,
為了節約記憶體可以使用選項UseCompressedOops來開啟/關閉指標壓縮,
其中,UseCompressedOops中的Oop為Ordinary Object Pointer(普通物件指標)的縮寫,
如果開啟UseCompressedOops選項,以下型別的指標將從64位壓縮至32位:
- Class物件的屬性指標(靜態變數)
- Object物件的屬性指標(成員變數)
- 普通物件陣列的元素指標,
當然,也不是所有的指標都會壓縮,一些特殊型別的指標不會壓縮,比如指向PermGen(永久代)的Class物件指標(JDK 8中指向元空間的Class物件指標)、本地變數、堆疊元素、入參、回傳值和NULL指
針等,
在堆記憶體小于32GB的情況下,64位虛擬機的UseCompressedOops選項是默認開啟的,該選項表示開啟Oop物件的指標壓碩訓將原來64位的Oop物件指標壓縮為32位,其中:
- 手動開啟Oop物件指標壓縮的Java指令為:
java -XX:+UseCompressedOops tagretClass<目標類>
- 手動關閉Oop物件指標壓縮的Java指令為:
java -XX:-UseCompressedOops tagretClass<目標類>
如果物件是一個陣列,那么物件頭還需要有額外的空間用于存盤陣列的長度(Array Length)欄位,
這也就意味著,Array Length欄位的長度也隨著JVM架構的不同而不同:在32位JVM上,長度為32位;在64位JVM上,長度為64位,
需要注意的是,在64位JVM如果開啟了Oop物件的指標壓縮,Array Length欄位的長度也將由64位壓縮至32位,
5.Java物件創建流程
Java物件創建流程主要分為物件實體化,類加載檢測,物件記憶體分配,值初始化,設定物件頭,執行初始化等6個步驟,

在了解完一個Java物件組成結構之后,我們便開始進入Java物件創建流程的剖析,掌握其本質有利于我們在實際開發作業中,可參考分析一段Java代碼的執行后,其在JVM中的產生的結果和影響,
從大致作業流程來看,可以分為物件實體化,類加載檢測,物件記憶體分配,值初始化,設定物件頭,執行初始化等6個步驟,其中:
- 物件實體化:一般在Java領域中指通過new關鍵字來實體化一個物件,在此之前Java HotSpot(TM) VM需要進行類加載檢測,
- 類加載檢測:進行類加載檢測,主要是檢測對應的符號參考是否被加載和初始化,最后才決定類是否可以被加載,
- 物件記憶體分配: 主要是指當類被加載完成之后,Java HotSpot(TM) VM會為其分配記憶體并開辟記憶體空間,根據情況來確定最終記憶體分配方案,
- 值初始化:根據Java HotSpot(TM) VM為其分配記憶體并開辟記憶體空間,來進行零值初始化,
- 設定物件頭: 完成值初始化之后,設定物件頭標記物件實體,
- 執行初始化: 執行初始化函式,一般是指類建構式,并為其設定相關屬性,
從Java物件創建流程的各個環節,具體詳細來看,其中:
首先,對于物件實體化來說,主要是看寫代碼時,用關鍵詞class定義一個類其實只是定義了一個類的模板,并沒有在記憶體中實際產生一個類的實體物件,也沒有分配記憶體空間,
而要想在記憶體中產生一個類的實體物件就需要使用相關方法申請分配記憶體空間,加上類的構造方法提供申請空間的大小規格,在記憶體中實際產生一個類的實體,一個類使用此類的構造方法,執行之后就在記憶體中分配了一個此類的記憶體空間,有了記憶體空間就可以向里面存放定義的資料和進行方法的呼叫,
在Java領域中,常見的Java物件實體化方式主要有:
- JDK提供的New 關健字:可以呼叫任意的建構式(無參的和帶引數的)創建物件,
- Class的newInstance()方法: 使用Class類的newInstance方法創建物件,其中,newInstance方法呼叫無參的建構式創建物件,
- Constructor的newInstance()方法: java.lang.reflect.Constructor類里也有一個newInstance方法可以創建物件,從而可以通過newInstance方法呼叫有引數的和私有的建構式,
- 實作Cloneable介面并實作其定義的clone()方法:呼叫一個物件的clone方法,jvm就會創建一個新的物件,將前面物件的內容全部拷貝進去,用clone方法創建物件并不會呼叫任何建構式,
- 反序列化的方式:當我們序列化和反序列化一個物件,jvm會給我們創建一個單獨的物件,在反序列化時,Java HotSpot(TM) VM創建物件并不會呼叫任何建構式,
其次,對于類加載檢測來說,當物件實體化之前,其Java HotSpot(TM) VM會自行進行檢測,主要是:
- 檢測物件實體化的指令是否在類的常量池資訊中定位到類的符號參考,
- 檢測符號參考是否被加載和初始化,倘若沒有的話便對類進行加載,
然而,對于物件記憶體分配來說,創建一個物件所需要的記憶體大小其實類加載完成就已經確定,記憶體分配主要是在堆中劃出一塊物件大小的對應記憶體,具體的分配方式依據堆記憶體的對齊方式來決定,而堆記憶體的對齊方式是根據當前程式的GC機制來決定的,
再者,對于值初始化來說,這只是依據Java HotSpot(TM) VM自動分配的記憶體對其進行初始化,并設定為零值,
接著,對于設定物件頭來說,就是對于每一個進入Java HotSpot(TM) VM的物件實體進行物件頭資訊設定,
最后,對于執行初始化來說,算是Java HotSpot(TM) VM真正意義上的執行,
6.Java物件記憶體分配機制
Java物件記憶體分配機制可以大致分為堆上分配,堆疊上分配,TLAB分配,以及年代區分配等方式,

一般來說,在理解Java物件記憶體分配機制之前,我們需要明確理解Java領域中的堆(Heap)與堆疊(Stack)概念,才能更好地掌握和清楚對應到相應的Java記憶體模型上去,主要是大多數時候,我們都是把這兩個結合起來講的,就是常說的“堆疊(Heap-Stack)“模型,其中:
- 堆(Heap): 用來存放程式動態生成的實體資料,是物件實體化(一般是指new)之后將其存盤,Java HotSpot(TM) VM會依據物件大小在Java Heap中為其開辟對應記憶體空間大小,
- 堆疊(Stack):用來存放基本資料型別和參考資料型別的實體,一般主要是指實體物件的在堆中的首地址,其中每一個執行緒都有自己的執行緒堆疊,被執行緒獨享,
因此,我們可以理解為堆記憶體和堆疊記憶體的概念,相對來說:
- 堆記憶體: 用于存盤java中的物件和陣列,當我們new一個物件或者創建一個陣列的時候,就會在堆記憶體中開辟一段空間給它,用于存放,堆記憶體的特點就是:先進先出,后進后出,堆可以動態地分配記憶體大小,生存期也不必事先告訴編譯器,因為它是在運行時動態分配記憶體的,但缺點是,由于要在運行時動態分配記憶體,存取速度較慢,由Java HotSpot(TM) VM虛擬機的自動垃圾回收器來管理,
- 堆疊記憶體: 主要是用來執行程式用的,堆疊記憶體的特點:先進后出,后進先出,存取速度比堆要快,僅次于暫存器,堆疊資料可以共享,但缺點是,存在堆疊中的資料大小與生存必須是確定的,缺乏靈活性,堆疊記憶體可以稱為一級快取,由垃圾回收器自動回收,
Java程式在Java HotSpot(TM) VM中運行時,從資料在記憶體區域的分布來看,大致可以分為執行緒私有區,執行緒共享區,直接記憶體等3大記憶體區域,其中 :

- 執行緒私有區(Thread Local): 執行緒私有資料主要是記憶體區域主要有程式計數器、虛擬機堆疊、本地方法區,該區域生命周期與執行緒相同, 依賴用戶執行緒的啟動/結束 而 創建/銷毀,
- 執行緒共享區(Thread Shared): 執行緒共享區的資料主要是JAVA 堆、方法區,其區域生命周期伴隨虛擬機的啟動/關閉而創建/銷毀,
- 直接記憶體(Direct Memory):非JVM運行時資料區的一部分, 但也會被頻繁的使用,不受Java HotSpot(TM) VM中GC控制,比如,在JDK 1.4引入的NIO提供了基于Channel與Buffer的IO方式, 它可以使用Native函式庫直接分配堆外記憶體, 然后使用DirectByteBuffer物件作為這塊記憶體的參考進行操作, 這樣就避免了在Java堆和Native堆中來回復制資料, 因此在一些場景中可以顯著提高性能,
由此可見,Java堆(Java Heap)是虛擬機所管理的記憶體中最大的一塊,Java堆是被所 有執行緒共享的一塊記憶體區域,在虛擬機啟動時創建,此記憶體區域的唯一目的就是存放物件實體,Java 世界里“幾乎”所有的物件實體都在這里分配記憶體,
對于物件記憶體分配來說,創建一個物件所需要的記憶體大小其實類加載完成就已經確定,記憶體分配主要是在堆中劃出一塊物件大小的對應記憶體,具體的分配方式依據堆記憶體的對齊方式來決定,而堆記憶體的對齊方式是根據當前程式的GC機制來決定的,
對于執行緒共享區的資料來說,常見的物件在堆記憶體分配主要有:
- 指標碰撞: 主要針對堆記憶體對齊的情況
- 空閑串列: 主要針對堆記憶體無法對齊的情況,相互交錯
- CAS自旋鎖和TLAB本地記憶體: 主要針對分配出現并發情況的解決方案
對于執行緒私有區的資料來說,常見的物件在堆記憶體分配原則主要有:
- 嘗試堆疊上分配:滿足堆疊上分配條件,進行堆疊上分配,否則進行嘗試TLAB分配,
- 嘗試TLAB分配:滿足TLAB分配條件,進行TLAB分配,否則進行嘗試老年代分配,
- 嘗試老年代分配:滿足老年代分配條件,進行老年代分配,否則嘗試新生代分配,
- 嘗試新生代分配:滿足新生代分配條件,進行新生代分配,
需要特別注意的是,不論是否能進行分配都是在Eden區進行分配的,主要是當出現多個執行緒同時創建一個物件的時候,TLAB分配做了優化,Java HotSpot(TM) VM虛擬機會在Eden區為其分配一塊共享空間給其執行緒使用,
Java物件成員初始化順序大致順序為靜態代碼快/靜態變數->非靜態代碼快/普通變數->一般類構造方法,其中:

按照Java程式代碼執行的順序來看,被static修飾的變數和代碼塊肯定是優先初始化的,其次結合繼承的思想,父類要比子類優先初始化,最后才是一般構造方法,
寫在最后

Java原始碼依據JDK提供的API來組織有效的代碼物體,一般都是通過呼叫API來編織和組成代碼的,
Java編譯機制主要可以分為編譯前端和編譯后端兩個階段,一般來說主要是指將源代碼翻譯為目標代碼的程序,稱為編譯程序,
Java類加載機制主要分為加載,驗證,準備,決議,初始化等5個階段,
Java物件(Object實體)結構主要包括物件頭、物件體和對齊位元組三部分,
Java物件記憶體分配機制可以大致分為堆上分配,堆疊上分配,TLAB分配,以及年代區分配等方式,
綜上所述,一個Java物件從創建到被托管給JVM時,會經歷和完成上面的一系列作業,
著作權宣告:本文為博主原創文章,遵循相關著作權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/503225.html
標籤:其他
上一篇:Java 并發編程決議 | 如何正確理解Java物件創建程序,我們主要需要注意些什么問題?
下一篇:設計模式之(4)——單例模式
