自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

硬核 JVM 壓縮指針詳解

開發(fā) 前端
當(dāng)今,Java已經(jīng)成為了世界上最流行的編程語言之一。在Java的生態(tài)系統(tǒng)中,JVM(Java虛擬機)是至關(guān)重要的組成部分。JVM 是 Java 程序運行的環(huán)境,它負(fù)責(zé)將 Java 字節(jié)碼翻譯成機器碼,并執(zhí)行程序。在 JVM 中,內(nèi)存使用以及分配一直是個重要的問題。

一、前言

當(dāng)今,Java已經(jīng)成為了世界上最流行的編程語言之一。在Java的生態(tài)系統(tǒng)中,JVM(Java虛擬機)是至關(guān)重要的組成部分。JVM 是 Java 程序運行的環(huán)境,它負(fù)責(zé)將 Java 字節(jié)碼翻譯成機器碼,并執(zhí)行程序。在 JVM 中,內(nèi)存使用以及分配一直是個重要的問題。

在 32 位系統(tǒng)中,一枚指針占用 4 字節(jié),隨著 64 位系統(tǒng)的逐漸普及,指針的大小也增長到了 8 個字節(jié),JVM 為了降低內(nèi)存占用,使用了指針壓縮技術(shù)來降低內(nèi)存的占用,接下來,我們將自頂向下的深入探討 JVM 指針壓縮的工作原理。

二、理解壓縮指針

為什么 JVM 需要壓縮指針?

在計算機中,指針的大小通常取決于計算機的字長。例如,在 32 位系統(tǒng)一字長為 4 字節(jié),即一枚指針的占用內(nèi)存空間大小為 4 字節(jié)。隨著計算機性能的提升,內(nèi)存價格的降低,64 位系統(tǒng)也開始逐漸普及。而在 64 位系統(tǒng)中,字長和指針也由原來的增長到 8 個字節(jié),JVM 為了降低內(nèi)存占用,開發(fā)了指針壓縮算法,即:在 64 位系統(tǒng)中,指針依然使用 4 字節(jié)存儲。

數(shù)據(jù)指針與字長的關(guān)系

字長是指一臺計算機中處理器可以一次性處理的二進制數(shù)字的位數(shù)。它通常是 8 位、16 位、32 位或 64 位,這意味著在一次處理中可以處理 8 個、16 個、32 個或 64 個二進制數(shù)字。字長越長,計算機處理數(shù)據(jù)的能力越強,但同時也會增加計算機的成本和復(fù)雜度。

CPU 的上下文是寄存器,CPU 運算的本質(zhì)就是不斷從 CS IP 寄存器中取指令然后執(zhí)行,CPU 運算所需的數(shù)據(jù)也是放在寄存器里,CPU 一次性處理的數(shù)據(jù)大小就是寄存器的大小。

圖片圖片

上圖左側(cè)是一段簡單的 C 語言代碼片段,右側(cè)是該代碼的匯編代碼(即 CPU 執(zhí)行這段代碼的實際機器碼的解釋)。

圖片圖片

CPU 執(zhí)行計算時,需要先將數(shù)據(jù)讀取到寄存器,存取內(nèi)存中的變量時,是直接操作變量所在地址及偏移量,而變量所在地址(即指針)也是存儲在寄存器中的,因此寄存器大小直接決定了 CPU 所能訪問內(nèi)存的地址空間。因此,在 32 位系統(tǒng)中,寄存器的最大長度是 32 bit(即 4 個字節(jié)),因此最大支持訪問 4GB 的內(nèi)存空間,在 64 位系統(tǒng)中,寄存器最大 64 bit(即 8 字節(jié))。而數(shù)據(jù)的指針由于需要指向整個內(nèi)存空間,因此也就是 8 字節(jié)。8 字節(jié)大小的指針?biāo)茉试S訪問的最大地址為:16EB(16384PB=16777216TB=17179869184GB)的內(nèi)存空間。

所以字長其實就等于寄存器的大小,指針為了能指向整個內(nèi)存空間,通常也等于字長大小。

理解內(nèi)存對齊

這是一段簡單的 C 語言代碼:

