1. JVM--Java運(yùn)行時數(shù)據(jù)區(qū)

        共 6505字,需瀏覽 14分鐘

         ·

        2020-12-06 01:45

        1. java運(yùn)行時數(shù)據(jù)區(qū)域


        ?????? Java 虛擬機(jī)在執(zhí)行Java程序的過程中會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。這些數(shù)據(jù)區(qū)域有各自的用途,以及創(chuàng)建和銷毀的時間,有的內(nèi)存區(qū)域隨著虛擬機(jī)進(jìn)程啟動而一直存在,有些區(qū)域則是依賴用戶線程的啟動和結(jié)束而建立和銷毀。Java虛擬機(jī)所管理的內(nèi)存主要包括以下幾個運(yùn)行時數(shù)據(jù)區(qū)域:




        1.1 程序計(jì)數(shù)器

        ? ? ? ?程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。在Java虛擬機(jī)的概念模型里,字節(jié)碼解釋器工作時就是通過改變這個計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,它是程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計(jì)數(shù)器來完成。
        ??????由于Java虛擬機(jī)中多線程是通過輪流切換、分配處理執(zhí)行時間的方式來實(shí)現(xiàn)的。在任何時刻,一個處理器只能執(zhí)行一條線程中的指令,即每個線程都有獨(dú)立的線程計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲,所以程序計(jì)數(shù)器是線程私有的。
        ? ? ? ?如果線程正在執(zhí)行的是一個Java方法,這個計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地 址;如果正在執(zhí)行的是本地(Native)方法,這個計(jì)數(shù)器值則應(yīng)為空(Undefined)。此內(nèi)存區(qū)域是唯 一一個在《Java虛擬機(jī)規(guī)范》中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。


        1.2 Java虛擬機(jī)棧

        ???????虛擬機(jī)棧描述的是Java方法執(zhí)行的線程內(nèi)存模型;與程序計(jì)數(shù)器一樣,也是屬于線程私有的,生命周期與線程相同;每個方法被執(zhí)行的時候,java虛擬機(jī)都會同步創(chuàng)建一個棧幀,用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。一個方法被調(diào)用直至執(zhí)行完畢的過程,就對應(yīng)一個棧幀在虛擬機(jī)中入棧到出棧的過程。
        ????局部變量表存放了編譯期可知的各種Java虛擬機(jī)基本數(shù)據(jù)類型(boolean、int、double、float、char、byte、long、short)、對象引用和returnAddress類型。
        ???????這些數(shù)據(jù)類型在局部變量表中的存儲空間以局部變量槽來表示,其中64位長度的long和double類型的數(shù)據(jù)會占用兩個變量槽,其余的數(shù)據(jù)類型只占用一個。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個方法時,這個方法需要在棧幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會改變變量表的大小。
        ???????在java虛擬機(jī)規(guī)范中,對虛擬機(jī)棧這個區(qū)域規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果虛擬機(jī)??梢詣討B(tài)擴(kuò)展,如果擴(kuò)展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常。


        1.3 本地方法棧

        ???????本地方法棧與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只有虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù)。而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)。
        ????????本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常。


        1.4 Java堆

        ? ? ? ?對于大多數(shù)應(yīng)用來說,Java堆是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動時創(chuàng)建,此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。所有的對象實(shí)例以及數(shù)組都在堆上分配內(nèi)存。但是隨著JIT編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會導(dǎo)致一些微妙的變化發(fā)生,所有的對象都分配在堆上也漸漸變得不那么絕對了。

        ???????java堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱作GC堆。從內(nèi)存回收的角度來看,由于現(xiàn)在手機(jī)器基本都采用分代收集算法,所以Java堆中可以細(xì)分為:新生代、老年代;在細(xì)致一點(diǎn)可以分為Eden空間、From Survivor空間、To Survivor空間等。


        ? ? ? ?如果從分配內(nèi)存的角度看,所有線程共享的Java堆中可以劃分出多個線程私有的分配緩沖區(qū) (Thread Local Allocation Buffer,TLAB),以提升對象分配時的效率。不過無論從什么角度,無論如何劃分,都不會改變Java堆中存儲內(nèi)容的共性,無論是哪個區(qū)域,存儲的都只能是對象的實(shí)例,將Java 堆細(xì)分的目的只是為了更好地回收內(nèi)存,或者更快地分配內(nèi)存。
        ? ? ? ?根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的,這點(diǎn)就像我們用磁盤空間去存儲文件一樣,并不要求每個文件都連續(xù)存放。但對于大對象(典型的如數(shù)組對象),多數(shù)虛擬機(jī)實(shí)現(xiàn)出于實(shí)現(xiàn)簡單、存儲高效的考慮,很可能會要求連續(xù)的 內(nèi)存空間。Java堆既可以被實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過當(dāng)前主流的Java虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過參數(shù)-Xmx和-Xms設(shè)定)。如果在Java堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時,Java虛擬機(jī)將會拋出OutOfMemoryError異常。


        1.5 方法區(qū)

        ? ? ? ?方法區(qū)與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即使編譯器編譯后的代碼等數(shù)據(jù)。
        ? ? ? ?對于習(xí)慣在HotSpot虛擬機(jī)上開發(fā)、部署程序的開發(fā)者來說,很多人都更愿意把方法區(qū)成為“永久代”,本質(zhì)上兩者并不等價,僅僅是因?yàn)镠ostSpot虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)選擇把GC分代收集擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分內(nèi)存,能夠省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作。
        ??????《Java虛擬機(jī)規(guī)范》對方法區(qū)的約束是非常寬松的,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選 擇固定大小或者可擴(kuò)展外,甚至還可以選擇不實(shí)現(xiàn)垃圾收集。相對而言,垃圾收集行為在這個區(qū)域的 確是比較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就如永久代的名字一樣“永久”存在了。這區(qū)域的內(nèi)存回 收目標(biāo)主要是針對常量池的回收和對類型的卸載,一般來說這個區(qū)域的回收效果比較難令人滿意,尤 其是類型的卸載,條件相當(dāng)苛刻,但是這部分區(qū)域的回收有時又確實(shí)是必要的。以前Sun公司的Bug列 表中,曾出現(xiàn)過的若干個嚴(yán)重的Bug就是由于低版本的HotSpot虛擬機(jī)對此區(qū)域未完全回收而導(dǎo)致內(nèi)存 泄漏。
        ? ? ? ?根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,如果方法區(qū)無法滿足新的內(nèi)存分配需求時,將拋出 OutOfMemoryError異常。


        1.6 運(yùn)行時常量池

        ? ? ? ?運(yùn)行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時常量池中。
        ? ? ? ?運(yùn)行時常量池相對于Class文件常量池的另外一個重要特征是具備動態(tài)性,Java語言并不要求常量 一定只有編譯期才能產(chǎn)生,也就是說,并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時常 量池,運(yùn)行期間也可以將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是String類的 intern()方法。
        ? ? ? ?既然運(yùn)行時常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請到內(nèi)存 時會拋出OutOfMemoryError異常。


        1.7 直接內(nèi)存

        ? ? ? ? ?直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中 定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。

        ?在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū) (Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java堆里面的 DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能,因?yàn)楸苊饬?在Java堆和Native堆中來回復(fù)制數(shù)據(jù)。
        ? ? ? ? 顯然,本機(jī)直接內(nèi)存的分配不會受到Java堆大小的限制,但是,既然是內(nèi)存,則肯定還是會受到 本機(jī)總內(nèi)存(包括物理內(nèi)存、SWAP分區(qū)或者分頁文件)大小以及處理器尋址空間的限制,一般服務(wù) 器管理員配置虛擬機(jī)參數(shù)時,會根據(jù)實(shí)際內(nèi)存去設(shè)置-Xmx等參數(shù)信息,但經(jīng)常忽略掉直接內(nèi)存,使得 各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級的限制),從而導(dǎo)致動態(tài)擴(kuò)展時出現(xiàn) OutOfMemoryError異常。


        2. HotSpot虛擬機(jī)對象探秘


        2.1 對象創(chuàng)建

        ? ? ? Java是一門面向?qū)ο蟮木幊陶Z言,Java程序運(yùn)行過程中無時無刻都有對象被創(chuàng)建出來。在語言層面 上,創(chuàng)建對象通常(例外:復(fù)制、反序列化)僅僅是一個new關(guān)鍵字而已,而在虛擬機(jī)中,對象的創(chuàng)建又是怎樣一個過程呢?
        ??????當(dāng)Java虛擬機(jī)遇到一條字節(jié)碼new指令時,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到 一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那 必須先執(zhí)行相應(yīng)的類加載過程。
        ??????在類加載檢查通過后,接下來虛擬機(jī)將為新生對象分配內(nèi)存。對象所需內(nèi)存的大小在類加載完成 后便可完全確定,為對象分配空間的任務(wù)實(shí)際上便等同于把一塊確定 大小的內(nèi)存塊從Java堆中劃分出來。假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有被使用過的內(nèi)存都被放在一 邊,空閑的內(nèi)存被放在另一邊,中間放著一個指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那 個指針向空閑空間方向挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump The Pointer)。但如果Java堆中的內(nèi)存并不是規(guī)整的,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯在一起,那 就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個列表,記錄上哪些內(nèi)存塊是可用的,在分 配的時候從列表中找到一塊足夠大的空間劃分給對象實(shí)例,并更新列表上的記錄,這種分配方式稱 為“空閑列表”(Free List)。選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用 的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定。因此,當(dāng)使用Serial、ParNew等帶壓縮 整理過程的收集器時,系統(tǒng)采用的分配算法是指針碰撞,既簡單又高效;而當(dāng)使用CMS這種基于清除 (Sweep)算法的收集器時,理論上就只能采用較為復(fù)雜的空閑列表來分配內(nèi)存。
        ??????除如何劃分可用空間之外,還有另外一個需要考慮的問題:對象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使僅僅修改一個指針?biāo)赶虻奈恢?,在并發(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對象A分配內(nèi)存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內(nèi)存的情況。解決這個問題有兩種可選方案:一種是對分配內(nèi)存空間的動作進(jìn)行同步處理——實(shí)際上虛擬機(jī)是采用CAS配上失敗重試的方式保證更新操作的原子性;另外一種是把內(nèi)存分配的動作按照線程劃分在不同的空間之中進(jìn)行,即每個線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(ThreadLocalAllocationBuffer,TLAB),哪個線程要分配內(nèi)存,就在哪個線程的本地緩沖區(qū)中分配,只有本地緩沖區(qū)用完了,分配新的緩存區(qū)時才需要同步鎖定。虛擬機(jī)是否使用TLAB,可以通過-XX:+/-UseTLAB參數(shù)來設(shè)定。
        ??????內(nèi)存分配完成之后,虛擬機(jī)必須將分配到的內(nèi)存空間(但不包括對象頭)都初始化為零值,如果使用了TLAB的話,這一項(xiàng)工作也可以提前至TLAB分配時順便進(jìn)行。這步操作保證了對象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,使程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值。
        ? ? ? 接下來,Java虛擬機(jī)還要對對象進(jìn)行必要的設(shè)置,例如這個對象是哪個類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼(實(shí)際上對象的哈希碼會延后到真正調(diào)用Object::hashCode()方法時才計(jì)算)、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(ObjectHeader)之中。根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對象頭會有不同的設(shè)置方式。關(guān)于對象頭的具體內(nèi)容,稍后會詳細(xì)介紹。


        2.2?對象的內(nèi)存布局

        ??????在HotSpot虛擬機(jī)里,對象在堆內(nèi)存中的存儲布局可以劃分為三個部分:對象頭(Header)、實(shí)例數(shù)據(jù)(InstanceData)和對齊填充(Padding)。

        ?????? HotSpot虛擬機(jī)對象的對象頭部分包括兩類信息。第一類是用于存儲對象自身的運(yùn)行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為32個比特和64個比特,官方稱它為“MarkWord”。對象需要存儲的運(yùn)行時數(shù)據(jù)很多,其實(shí)已經(jīng)超出了32、64位Bitmap結(jié)構(gòu)所能記錄的最大限度,但對象頭里的信息是與對象自身定義的數(shù)據(jù)無關(guān)的額外存儲成本,考慮到虛擬機(jī)的空間效率,MarkWord被設(shè)計(jì)成一個有著動態(tài)定義的數(shù)據(jù)結(jié)構(gòu),以便在極小的空間內(nèi)存儲盡量多的數(shù)據(jù),根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間。



        ??????對象頭的另外一部分是類型指針,即對象指向它的類型元數(shù)據(jù)的指針,Java虛擬機(jī)通過這個指針來確定該對象是哪個類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針,換句話說,查找對象的元數(shù)據(jù)信息并不一定要經(jīng)過對象本身。此外,如果對象是一個Java數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過普通Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是如果數(shù)組的長度是不確定的,將無法通過元數(shù)據(jù)中的信息推斷出數(shù)組的大小。
        ? ? ? 對象的第三部分是對齊填充,這并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot虛擬機(jī)的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說就是任何對象的大小都必須是8字節(jié)的整數(shù)倍。對象頭部分已經(jīng)被精心設(shè)計(jì)成正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,如果對象實(shí)例數(shù)據(jù)部分沒有對齊的話,就需要通過對齊填充來補(bǔ)全。


        2.3 對象的訪問定位

        ??????創(chuàng)建對象自然是為了后續(xù)使用該對象,我們的Java程序會通過棧上的reference數(shù)據(jù)來操作堆上的具體對象。由于reference類型在《Java虛擬機(jī)規(guī)范》里面只規(guī)定了它是一個指向?qū)ο蟮囊?,并沒有定義這個引用應(yīng)該通過什么方式去定位、訪問到堆中對象的具體位置,所以對象訪問方式也是由虛擬機(jī)實(shí)現(xiàn)而定的,主流的訪問方式主要有使用句柄和直接指針兩種:

        • ·如果使用句柄訪問的話,Java堆中將可能會劃分出一塊內(nèi)存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自具體的地址信息。


        • 如果使用直接指針訪問的話,Java堆中對象的內(nèi)存布局就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,reference中存儲的直接就是對象地址,如果只是訪問對象本身的話,就不需要多一次間接訪問的開銷。


        瀏覽 43
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 国产精品丝袜在线 | 久色导航| 高清无码不卡在线观看 | 青青草性爱| 婷婷国产成人精品免费视频 |