深入探究JVM | klass-oop对象模型研究
├─agent ? ? ? ? ? ? ? ? ? ? ? ? ? ?Serviceability Agent的客戶端實現
├─make ? ? ? ? ? ? ? ? ? ? ? ? ? ? 用來build出HotSpot的各種配置文件
├─src ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?HotSpot VM的源代碼
│ ?├─cpu ? ? ? ? ? ? ? ? ? ? ? ? ? ?CPU相關代碼(匯編器、模板解釋器、ad文件、部分runtime函數在這里實現)
│ ?├─os ? ? ? ? ? ? ? ? ? ? ? ? ? ? 操作系相關代碼
│ ?├─os_cpu ? ? ? ? ? ? ? ? ? ? ? ? 操作系統+CPU的組合相關的代碼
│ ?└─share ? ? ? ? ? ? ? ? ? ? ? ? ?平臺無關的共通代碼
│ ? ? ?├─tools ? ? ? ? ? ? ? ? ? ? ? ?工具
│ ? ? ?│ ?├─hsdis ? ? ? ? ? ? ? ? ? ? ?反匯編插件
│ ? ? ?│ ?├─IdealGraphVisualizer ? ? ? 將server編譯器的中間代碼可視化的工具
│ ? ? ?│ ?├─launcher ? ? ? ? ? ? ? ? ? 啟動程序“java”
│ ? ? ?│ ?├─LogCompilation ? ? ? ? ? ? 將-XX:+LogCompilation輸出的日志(hotspot.log)整理成更容易閱讀的格式的工具
│ ? ? ?│ ?└─ProjectCreator ? ? ? ? ? ? 生成Visual Studio的project文件的工具
│ ? ? ?└─vm ? ? ? ? ? ? ? ? ? ? ? ? ? HotSpot VM的核心代碼
│ ? ? ? ? ?├─adlc ? ? ? ? ? ? ? ? ? ? ? 平臺描述文件(上面的cpu或os_cpu里的*.ad文件)的編譯器
│ ? ? ? ? ?├─asm ? ? ? ? ? ? ? ? ? ? ? ?匯編器接口
│ ? ? ? ? ?├─c1 ? ? ? ? ? ? ? ? ? ? ? ? client編譯器(又稱“C1”)
│ ? ? ? ? ?├─ci ? ? ? ? ? ? ? ? ? ? ? ? 動態編譯器的公共服務/從動態編譯器到VM的接口
│ ? ? ? ? ?├─classfile ? ? ? ? ? ? ? ? ?類文件的處理(包括類加載和系統符號表等)
│ ? ? ? ? ?├─code ? ? ? ? ? ? ? ? ? ? ? 動態生成的代碼的管理
│ ? ? ? ? ?├─compiler ? ? ? ? ? ? ? ? ? 從VM調用動態編譯器的接口
│ ? ? ? ? ?├─gc_implementation ? ? ? ? ?GC的實現
│ ? ? ? ? ?│ ?├─concurrentMarkSweep ? ? ?Concurrent Mark Sweep GC的實現
│ ? ? ? ? ?│ ?├─g1 ? ? ? ? ? ? ? ? ? ? ? Garbage-First GC的實現(不使用老的分代式GC框架)
│ ? ? ? ? ?│ ?├─parallelScavenge ? ? ? ? ParallelScavenge GC的實現(server VM默認,不使用老的分代式GC框架)
│ ? ? ? ? ?│ ?├─parNew ? ? ? ? ? ? ? ? ? ParNew GC的實現
│ ? ? ? ? ?│ ?└─shared ? ? ? ? ? ? ? ? ? GC的共通實現
│ ? ? ? ? ?├─gc_interface ? ? ? ? ? ? ? GC的接口
│ ? ? ? ? ?├─interpreter ? ? ? ? ? ? ? ?解釋器,包括“模板解釋器”(官方版在用)和“C++解釋器”(官方版不在用)
│ ? ? ? ? ?├─libadt ? ? ? ? ? ? ? ? ? ? 一些抽象數據結構
│ ? ? ? ? ?├─memory ? ? ? ? ? ? ? ? ? ? 內存管理相關(老的分代式GC框架也在這里)
│ ? ? ? ? ?├─oops ? ? ? ? ? ? ? ? ? ? ? HotSpot VM的對象系統的實現
│ ? ? ? ? ?├─opto ? ? ? ? ? ? ? ? ? ? ? server編譯器(又稱“C2”或“Opto”)
│ ? ? ? ? ?├─prims ? ? ? ? ? ? ? ? ? ? ?HotSpot VM的對外接口,包括部分標準庫的native部分和JVMTI實現
│ ? ? ? ? ?├─runtime ? ? ? ? ? ? ? ? ? ?運行時支持庫(包括線程管理、編譯器調度、鎖、反射等)
│ ? ? ? ? ?├─services ? ? ? ? ? ? ? ? ? 主要是用來支持JMX之類的管理功能的接口
│ ? ? ? ? ?├─shark ? ? ? ? ? ? ? ? ? ? ?基于LLVM的JIT編譯器(官方版里沒有使用)
│ ? ? ? ? ?└─utilities ? ? ? ? ? ? ? ? ?一些基本的工具類
└─test ? ? ? ? ? ? ? ? ? ? ? ? ? ? 單元測試
Hotspot簡介
Hotspot是openjdk的JVM虛擬機, Linux發行版下默認安裝的是openjdk, 而Oracle 的jdk也基本是由openjdk代碼編譯而來, 外加上一些商業代碼, 形成orcale的jdk. 由此可見, hotspot的無處不在. 現在越來越多的應用構建在Java之上,大數據的很多項目, 如Hbase,Hive, flume等等, 都可運行在hotspot之上. 此外, Android的開發用的也是java, 雖然不是運行在hotspot之上, 底下也是JVM在支撐.
所以, hotspot JVM的研究價值無疑是巨大的. 而在使用hotspot眾多問題之中, 又以gc問題最為廣泛和突出, 遂萌生了研究hotspot GC的想法. 為了備忘和分享, 將研究過程中的點滴, 記錄至博客.
按照openjdk官網的說法, hotspot是 “the best Java virtual machine on the planet”
準備資料
Open JDK官網
Hotspot源碼
Hotspot術語
Hotspot存儲管理
學習計劃
基礎準備, 對象布局, 指針壓縮, GC Root, GC安全點等
- Serial GC算法 - 單核小內存環境適合的算法
- Parallel GC算法 - 吞吐量型算法
- Garbage First算法(G1) - 低延時, 同時兼顧吞吐量, 適應大內存
至于Concurrent Mark Sweep算法,見如下JEP(JDK Enhancement Proposal)
JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector
Summary
Deprecate the Concurrent Mark Sweep (CMS) garbage collector, with the intent to stop supporting it in a future major release.
Goals
Accelerate the development of other garbage collectors in HotSpot.
Motivation
Dropping support for CMS and then removing the CMS code, or at least more thoroughly segregating it, will reduce the maintenance burden of the GC code base and accelerate new development. The G1 garbage collector is intended, in the long term, to be a replacement for most uses of CMS.
在JDK9中, G1將作為默認選項(之前是Parallel Scavenge), 并意圖取代Low Pause類型的CMS收集器, 所以CMS只做大概了解.
研究環境
Ubuntu 15.10 64bit
Hotspot 64位默認參數下的行為
第一篇-對象內存布局
對象的metadata
內存的抽象就是一個線性空間內的字節數組, 通過下標來訪問某一位置的數據. 熟悉C語言的同學對C式內存應該都不會陌生, 這些背景了解一點就好, 不了解也無傷大雅, 這里就不討論c語言的細節了.
在C語言中, 動態分配一塊內存通常是使用malloc函數, 形如:
//分配一塊1024字節的內存
char* pBuffer = (char*)malloc(1024);
//訪問內存的內容
pBuffer[0] = 'a';
pBuffer[1] = 'b';
//釋放
free(pBuffer);
在C語言中使用內存直接通過指針使用base[index]的方式訪問內存的某一個Item, 指針的第一個位置直接就是buffer內存段的開始. 而對于java對象來說, 雖然經過了jvm的一層屏蔽, 把指針這個概念給隱去了, 但對象終歸是要存在內存當中的. 我們知道java有各種各樣的class, 在內存中分配對象時, class就是對應要分配的對象模板, 對象占多大空間, 每個字段在此空間內的偏移值, 等等信息, 都由class的定義提供. 對于GC來說, 必須知道對象占多大空間, 才好在回收時把相應的內存釋放, 不然就沒辦法準確的管理了.
JVM的heap可以理解為一次性malloc了一大塊的內存, 比如1G等, 然后由自己管理內部對象的分配. 由于回收需要知道對象占多大空間, 所以在分配對象時, 除了對象本身我們看得見的字段外, 還需要對象的描述信息, 這就是對象的metadata. 直覺來看, 只要在對象buffer的頭幾個字節中保留一份對應的class信息即可,確實如此. 來看代碼:
//hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
?//....
private:
? volatile markOop _mark;
? union _metadata {
? ? Klass* ? ? ?_klass;
? ? narrowKlass _compressed_klass;
? } _metadata;
?//....
}
在hotspot中對象指針稱為oop(ordinary object pointer), 而oopDesc則是對象頭的結構. 除了預想中的Klass(之所以叫kclass是因為class是C++關鍵字,被占用掉了。這里用Klass代替)指針外, 還由一個_mark字段, 是因為除了對象的class信息以外, 還有一些對象信息需要保留, 比如GC年齡, 鎖狀態等.
對于其中的_klass是存在于union類型的_metadata中的, 我們知道union類型的分配是按成員最大的那個進行分配的, 然后對這塊內存的解釋取決于代碼中使用的是其中哪個字段.
typedef juint ?narrowKlass; -> typedef uint32_t juint;
為什么要這么寫呢, 從narrowKlass中可以窺得一二, 之所以叫narrow, 小的意思. 因為64位環境下, 寄存器是64位的, 對應指針也就成64位了, 也就是8字節. 我們知道4字節可以表示4G, 實際中基本不會有需要加載這么多對象的情況, 因此8字節就顯得浪費了, narrowKlass只使用4個字節, 預分配給_metadata的8字節中的另外4字節就可以用做他用了. 看似4個字節無關緊要, 但是堆中存在上千萬到億個對象時, 省下的內存就是幾百兆啊.
另外一個字段:
typedef class ? markOopDesc* ? ? ? ? ? ? ? ?markOop;
指針類型, 8字節.
總結以上, 對象頭默認情況占16字節, 在開啟壓縮對象指針時(通過-XX:+UseCompressedClassPointers), 占12字節, 默認狀態是開啟的.
對象的成員
介紹完了對象頭, 接下來就是對象的成員了. 對于原始數據類型:
long / double - 8 bytes
int / float - 4 bytes
short / char - 2 bytes
byte/boolean - 1 bytes
reference type - 4 or 8 bytes
對于對象引用, 最直接的方式就是存對象的指針了, 這樣可以方便的操作對象的各部分內容. 不過又回到64bit的問題, 64bit能表達的數量實在太大了, 實際中很少需要這么大的表達能力. 因此, 類似與kclass指針的做法, 可以選擇性的啟用指針壓縮技術, 將引用壓縮為4字節表示, 由于對象引用遠比kclass引用來的多, 因此節省的內存相當可觀.
當采用4字節表示引用時, 直觀來看是表示4G bytes大小的空間, 但是, 由于對象分配時是8字節對齊的, 也就是對象指針的低3bit是0, 因此可以把這3bit壓縮掉, 實際32bit的可以表示4G*8 bytes = 32G bytes的內存空間, 對于大部分服務來說足夠了. heap小于32G時, 指針壓縮默認開啟. JVM相應的控制參數為: -XX:+/-UseCompressedOops.
對象布局
對象的定義順序和布局順序是不一樣的, 我們在寫代碼的時候想怎么寫就怎么寫, 不用關心內存對齊問題, byte后面跟個double或者int, 都沒有關系, 但是如果內存也按這么布局的話, 由于cpu讀取內存時, 是按寄存器(64bit)大小單位載入的, 如果載入的數據橫跨兩個64bit, 要操作該數據的話至少需要兩次讀取, 加上組合移位, 會產生效率問題, 甚至會引發異常. 比如在一些ARM處理器上, 如果不按對齊要求訪問數據, 會觸發硬件異常.
基于此, JVM內部的對象布局和定義布局是不同的. 在class文件中, 字段的定義是按照代碼順序排列的, 加載后, 會生成相應的數據結構, 包含字段的名稱, 字段在對象中的偏移等, 重新布局后, 只要改變相應的偏移值即可. 呵呵, 有沒有聯想到java字段反射?
在hotspot中, 對象布局有三種模式, 看代碼注釋更直觀:
? // Rearrange fields for a given allocation style
? if( allocation_style == 0 ) {
? ? // Fields order: oops, longs/doubles, ints, shorts/chars, bytes, padded fields
? ? ....
? } else if( allocation_style == 1 ) {
? ? // Fields order: longs/doubles, ints, shorts/chars, bytes, oops, padded fields
? ? ....
? } else if( allocation_style == 2 ) {
? ? // Fields allocation: oops fields in super and sub classes are together.
? ? ....
? }?
類型0, 引用在原始類型前面, 然后依次是longs/doubles, ints, shorts/chars, bytes, 最后是填充字段, 以滿足對其要求.
類型1, 引用在原始類型后面
類型2, JVM在布局時會盡量使父類對象和子對象挨在一起, 原因后面解釋.
另外, 由于填充會形成gap空洞, 比如使用壓縮kclass指針時, 頭占12字節, 后面如果是long的話, long的對齊要求是8字節, 中間會有4個字節的空洞, 為了高效利用, 可以把int/short/byte等比較小的對象塞進去, 與此同時JVM提供了開關控制該特性-XX:+/-CompactFields, 默認開啟.
來看個代碼例子:
public class JavaTest {
? ? public static class TestLayout {
? ? ? ? Object filed1;
? ? ? ? char field2;
? ? ? ? short field3;
? ? ? ? Object filed4;
?
? ? ? ? long field5;
? ? ? ? byte field6;
? ? ? ? double filed7;
? ? }
?
? ? public static class SubTestLayout extends TestLayout{
? ? ? ? Object subFiled1;
? ? ? ? char subField2;
? ? ? ? short subField3;
? ? ? ? Object subFiled4;
?
? ? ? ? long subField5;
? ? ? ? byte subField6;
? ? ? ? double subFiled7;
? ? }
?
? ? /**
? ? ?* @param args
? ? ?*/
? ? public static void main(String[] args) throws Exception{
? ? ? ? SubTestLayout.class.toString();
? ? }
}
不用覺得main函數奇怪, 我們只需要載入類, 然后利用-XX:PrintFieldLayout來查看布局情況. 該選項只在調試版本中有效. 至于布局模式, 可以使用-XX:FieldsAllocationStyle=mode來指定, 默認是1.
FieldsAllocationStyle=0, oop在前面
@后面是偏移值
com.lqp.test.JavaTest$TestLayout: field layout?
@ 12 — instance fields start —?
@ 12 “filed1” Ljava.lang.Object;?
@ 20 “field2” C har
@ 22 “field3” S tring
@ 16 “filed4” Ljava.lang.Object;?
@ 24 “field5” J?
@ 40 “field6” B ool
@ 32 “filed7” D ouble
@ 44 — instance fields end —?
@ 48 — instance ends —?
@112 — static fields start —?
@112 — static fields end —
FieldsAllocationStyle=1, oop在末尾
com.lqp.test.JavaTest$TestLayout: field layout?
@ 12 — instance fields start —?
@ 36 “filed1” Ljava.lang.Object;?
@ 12 “field2” C?
@ 14 “field3” S?
@ 40 “filed4” Ljava.lang.Object;?
@ 16 “field5” J?
@ 32 “field6” B?
@ 24 “filed7” D?
@ 44 — instance fields end —?
@ 48 — instance ends —?
@112 — static fields start —?
@112 — static fields end —
FieldsAllocationStyle=2, 父子oop相連
com.lqp.test.JavaTest$TestLayout: field layout?
@ 12 — instance fields start —?
@ 36 “filed1” Ljava.lang.Object;?
@ 12 “field2” C?
@ 14 “field3” S?
@ 40 “filed4” Ljava.lang.Object;?
@ 16 “field5” J?
@ 32 “field6” B?
@ 24 “filed7” D?
@ 44 — instance fields end —?
@ 48 — instance ends —?
@112 — static fields start —?
@112 — static fields end —
com.lqp.test.JavaTest$SubTestLayout: field layout?
@ 44 — instance fields start —?
@ 44 “subFiled1” Ljava.lang.Object;?
@ 52 “subField2” C?
@ 54 “subField3” S?
@ 48 “subFiled4” Ljava.lang.Object;?
@ 56 “subField5” J?
@ 72 “subField6” B?
@ 64 “subFiled7” D?
@ 76 — instance fields end —?
@ 80 — instance ends —?
@112 — static fields start —?
@112 — static fields end —
為什么由父子oop布局連續的形式呢, 從代碼來看, 我能看到的好處: 一個好處是減少OopMapBlock的數量. 由于GC收集時要掃描存活的對象, 所以必須知道對象中引用的內存位置, 對于原始類型, 是不需要掃描的, OopMapBlock結構用于描述某個對象中引用區域的起始偏移和引用個數(見下面代碼引用). 另外一個好處是連續的對象區域使得cache line的使用效率更高. 試想如果父對象和子對象的對象引用區域不連續, 而中間插入了原始類型字段的話, 那么在做GC對象掃描時, 很可能需要跨cache line讀取才能完成掃描.
OopMapBlock結構如下:
class OopMapBlock {
....
?private:
? int ?_offset;
? uint _count;
};
由起始偏移和數量描述, 描述的是連續的空間, 當在對象中, 父對象和子對象oop連續時, 只需要一個OopMapBlock結構, 不然就需要2個了.
布局的代碼位于:
layout_fields() - hotspot/src/share/vm/classfile/classFileParser.cpp
當我們在寫Java代碼的時候,我們會面對著無數個接口,類,對象和方法。但我們有沒有想過,Java中的這些對象、類和方法,在HotSpot JVM中的結構又是怎么樣呢?HotSpot JVM底層都是C++實現的,那么Java的對象模型與C++對象模型之間又有什么關系呢?今天就來分析一下HotSpot JVM中的對象模型:oop-klass model,它們的源碼位于openjdk-8/openjdk/hotspot/src/share/vm/oops文件夾內。
注:本文對應的OpenJDK版本為openjdk-8u76-b02。對于不同的版本(openjdk-7, openjdk-8, openjdk-9),其對應的HotSpot JVM的對象模型有些許差別(7和8的差別比較大)
oop-klass model概述
HotSpot JVM并沒有根據Java實例對象直接通過虛擬機映射到新建的C++對象,而是設計了一個oop-klass model。
這里的oop指的是 Ordinary Object Pointer(普通對象指針),它用來表示對象的實例信息,看起來像個指針實際上是藏在指針里的對象。而klass則包含元數據和方法信息,用來描述Java類。
那么為何要設計這樣一個一分為二的對象模型呢?這是因為HotSopt JVM的設計者不想讓每個對象中都含有一個vtable(虛函數表),所以就把對象模型拆成klass和oop,其中oop中不含有任何虛函數,而klass就含有虛函數表,可以進行method dispatch。這個模型其實是參照的Strongtalk VM 底層的對象模型。
體系總覽
在oopsHierarchy.hpp里定義了oop和klass各自的體系。
這是oop的體系:
typedef class oopDesc* oop;
typedef class instanceOopDesc* instanceOop;
typedef class arrayOopDesc* arrayOop;
typedef class objArrayOopDesc* objArrayOop;
typedef class typeArrayOopDesc* typeArrayOop;
注意由于Java 8引入了Metaspace,OpenJDK 1.8里對象模型的實現與1.7有很大的不同。原先存于PermGen的數據都移至Metaspace,因此它們的C++類型都繼承于MetaspaceObj類(定義見vm/memory/allocation.hpp),表示元空間的數據。
這是元數據的體系:
// The metadata hierarchy is separate from the oop hierarchy
// class MetaspaceObj
class ConstMethod;
class ConstantPoolCache;
class MethodData;
// class Metadata
class Method;
class ConstantPool;
// class CHeapObj
class CompiledICHolder;
這是klass的體系:
// The klass hierarchy is separate from the oop hierarchy.
class Klass;
class InstanceKlass;
class InstanceMirrorKlass;
class InstanceClassLoaderKlass;
class InstanceRefKlass;
class ArrayKlass;
class ObjArrayKlass;
class TypeArrayKlass;
注意klass代表元數據,繼承自Metadata類,因此像Method、ConstantPool都會以成員變量(或指針)的形式存在于klass體系中。
以下是JDK 1.7中的類在JDK 1.8中的存在形式:
klassOop->Klass*
klassKlass不再需要
methodOop->Method*
methodDataOop-> MethodData*
constMethodOop -> ConstMethod*
constantPoolOop -> ConstantPool*
constantPoolCacheOop -> ConstantPoolCache*
klass
一個Klass對象代表一個類的元數據(相當于java.lang.Class對象)。它提供:
language level class object (method dictionary etc.)
provide vm dispatch behavior for the object
所有的函數都被整合到一個C++類中。
Klass對象的繼承關系:xxxKlass <:< Klass <:< Metadata <:< MetaspaceObj
klass對象的布局如下:
// Klass layout:
// [C++ vtbl ptr ] (contained in Metadata)
// [layout_helper ]
// [super_check_offset ] for fast subtype checks
// [name ]
// [secondary_super_cache] for fast subtype checks
// [secondary_supers ] array of 2ndary supertypes
// [primary_supers 0]
// [primary_supers 1]
// [primary_supers 2]
// ...
// [primary_supers 7]
// [java_mirror ]
// [super ]
// [subklass ] first subclass
// [next_sibling ] link to chain additional subklasses
// [next_link ]
// [class_loader_data]
// [modifier_flags]
// [access_flags ]
// [last_biased_lock_bulk_revocation_time] (64 bits)
// [prototype_header]
// [biased_lock_revocation_count]
// [_modified_oops]
// [_accumulated_modified_oops]
// [trace_id]
oop
oop類型其實是oopDesc*。在Java程序運行的過程中,每創建一個新的對象,在JVM內部就會相應地創建一個對應類型的oop對象。各種oop類的共同基類為oopDesc類。
JVM內部,一個Java對象在內存中的布局可以連續分成兩部分:instanceOopDesc和實例數據。instanceOopDesc和arrayOopDesc又稱為對象頭。
instanceOopDesc對象頭包含兩部分信息:Mark Word 和 元數據指針(Klass*):
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
分別來看一下:
- Mark Word:instanceOopDesc中的_mark成員,允許壓縮。它用于存儲對象的運行時記錄信息,如哈希值、GC分代年齡(Age)、鎖狀態標志(偏向鎖、輕量級鎖、重量級鎖)、線程持有的鎖、偏向線程ID、偏向時間戳等
- 元數據指針:instanceOopDesc中的_metadata成員,它是聯合體,可以表示未壓縮的Klass指針(_klass)和壓縮的Klass指針。對應的klass指針指向一個存儲類的元數據的Klass對象
下面我們來分析一下,執行new A()的時候,JVM native層里發生了什么。首先,如果這個類沒有被加載過,JVM就會進行類的加載,并在JVM內部創建一個instanceKlass對象表示這個類的運行時元數據(相當于Java層的Class對象)。到初始化的時候(執行invokespecial A::<init>),JVM就會創建一個instanceOopDesc對象表示這個對象的實例,然后進行Mark Word的填充,將元數據指針指向Klass對象,并填充實例變量。
根據對JVM的理解,我們可以想到,元數據—— instanceKlass 對象會存在元空間(方法區),而對象實例—— instanceOopDesc 會存在Java堆。Java虛擬機棧中會存有這個對象實例的引用。
?
?
?
總結
以上是生活随笔為你收集整理的深入探究JVM | klass-oop对象模型研究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浦发信用卡免盗刷极致版多少钱
- 下一篇: 7月银行居民存款再出现搬家,比6月减少了