主頁 > 軟體設計 > Java 并發編程決議 | 如何正確理解Java物件創建程序,我們主要需要注意些什么問題?

Java 并發編程決議 | 如何正確理解Java物件創建程序,我們主要需要注意些什么問題?

2022-08-31 09:19:15 軟體設計

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

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時,會經歷和完成上面的一系列作業,

著作權宣告:本文為博主原創文章,遵循相關著作權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/503224.html

標籤:架構設計

上一篇:初識設計模式 - 工廠模式

下一篇:Java 并發編程決議 | 如何正確理解Java物件創建程序,我們主要需要注意些什么問題?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more