#include <stdio.h>
struct Test {
    int a;
    char b;
    int c;
} test;


int main(void) {
    test.a = 1;
    test.b = 2;
    test.c = 3;
    printf("struct Test size: %d\n", sizeof(test));
    return 0;
}

在 C 語言中,結(jié)構(gòu)體數(shù)據(jù)是連續(xù)的,Int 為 4 字節(jié),Char 為 1 字節(jié),所以 Struct Test 為 9 字節(jié)。

但是實際輸出結(jié)果卻是 12 字節(jié):

圖片圖片

圖片圖片

通過匯編指令可以分析出,結(jié)構(gòu)體 Test 的內(nèi)存布局如下:

圖片圖片

可以看出,在 Char 類型數(shù)據(jù)后面被空出了 3 個字節(jié)的位置,原因如下:

計算機只能從 4 字節(jié)的整數(shù)倍開始尋址,如果在 Char b 后不進行空數(shù)據(jù)填充,則編譯后的指令會很長,極大的降低 CPU 執(zhí)行效率:

圖片圖片

所以在 JVM 中,對象的存儲也是如此設(shè)計:

class Test {
    int a;
    char b;
    int c;
}

對象布局如下:

圖片圖片

可以看出 Char b 數(shù)據(jù)后空出了 3 個字節(jié)的內(nèi)存空間作為 Padding, 以供后面的對象進行內(nèi)存對齊。

為什么計算機只能從特定地址讀取數(shù)據(jù)?

計算機之所以只能從特定地址開始讀取數(shù)據(jù),是由于在內(nèi)存中存儲的物理位置導(dǎo)致的。

圖片圖片

這是一根內(nèi)存條,上面有 4 個內(nèi)存顆粒(Chip),在我們聲明一個變量 Int a = 1;時,CPU 想一次從內(nèi)存中同時取出 32 位數(shù)據(jù),為了發(fā)揮并行傳輸數(shù)據(jù)的能力,同時與 4 個內(nèi)存顆粒進行交互肯定比一個內(nèi)存交互要快,因此數(shù)據(jù) a 分別在 4 個顆粒中存儲 0x01 0x00 0x00 0x00。

圖片圖片

每個內(nèi)存顆粒 Chip 中有 8 個 bank,每次同時從 8 個 bank 中取一位數(shù)據(jù)。

圖片圖片

為了盡可能節(jié)約地址總線位數(shù),變量 a 的每字節(jié)數(shù)據(jù)在各 Chip 中相對位置是相同的,每字節(jié)中的每位數(shù)據(jù)在 bank 中的行數(shù)和列數(shù)也是相同的。

總結(jié),為了能利用有限的地址總線,盡量快速尋址到盡可能多的數(shù)據(jù),在設(shè)計計算機時取了巧,CPU 同時只能訪問 32 個相對地址相同的數(shù)據(jù)位,表現(xiàn)上就是只能從被 4 字節(jié)整除的地址開始尋址。

理解指針壓縮

在 64 位系統(tǒng)中,JVM 為了降低 8 字節(jié)的指針對內(nèi)存的占用,使用了指針壓縮的技術(shù),將 8 字節(jié)的指針壓縮為 4 字節(jié)。

前面說到,JVM 出于性能考慮,對數(shù)據(jù)做了對齊到 4 字節(jié)的處理,因此指針的值末尾 5 bit 始終為 0B11111。JVM 的指針壓縮算法就是,把本來應(yīng)該使用 8 個字節(jié)的指針,直接改為 4 字節(jié)的進行代替,那么 4 字節(jié)的指針實際最高可以表示 32G 的內(nèi)存空間。這也就是為什么當(dāng)物理內(nèi)存超過 32G 時,需要關(guān)閉 JVM 指針壓縮。

三、實戰(zhàn)解碼指針

工具介紹

HSDB

HSDB(HotSpot Debugger),是一個用于 HotSpot 虛擬機的調(diào)試工具。它提供了一種可視化的方式來查看和調(diào)試 Java 應(yīng)用程序在 JVM 上的運行情況。HSDB 可以用于分析線程、堆、類、對象、方法、編譯器和代碼緩存等方面的信息。它還可以用于監(jiān)視虛擬機性能和調(diào)試?yán)厥掌?。HSDB 是一個非常強大的工具,可以幫助開發(fā)人員更好地理解和優(yōu)化 Java 應(yīng)用程序的性能。

