JVM系列(三) - 對(duì)象創(chuàng)建過(guò)程以及內(nèi)存分配機(jī)制

內(nèi)容導(dǎo)讀

  • 對(duì)象的創(chuàng)建過(guò)程
  • 內(nèi)存的分配方法以及分配時(shí)面臨的問(wèn)題和解決方案
  • 什么是對(duì)象頭
  • 對(duì)象棧上創(chuàng)建: 逃逸分析和標(biāo)量替換
  • 對(duì)象內(nèi)存回收

一. 對(duì)象的創(chuàng)建過(guò)程

對(duì)象的創(chuàng)建過(guò)程.png
  • 類是否加載
    檢查Class文件是否已經(jīng)被類加載子系統(tǒng)加載到內(nèi)存.沒(méi)有的話則走類的加載過(guò)程(load-link-init).

  • 分配內(nèi)存
    類加載完后, 需要在堆內(nèi)開(kāi)辟一塊內(nèi)存區(qū)域.
    但是在分配內(nèi)存時(shí), 需要解決兩個(gè)問(wèn)題:

    1. 內(nèi)存如何分配
      首先, 第一個(gè)問(wèn)題內(nèi)存如何分配, JVM給出的結(jié)局方案是指針碰撞空閑列表
      指針碰撞
      JVM默認(rèn)使用指針碰撞, 如果Java是一塊連續(xù)的內(nèi)存,指針的一邊是使用過(guò)的內(nèi)存, 一邊是未使用的內(nèi)存, 而指針?biāo)诘奈恢镁褪莾蓧K內(nèi)存的邊界. 當(dāng)創(chuàng)建新對(duì)象時(shí), 指針會(huì)向未使用的內(nèi)存移動(dòng)新對(duì)象大小的內(nèi)存距離.
      空閑列表
      對(duì)于不連續(xù)的內(nèi)存, 無(wú)法使用指針碰撞的方式, JVM則需要維護(hù)一個(gè)列表, 記錄未使用的內(nèi)存區(qū)域的地址.當(dāng)創(chuàng)建新對(duì)象時(shí), 從列表中找出一塊大小適合的內(nèi)存存放該對(duì)象, 并更新列表

      內(nèi)存分配方式.png

    2. 并發(fā)的情況下, 如何避免分配失敗?
      CAS
      JVM采用CAS和失敗重試的方式保證內(nèi)存分配的原子性

      線程本地分配緩沖(Thread Local Allocation Buffer, TLAB)
      為每個(gè)線程預(yù)留一塊內(nèi)存, 每個(gè)線程在自己的內(nèi)存空間上創(chuàng)建對(duì)象.如果TLAB依舊創(chuàng)建失敗, 則會(huì)自動(dòng)采用CAS的方式解決. 可以通過(guò)-XX:+UseTLAB開(kāi)始TLAB, -XX:TLABSize指定TLAB的大小

  • 初始化(賦默認(rèn)值)
    內(nèi)存分配完畢后, JVM會(huì)為分配的內(nèi)存設(shè)置默認(rèn)值

  • 設(shè)置對(duì)象頭
    對(duì)象在JVM中一共分為三塊: 對(duì)象頭, 實(shí)例數(shù)據(jù), 對(duì)齊填充
    而對(duì)象頭由分為: MarkWord, Klass Point(類型指針), 數(shù)組長(zhǎng)度4個(gè)字節(jié)

    對(duì)象的構(gòu)成.png

    MarkWord
    保存對(duì)象的hashCode, 鎖標(biāo)記, gc年齡, 偏向ID等信息.32位系統(tǒng)占4個(gè)字節(jié), 64位系統(tǒng)占8個(gè)字節(jié)
    Klass Point
    類型指針: 指向方法區(qū)中類元信息.開(kāi)啟壓縮占4個(gè)字節(jié), 關(guān)閉壓縮占8個(gè)字節(jié)
    數(shù)組長(zhǎng)度
    對(duì)象時(shí)數(shù)組類型的才有, 所以圖上以虛線表示, 占4個(gè)字節(jié)

    對(duì)象最終的大小始終是8的倍數(shù)

  • 初始化
    執(zhí)行<init>方法: 為屬性賦值和執(zhí)行構(gòu)造方法

指針壓縮

JDK1.6以后支持指針壓縮, 可以通過(guò)-XX:+CompressedOops開(kāi)啟

