Java 併發編程解析 | 如何正確理解Java對象創建過程,我們主要需要注意些什麼問題?

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

Picture-Navigation

寫在開頭

Picture-Header

從接觸 Java 開發到現在,大家對 Java 最直觀的印象是什麼呢?是它宣傳的 “Write once, run anywhere”,還是目前看已經有些過於形式主義的語法呢?有沒有靜下心來仔細想過,對於 Java 到底瞭解到什麼程度?

自從業以來,對於Java的那些紛紛擾擾的問題,我們或多或少都有些道不明,說不清的情緒,一直心有餘悸,甚至困惑着我們的,可曾入夢。

是不是有着,不論查閱了多少遍的資料,以及翻閱了多少技術大咖的書籍,也未能解開心裏那由來已久的疑惑,就像一個個未解之謎一般縈繞心扉,惶惶不可終日?

我一直都在問自己,一段Java代碼的中類,從編寫到編譯,經過一系列的步驟加載到JVM,再到運行的過程,它究竟是如何運作和流轉的,其機制是什麼?我們看到的結果究竟是如何呈現出來的,這其中發生了什麼?

雖然,從學習Java之初,我們都會了解和記憶,以及在後來大家在提及的時候,大多數都是一句“我們應該都不陌生”,甚至“我相信大家都瞭然於心”之類話“蜻蜓點水”般輕描淡寫。

但是,如果真的要問一問的話,能詳細說道一二的,想必都會以“夏蟲不可語冰“的悲劇上演了吧!作爲一名Java Develioer來說,正確瞭解和掌握這些原理和機制,早已經不是什麼”不能說的祕密“。

帶着這些問題,今日我們便來扒一扒一個Java對象中的那些枝末細節,一個Java對象是如何被創建和執行的,我們又該如何理解和認識這些原理和機制,以及在日常開發工作中,我們需要注意些什麼?

關健術語

Picture-Keyword

本文用到的一些關鍵詞語以及常用術語,主要如下:

  • 指針壓縮(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對象創建出來會在棧上進行內存分配,不是所有的對象都可以實現棧上分配。要想實現棧上分配,需要進行逃逸分析和標量替換。

基本概述

Picture-Content

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來編織和組成代碼的。

v2LF0I.png

對於一段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-Compile

編譯從一定意義上來說,根本上就是“翻譯”,指的計算機能否識別和認識,促成我們與計算機通信的工作機制。

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個階段。

v2b5Md.png

當源代碼編譯完成之後,便是執行過程,其中需要一定的加載機制來幫助我們簡化流程,從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)會依據系統環境自動選擇啓動參數。

vhuZEd.png

在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種類加載器,其中:

vhuIqe.png

  • 啓動類加載器(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,子類加載器纔會嘗試自己去加載。

vhK9aj.png

採用雙親委派的一個好處是比如加載位於rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object對象。

由此可見,使用雙親委派之後,外部類想要替換系統JDK的類時,或者篡改其實現時,父類加載器已經加載過的,系統JDK子類加載器便不會再次加載,從而一定程度上防止了危險代碼的植入。

4.Java對象組成結構

Java對象(Object實例)結構主要包括對象頭、對象體和對齊字節三部分。

v2qFiT.png

在一個Java對象(Object Instance)中,主要包含對象頭(Object Header),對象體(Object Entry),以及對齊字節(Byte Alignment)等內容。

換句話說,一個JAVA對象在內存中的存儲分佈情況,其抽象成存儲結構,在Hotspot虛擬機中,對象在內存中的存儲佈局分爲 3 塊區域,其中:

vhknVx.png

  • 對象頭(Object Header):對象頭部信息,主要分爲標記信息字段,類對象指針,以及數組長度等三部分信息。
  • 對象體(Object Entry):對象體信息,也叫作實例數據(Instance Data),主要包含對象的實例變量(成員變量),用於成員屬性值,包括父類的成員屬性值。這部分內存按4字節對齊。
  • 對齊字節(Byte Alignment):也叫作填充對齊(Padding),其作用是用來保證Java對象所佔內存字節數爲8的倍數HotSpot VM的內存管理要求對象起始地址必須是8字節的整數倍。

一般來說,對象頭本身是填充對齊的參考指標是8的倍數,當對象的實例變量數據不是8的倍數時,便需要填充數據來保證8字節的對齊。其中,對於對象頭來說:

vhkJsA.png

  • 標記信息字段(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個步驟。

Picture-Content

在瞭解完一個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分配,以及年代區分配等方式。

v2qTl4.png

一般來說,在理解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大內存區域。其中 :

vhHFtx.png

  • 線程私有區(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對象成員初始化順序大致順序爲靜態代碼快/靜態變量->非靜態代碼快/普通變量->一般類構造方法,其中:

v2qL01.png

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

寫在最後

Picture-Footer

Java源碼依據JDK提供的API來組織有效的代碼實體,一般都是通過調用API來編織和組成代碼的。

Java編譯機制主要可以分爲編譯前端和編譯後端兩個階段,一般來說主要是指將源代碼翻譯爲目標代碼的過程,稱爲編譯過程。

Java類加載機制主要分爲加載,驗證,準備,解析,初始化等5個階段。

Java對象(Object實例)結構主要包括對象頭、對象體和對齊字節三部分。

Java對象內存分配機制可以大致分爲堆上分配,棧上分配,TLAB分配,以及年代區分配等方式。

綜上所述,一個Java對象從創建到被託管給JVM時,會經歷和完成上面的一系列工作。

版權聲明:本文爲博主原創文章,遵循相關版權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章