官方提供的 HSDB 命令行不是很好用,比如命令補全、命令提示、光標(biāo)移動、命令歷史記錄等都不存在,所以本次分享講利用 PerfMa 開源的 XPocket 工具,配合 HSDB 插件來操作。

XPocket 工具

XPocket 是 PerfMa 為解決性能問題而生的開源的插件容器,它是性能領(lǐng)域的樂高,將定位或者解決各種性能問題的常見的 Linux 命令,JDK 工具,知名性能工具等適配成各種 XPocket 插件,并讓它們可以相互聯(lián)動一鍵解決特定的性能問題。目前 XPocket 插件生態(tài)已經(jīng)實現(xiàn)了 HSDB、JDB、JConsole、Perf、Arthas 等多個優(yōu)秀的開源性能工具的插件化集成。

快速開始

下載 XPocket,然后解壓運行。

wget https://a.perfma.net/xpocket/download/XPocket.tar.gz
tar -xvf  XPocket.tar.gz
sh xpocket/xpocket.sh

圖片圖片

# 切換至 HSDB 插件空間
XPocket [system] > use jhsdb@JDK
# 啟動 clhsdb 命令行
XPocket [jhsdb] > clhsdb
# attach 到目標(biāo)進程
XPocket [jhsdb] > attach 29516

利用 HSDB 查看 JVM 對象內(nèi)存布局

準(zhǔn)備工作

編寫一個測試類,啟動 JVM 進程。

@Data
@Component
public class BeanTest {
    private long l = 1;
    private BeanTest pointer = this;
    private int i = 2;
    private boolean b = false;
    private char c = 3;
    private BeanTest[] arr = {this};
}

想要查看對象的內(nèi)存布局,首先要找到這個對象所在位置,JVM 的對象分布在堆上,可以通過 Universe 命令確定堆內(nèi)的相關(guān)區(qū)域?qū)?yīng)位置。

# 執(zhí)行以下命令,切換至 HSDB 插件
XPocket [system] > use jhsdb@JDK
# 啟動 HSDB 插件
XPocket [jhsdb] > clhsdb
# 通過 jps 命令,查詢 JVM 進程的 pid,attach 到這個 JVM 進程
XPocket [jhsdb] > attach 29516
# 執(zhí)行 universe 命令,查看堆內(nèi)存分布情況
XPocket [jhsdb : 29516] > universe

內(nèi)存分布情況如下:

圖片圖片

括號內(nèi)的第一個值為內(nèi)存起始位置,第二個值為已使用的位置,第三個值為該區(qū)域最大地址。以上圖 eden 區(qū)為例:

eden 區(qū)空間范圍為:0x00000000fab00000 ~ 0x00000000fef00000,相減得到 71303168,68MB。

eden 區(qū)已使用空間為:0x00000000fab00000 ~ 0x00000000fafd2a60,相減得到 5057120,4.82M。

找到我們定義的對象

我們要找的對象是受 Spring 管理的,所以很容易判斷,通常情況下都會在老年代里。那么我們直接在老年代內(nèi)檢索這個對象即可。由于 Oop(簡單對象指針)是所有 Java 對象的基類,所以我們可以利用 Scanoops 命令來檢索這個對象。

XPocket [jhsdb : 29516] > scanoops 0x00000000f0000000 0x00000000f1265ca8 com.poizon.robot.test.BeanTest

檢索到的結(jié)果如下:

圖片圖片

0x00000000f1060898 是這個對象實例的內(nèi)存地址,也就是該對象的指針值。

查看對象內(nèi)存布局

然后我們就可以通過 Inspect 命令來查看該對象的內(nèi)存布局了。

XPocket [jhsdb : 29516] > inspect 0x00000000f1060898

得到結(jié)果如下:

圖片圖片

注意:這里顯示的數(shù)據(jù)是按照類變量順序來展示的,并非實際結(jié)構(gòu)