為什么要進(jìn)行指針壓縮?
1.在64位平臺(tái)的HotSpot中使用32位指針,內(nèi)存使用會(huì)多出1.5倍左右,使用較大指針在主內(nèi)存和緩存之間移動(dòng)數(shù)據(jù),占用較大寬帶,同時(shí)GC也會(huì)承受較大壓力
2.為了減少64位平臺(tái)下內(nèi)存的消耗,啟用指針壓縮功能
3.在jvm中,32位地址最大支持4G內(nèi)存(2的32次方),可以通過(guò)對(duì)對(duì)象指針的壓縮編碼、解碼方式進(jìn)行優(yōu)化,使得jvm
只用32位地址就可以支持更大的內(nèi)存配置(小于等于32G)
4.堆內(nèi)存小于4G時(shí),不需要啟用指針壓縮,jvm會(huì)直接去除高32位地址,即使用低虛擬地址空間
5.堆內(nèi)存大于32G時(shí),壓縮指針會(huì)失效,會(huì)強(qiáng)制使用64位(即8字節(jié))來(lái)對(duì)java對(duì)象尋址,這就會(huì)出現(xiàn)1的問(wèn)題,所以堆內(nèi)
存不要大于32G為好

二. 內(nèi)存分配機(jī)制

對(duì)象內(nèi)存分配的流程如圖所示:


對(duì)象內(nèi)存分配過(guò)程.png

棧上分配

通常對(duì)象在堆上分配, 當(dāng)對(duì)象不在被引用后, 會(huì)被GC回收.如果堆上的對(duì)象非常多, GC的時(shí)間會(huì)很長(zhǎng), 影響性能. 這個(gè)時(shí)候JVM會(huì)通過(guò)逃逸分析標(biāo)量替換決定一些對(duì)象可以直接在棧上分配. 棧上分配的對(duì)象會(huì)隨棧幀的出棧而銷毀, 不用通過(guò)GC回收, 節(jié)省內(nèi)存空間.

  • 逃逸分析
    某個(gè)方法內(nèi)創(chuàng)建的對(duì)象, 在方法外部不存在引用關(guān)系, 并且可以進(jìn)一步分解. JVM對(duì)于這種對(duì)象, 不會(huì)在堆上創(chuàng)建, 而是在棧上分配.
-XX:+DoEscapeAnalysis  開(kāi)啟逃逸分析, JDK1.7默認(rèn)開(kāi)啟
  • 標(biāo)量替換
    首先得清楚什么是標(biāo)量?
    標(biāo)量聚合量
    標(biāo)量: 即不能進(jìn)一步分解的量. Java的基本類型就是不可分解的標(biāo)量.
    聚合量: 可以進(jìn)一步分解的量, 就是聚合量, 比如對(duì)象
    通過(guò)逃逸分析確定對(duì)象不會(huì)被外部訪問(wèn), 且可以進(jìn)一步分解時(shí), JVM不會(huì)創(chuàng)建對(duì)象, 而是將對(duì)象的成員變量分成若干個(gè)方法的變量, 而這個(gè)被替換的成員變量可以直接在棧幀或者寄存器里分配, 從而避免對(duì)象不夠分配的情況.
-XX:+EliminateAllocations 開(kāi)啟標(biāo)量替換

案例:

package com.learn.jvm;

/**
 * Description:逃逸分析 
 * -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -XX:+PrintGC -Xmx15m -Xms15m
 * <p>
 * 對(duì)象棧上分配, 必須得開(kāi)啟逃逸分析和標(biāo)量替換, JDK1.7以后默認(rèn)開(kāi)啟<br/>
 * -XX:+DoEscapeAnalysis -XX:+EliminateAllocations 有效 只有一次GC<br/>
 * -XX:+DoEscapeAnalysis -XX:-EliminateAllocations 無(wú)效 大量GC<br/>
 * -XX:-DoEscapeAnalysis -XX:+EliminateAllocations 無(wú)效 大量GC<br/>
 * -XX:-DoEscapeAnalysis -XX:-EliminateAllocations 無(wú)效 大量GC<br/>
 * </p>
 * 
 */
public class EscapeAnalysisTest {
    public static void main(String[] args) {
        final long l = System.currentTimeMillis();
        for (int i = 0; i < 100_000_000; i++) {
            allocation();
        }
        final long end = System.currentTimeMillis();
        System.out.println(end - l);
    }

    private static void allocation() {
        User user = new User();
        user.setAge(1);
        user.setName("test");
    }
}

對(duì)象在Eden分配

新創(chuàng)建的對(duì)象會(huì)分配在Eden區(qū),當(dāng)發(fā)生Minor GC時(shí), 沒(méi)被回收的對(duì)象, 會(huì)進(jìn)入Survivor區(qū), 同時(shí)會(huì)記錄對(duì)象的分代年齡. 每發(fā)生一次Minor GC, 分代年齡就會(huì)加1, 到達(dá)一定閾值后, 會(huì)進(jìn)入老年代.

影響新對(duì)象的參數(shù)有:
-Xms : 堆大小, 堆越小, Eden和Survivor區(qū)就越小, MinorGC就越頻繁, 導(dǎo)致分代年齡增長(zhǎng)快
–XX:NewRatio : 設(shè)置年輕代和老年代的比例, 也影響年輕代的大小
-XX:SurvivorRatio=8 : 設(shè)置Eden和Survivor的比例, 影響Eden的大小
-XX:+UseAdaptiveSizePolicy : 默認(rèn)開(kāi)啟, 自動(dòng)調(diào)整Eden和Survivor的比例, 開(kāi)啟后不是絕對(duì)的8:1:1
-XX:MaxTenuringThreshold : 設(shè)置對(duì)象年齡, 超過(guò)該閾值的對(duì)象, 進(jìn)入老年代

測(cè)試對(duì)象在Eden分配

package com.learn.jvm.allocation;

/**
 * 對(duì)象分配測(cè)試 -Xms15m -Xmx15M -XX:+PrintGCDetails Eden: 4.5M Survivor: 1M Old:11M
 */
public class ObjectAllocationTest {
    public static void main(String[] args) {
        byte[] bytes = new byte[1024 * 1024 * 2];
        byte[] bytes1 = new byte[1024 * 1024 * 2];
        byte[] bytes2 = new byte[1024 * 520];
    }
}

// 只創(chuàng)建  byte[] bytes = new byte[1024 * 1024 * 2];