由圖返回結(jié)果可以看出, BeanTest 對象大小為 40 字節(jié),當(dāng)前系統(tǒng)為 X64 架構(gòu),一個字長為 8 字節(jié),即此對象占用 5 個字長。執(zhí)行 Mem 命令,獲取整個對象(5 個字長)的數(shù)據(jù)。

XPocket [jhsdb : 29516] > mem 0x00000000f1060898 5

得到結(jié)果如下:

圖片圖片

左側(cè)為當(dāng)前字長的數(shù)據(jù)起始地址,右側(cè)為數(shù)據(jù)值。

我們再來復(fù)習(xí)一遍對象頭(Oop數(shù)據(jù)),前 8 位是 Mark Word,后 8 位中,如果開啟了指針壓縮,則前 4 位為當(dāng)前實例所在類的指針,后 4 位填充當(dāng)前對象 <=4 字節(jié)的數(shù)據(jù)(Gap Padding);如果未開啟指針壓縮,則該 8 位為當(dāng)前實例對所在類的指針。

查找類的指針

我們這次啟動的進程開啟了指針壓縮,把第二個字長的數(shù)據(jù)(0x000000022007a2bb)按 4 字節(jié)拆成兩部分:

0x00000002:BeanTest 對象中只有一個值為 2 ,所以該值是Int i = 2;

0x2007a2bb(類指針):前面說過,指針壓縮的原理只是簡單的把 8 字節(jié)指針削減為 4 字節(jié),因為后 5 位始終為 0,因此若要還原壓縮后的指針實際內(nèi)存地址,直接把指針值 * 8即可。

BeanTest 類所在地址為:0x2007a2bb * 8 = 0x1003D15D8

執(zhí)行 Inspect 命令,查看 BeanTest 類結(jié)構(gòu)。

XPocket [jhsdb : 29516] > inspect 0x1003D15D8

結(jié)果如下:

圖片圖片

證實

BeanTest 在 JVM 對象為 c++ 的 InstanceKlass 實例,可以看出 _name 屬性為 Symbol,接下來查看 InstanceKlass _name 屬性,證實當(dāng)前為 BeanTest 類的實例,執(zhí)行 Symbol 命令查看 _name 的值:

XPocket [jhsdb : 29516] > symbol 0x00007f1838abb740

圖片圖片

證實 0x1003D15D8 所在的內(nèi)存地址,即是 com.poizon.robot.test.BeanTest 的 Class 對象。

四、總結(jié)

在我們?nèi)粘i_發(fā)中遇到的一點小小的,看似不起眼的奇怪的規(guī)范,深挖之下往往能夠牽扯出一大串知識體系,保持好奇心刨根問底同樣也是很重要學(xué)習(xí)方法。有興趣的同學(xué)也可以自己嘗試找一下示例類中其他屬性所對應(yīng)的 Class 指針,說不定還可以對網(wǎng)上的一些文章做一次勘誤呢~

責(zé)任編輯:武曉燕 來源: 得物技術(shù)
相關(guān)推薦

2024-07-26 10:23:52

2023-10-20 13:12:10

Btrfs壓縮

2021-12-21 15:31:10

C++語言指針

2015-12-24 09:48:40

JavaScriptthis指針深

2010-07-16 16:40:48

Perl引用

2009-12-18 15:24:52

2024-11-25 16:29:48

2010-09-26 11:00:48

JVM參數(shù)配置

2010-09-27 13:48:41

JVM內(nèi)存結(jié)構(gòu)

2010-09-25 12:38:40

JVM內(nèi)存模型

2009-07-17 17:11:47

Ruby生成JVM代碼

2010-01-04 09:27:31

Linux壓縮解壓縮命令詳解

2009-07-09 14:01:22

JVM工作原理

2010-09-17 15:57:23

TomcatJVM

2009-07-08 10:41:54

JDK JRE JVM

2010-09-26 08:50:11

JVM工作原理

2023-08-02 08:38:27

JVM加載機制

2018-11-01 10:34:37

JVM內(nèi)存配置

2010-09-25 13:38:23

Inside JVM

2010-09-26 13:23:13

JVM內(nèi)存管理機制
點贊
收藏

51CTO技術(shù)棧公眾號