Heap
 PSYoungGen      total 4608K, used 2689K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 65% used [0x00000000ffb00000,0x00000000ffda0488,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 11264K, used 2048K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
  object space 11264K, 18% used [0x00000000ff000000,0x00000000ff200010,0x00000000ffb00000)
 Metaspace       used 3185K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K
可以看到Eden一共4M, 使用了65%

// 當(dāng)創(chuàng)建byte[] bytes = new byte[1024 * 1024 * 2]; byte[] bytes1 = new byte[1024 * 1024 * 2];時(shí)

Heap
 PSYoungGen      total 4608K, used 2773K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 67% used [0x00000000ffb00000,0x00000000ffdb5420,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 11264K, used 4096K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
  object space 11264K, 36% used [0x00000000ff000000,0x00000000ff400020,0x00000000ffb00000)
 Metaspace       used 3194K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

Eden還是用67%, 但是PerOldGen則用了36%, 說(shuō)明有一個(gè)對(duì)象進(jìn)入了老年代

// 當(dāng)三個(gè)對(duì)象都創(chuàng)建時(shí)
Heap
 PSYoungGen      total 4608K, used 3214K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 78% used [0x00000000ffb00000,0x00000000ffe23a68,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 11264K, used 4096K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
  object space 11264K, 36% used [0x00000000ff000000,0x00000000ff400020,0x00000000ffb00000)
 Metaspace       used 3248K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K
Eden使用了78%, ParOldGen使用了36%, 說(shuō)明Eden分配了兩個(gè)對(duì)象, 有一個(gè)對(duì)象進(jìn)入了老年代

大對(duì)象直接進(jìn)入老年代

對(duì)于Eden和Survivor區(qū)都放不下的對(duì)象會(huì)直接進(jìn)入老年代., -XX:PretenureSizeThreshold=100000(單位字節(jié)), 設(shè)置大對(duì)象的閾值, 超過(guò)該大小的對(duì)象直接進(jìn)入老年代

測(cè)試

package com.learn.jvm.allocation;

/**
 * Description: -Xms15m -Xmx15M -XX:+PrintGCDetails  大概內(nèi)存分配 Eden: 4.5M Survivor: 1M Old:11M
 */
public class LargeObjectTest {
    public static void main(String[] args) {
        byte[] obj = new byte[1024 * 1024 * 5];
    }
}

Heap
 PSYoungGen      total 4608K, used 2773K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 67% used [0x00000000ffb00000,0x00000000ffdb55e0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 11264K, used 5120K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
  object space 11264K, 45% used [0x00000000ff000000,0x00000000ff500010,0x00000000ffb00000)
 Metaspace       used 3231K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
Eden: 大約4M,  Survivor: 512K, 放不下5M的對(duì)象, 
ParOldGen: 使用了45% , 說(shuō)明對(duì)象直接進(jìn)入了老年代

對(duì)象動(dòng)態(tài)年齡判斷

-XX:TargetSurvivorRatio=n : 指定年齡
當(dāng)survivor區(qū)有一批對(duì)象的總大小大于survivor區(qū)的50%, 那么這批年齡大于n的對(duì)象, 直接進(jìn)入老年代.
對(duì)象動(dòng)態(tài)年齡判斷一般發(fā)生在Minor GC之后

老年代空間擔(dān)保機(jī)制

年輕代每次MinorGC之前, 都會(huì)計(jì)算下老年代的剩余可用空間, 如果剩余可用空間小于年輕代里對(duì)象大小的總和, 就會(huì)檢查是否設(shè)置了 -XX:-HandlePromotionFailure該參數(shù), 如果配置了, 則會(huì)檢查老年代剩余空間是否小于歷史年輕代MinorGC后進(jìn)入老年代的對(duì)象的平均值

如果小于的話, 則進(jìn)行Full GC, 如果FullGC之后, 還不夠, 則會(huì)發(fā)生OOM.

如果大于的話, 則只進(jìn)行Minor GC. 但是MinorGC后, 需要進(jìn)入老年代的對(duì)象依舊大于老年代的剩余可用空間, 則需要進(jìn)行FullGC, 如果FullGC之后, 還不夠, 則會(huì)發(fā)生OOM.

目的:可以減少一次Full GC

老年代空間擔(dān)保機(jī)制.png

三. 對(duì)象內(nèi)存回收

回收算法

  • 引用計(jì)數(shù)法
    增加一個(gè)對(duì)象引用計(jì)數(shù)器.每有一個(gè)地方引用對(duì)象, 引用計(jì)數(shù)器就加1; 引用失效了就減1. 當(dāng)計(jì)數(shù)器為0時(shí), 可以被回收

弊端: 無(wú)法解決循環(huán)依引用的問(wèn)題., 比如A對(duì)象有個(gè)成員變量b, B對(duì)象有個(gè)成員變量a, 但是對(duì)象A和對(duì)象B都沒(méi)有任何地方在引用它們, 但是引用計(jì)數(shù)算法會(huì)認(rèn)為A對(duì)象引用了B對(duì)象.

  • 可達(dá)性分析算法
    在說(shuō)可達(dá)性分析之前, 先了解下什么是GC Root
    GC Root: 主要指線程棧的本地變量, 靜態(tài)變量, 本地方法棧的變量等.
    以GC Root作為起點(diǎn), 向下搜索對(duì)象. 所有找到的對(duì)象都是非垃圾對(duì)象, 不會(huì)被回收.

常見(jiàn)的引用類型

  • 強(qiáng)引用
    存在引用關(guān)系, 就不會(huì)被回收.不管是否會(huì)OOM.
Object object = new Object()
  • 軟引用
    將對(duì)象用SoftRefernce類型包裝, 正常情況下不會(huì)被回收. 如果GC做完后發(fā)現(xiàn)釋放不出什么空間, 那么會(huì)在下次GC時(shí)回收.可以用于內(nèi)存敏感的高速緩存.
public static SoftReference<User> reference = new SoftReference<>(new User());
  • 弱引用
    使用WeakReference包裝的對(duì)象, 當(dāng)發(fā)生GC時(shí), 會(huì)被回收.
  • 虛引用
    一般是垃圾回收器會(huì)用到.
    無(wú)論如何都會(huì)被回收的對(duì)象.

如何判斷一個(gè)類"無(wú)用"

方法區(qū)也會(huì)發(fā)生OOM, 因此方法區(qū)也要回收, 一般是回收無(wú)用的類.但是無(wú)用的類的條件很苛刻.

  • 該類的所有實(shí)例對(duì)象都已被回收.
  • 加載該類的類加載器也已被回收
  • 該類對(duì)應(yīng)的java.lang.Class對(duì)象沒(méi)有任何引用, 無(wú)法在任何地方通過(guò)反射獲取該類的實(shí)例

創(chuàng)建類實(shí)例的方法

  • new的方法
  • Class.forName()
  • 通過(guò)java.lang.Class反射
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容