背景
從 JDK 1.7 開始,Oracle 團隊就開始對 HotSpot VM 的永久代(PermGen)大刀闊斧的修改、移除,導致 HotSpot 的內存區域發生了很多改變,最終在 JDK 1.8 元空間(Metaspace)取代了永久代成為 HotSpot VM 對方法區的實現。
我們入門虛擬機的學習大多是通過《Java 虛擬機規范》、《深入理解Java虛擬機》這兩本經典。但是由於 Java 環境復雜、JDK版本更新、市面上的虛擬機型號眾多等問題,這兩本書只能幫助我們解決一些宏觀認識的問題,對於一些細節問題還是需要我們結合具體的環境來看。
本文主要研究的問題是 java.lang.Class 對象和 static 成員變量在運行時內存的位置。這里先給出結論,JDK 1.8 中,兩者都位於堆(Heap),且static 成員變量位於 Class對象內。
相信讀者都曾閱讀過《深入理解Java虛擬機 第2版》中的下面這兩段話:
方法區(Method Area)與 Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、及時編譯器編譯后的代碼等數據。
…
加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,方法區中的數據存儲格式由虛擬機實現自行定義,虛擬機規范未規定此區域的具體數據結構。然后在內存中實例化一個java.lang.Class的對象(並沒有明確規定是在java堆中,對於HotSpot虛擬機而言,Class對象比較特殊,它雖然是對象,但是存放在方法區里面)
…
在筆者寫這篇文章的時候,暫時還沒有找到第3版,而第2版是基於 JDK 1.7 的,接下來我們整理一下網上的博客與實驗,看看 JDK 1.8 中這些變化的產生。
java.lang.Class
我們知道在類加載(Class Loading)的 5 個過程中,加載(Loading)的最終產物是一個 java.lang.Class 對象,它是我們訪問方法區中的類型數據的外部接口,我們需要通過這個對象來訪問類的字段、方法、運行時常量池等。

那么這個 Class 對象在哪里呢?下面是搜集的幾份資料。
-
hotspot java虛擬機Class對象是放在方法區還是堆中 ? - 潛龍勿用的回答 - 知乎
Class對象是存放在堆區的,不是方法區,這點很多人容易犯錯。類的元數據(元數據並不是類的Class對象!Class對象是加載的最終產品,類的方法代碼,變量名,方法名,訪問權限,返回值等等都是在方法區的)才是存在方法區的。
…
這個回答指出 Class 對象並非在方法區中,但沒有指出JDK的版本,不過里面詳細收錄了 JVM 需要保存的 .class 文件數據結構(元數據)。特別推薦看這個回答最后舉例代碼怎樣調用方法區的信息,很有用!
-
hotspot java虛擬機Class對象是放在方法區還是堆中 ? - ETIN的回答 - 知乎
這個回答直接從 openJDK 1.8 中關於虛擬機實現的源碼入手,分析了 Class 對象分配內存的過程,最后指出 Class 確實是分配在 Heap 上。openJDK 1.8 這部分源碼是用 C/C++ 寫的,初學者慎入!(有理有據,代碼說話,但我看不懂…)
-
Java static變量保存在哪?
這篇博客我也很推薦大家看,在 1.8 環境下,作者通過一些調試工具打印出虛擬機內存,追蹤 Class 對象的分配,明確了 Class 對象的真實地址就是在堆中,並且, 在 Class 的實例中找到靜態成員變量的分配位置。(一石二鳥,好文,調試的工具可以學一學)
static 成員變量
上一節其實已經通過實驗知道 static 成員變量在 Class 對象里,也就是在 Heap 上,這里進一步分析這個問題。
java中的靜態變量和Class對象究竟存放在哪個區域? - ETIN的回答 - 知乎 還是通過源碼分析靜態成員的分配,初學者慎入!
而其實在官方的 Bug 文檔中已經提到了 static 成員變量位置變化的說明,JDK-7017732 : move static fields into Class to prepare for perm gen removal 里提到為了迎合移除永久代的需要,靜態字段被移到了 Class 對象中。這里摘一段關鍵:
Currently static fields are stored in the instanceKlass but when those are moved into native memory we’d have to have a new card mark strategy for static fields. This could be something like setting a flag in the instanceKlass and then rescanning every klass during a GC which seems expensive or marking the card for the java.lang.Class then making sure to scan the instanceKlass when scanning the Class. If we move them into the Class then almost all the existing machinery works exactly as it always has. The only execution difference is which constant is materialized for the field access.
看不懂沒關系,看標題意思意思也差不多了。
題外話
由於 JDK 版本的變化,一些經典的書會在某些方面給我們帶來很多誤解,這是不可避免的,我們要做的就是根據實際解決問題,如果我們還不具備自己驗證的能力,也不要吝惜請教。最后,送大家一句話:“盡信書不如無書。”
正文結束,歡迎留言。
Class對象是存放在堆區的,不是方法區,這點很多人容易犯錯。類的元數據(元數據並不是類的Class對象!Class對象是加載的最終產品,類的方法代碼,變量名,方法名,訪問權限,返回值等等都是在方法區的)才是存在方法區的。
方法區
在一個JVM實例的內部,類型信息被存儲在一個稱為方法區的內存邏輯區中。類型信息是由類加載器在類加載時從類文件中提取出來的。類(靜態)變量也存儲在方法區中。
JVM實現的設計者決定了類型信息的內部表現形式。如,多字節變量在類文件是以big-endian存儲的,但在加載到方法區后,其存放形式由jvm根據不同的平台來具體定義。
JVM在運行應用時要大量使用存儲在方法區中的類型信息。在類型信息的表示上,設計者除了要盡可能提高應用的運行效率外,還要考慮空間問題。根據不同的需求,JVM的實現者可以在時間和空間上追求一種平衡。
因為方法區是被所有線程共享的,所以必須考慮數據的線程安全。假如兩個線程都在試圖找lava的類,在lava類還沒有被加載的情況下,只應該有一個線程去加載,而另一個線程等待。
方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。同樣方法區也不必是連續的。方法區可以在堆(甚至是虛擬機自己的堆)中分配。jvm可以允許用戶和程序指定方法區的初始大小,最小和最大尺寸。
方法區同樣存在垃圾收集,因為通過用戶定義的類加載器可以動態擴展java程序,一些類也會成為垃圾。jvm可以回收一個未被引用類所占的空間,以使方法區的空間最小。
類型信息
對每個加載的類型,jvm必須在方法區中存儲以下類型信息:
一 這個類型的完整有效名
二 這個類型直接父類的完整有效名(除非這個類型是interface或是
java.lang.Object,兩種情況下都沒有父類)
三 這個類型的修飾符(public,abstract, final的某個子集)
四 這個類型直接接口的一個有序列表
類型名稱在java類文件和jvm中都以完整有效名出現。在java源代碼中,完整有效名由類的所屬包名稱加一個”.”,再加上類名
組成。例如,類Object的所屬包為java.lang,那它的完整名稱為java.lang.Object,但在類文件里,所有的”.”都被
斜杠“/”代替,就成為java/lang/Object。完整有效名在方法區中的表示根據不同的實現而不同。
除了以上的基本信息外,jvm還要為每個類型保存以下信息:
類型的常量池( constant pool)
域(Field)信息
方法(Method)信息
除了常量外的所有靜態(static)變量
常量池
jvm為每個已加載的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,
integer, 和floating point常量)和對類型,域和方法的符號引用。池中的數據項象數組項一樣,是通過索引訪問的。
因為常量池存儲了一個類型所使用到的所有類型,域和方法的符號引用,所以它在java程序的動態鏈接中起了核心的作用。
域信息
jvm必須在方法區中保存類型的所有域的相關信息以及域的聲明順序,
域的相關信息包括:
域名
域類型
域修飾符(public, private, protected,static,final volatile, transient的某個子集)
方法信息
jvm必須保存所有方法的以下信息,同樣域信息一樣包括聲明順序
方法名
方法的返回類型(或 void)
方法參數的數量和類型(有序的)
方法的修飾符(public, private, protected, static, final, synchronized, native, abstract的一個子集)除了abstract和native方法外,其他方法還有保存方法的字節碼(bytecodes)操作數棧和方法棧幀的局部變量區的大小
異常表
類變量( Class Variables 譯者:就是類的靜態變量,它只與類相關,所以稱為類變量 )
類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。這些變量只與類相關,所以在方法區中,它們成為類數據在邏輯上的一部分。在jvm使用一個類之前,它必須在方法區中為每個non-final類變量分配空間。
常量(被聲明為final的類變量)的處理方法則不同,每個常量都會在常量池中有一個拷貝。non-final類變量被存儲在聲明它的
類信息內,而final類被存儲在所有使用它的類信息內。
對類加載器的引用
jvm必須知道一個類型是由啟動加載器加載的還是由用戶類加載器加載的。如果一個類型是由用戶類加載器加載的,那么jvm會將這個類加載器的一個引用作為類型信息的一部分保存在方法區中。
jvm在動態鏈接的時候需要這個信息。當解析一個類型到另一個類型的引用的時候,jvm需要保證這兩個類型的類加載器是相同的。這對jvm區分名字空間的方式是至關重要的。
對Class類的引用
jvm為每個加載的類型(譯者:包括類和接口)都創建一個java.lang.Class的實例。而jvm必須以某種方式把Class的這個實例和存儲在方法區中的類型數據聯系起來。
你可以通過Class類的一個靜態方法得到這個實例的引用// A method declared in class java.lang.Class:
public static Class forName(String className);
假如你調用forName(“java.lang.Object”),你會得到與java.lang.Object對應的類對象。你甚至可以通過這個函數 得到任何包中的任何已加載的類引用,只要這個類能夠被加載到當前的名字空間。如果jvm不能把類加載到當前名字空間,forName就會拋出ClassNotFoundException。
(譯者:熟悉COM的朋友一定會想到,在COM中也有一個稱為 類對象(Class Object)的東東,這個類對象主要 是實現一種工廠模式,而java由於有了jvm這個中間 層,類對象可以很方便的提供更多的信息。這兩種類對象 都是Singleton的)
也可以通過任一對象的getClass()函數得到類對象的引用,getClass被聲明在Object類中:
// A method declared in class java.lang.Object:
public final Class getClass();
例如,假如你有一個java.lang.Integer的對象引用,可以激活getClass()得到對應的類引用。
通過類對象的引用,你可以在運行中獲得相應類存儲在方法區中的類型信息,下面是一些Class類提供的方法:
// Some of the methods declared in class java.lang.Class:
public String getName();
public Class getSuperClass();
public boolean isInterface();
public Class[] getInterfaces();
public ClassLoader getClassLoader();
這些方法僅能返回已加載類的信息。getName()返回類的完整名,getSuperClass()返回父類的類對象,isInterface()判斷是否是接口。getInterfaces()返回一組類對象,每個類對象對應一個直接父接口。如果沒有,則返回一個長度為零的數組。
getClassLoader()返回類加載器的引用,如果是由啟動類加載器加載的則返回null。所有的這些信息都直接從方法區中獲得。
方法表
為了提高訪問效率,必須仔細的設計存儲在方法區中的數據信息結構。除了以上討論的結構,jvm的實現者還可以添加一些其他的數據結構,如方法表。jvm對每個加載的非虛擬類的類型信息中都添加了一個方法表,方法表是一組對類實例方法的直接引用(包括從父類繼承的方法)。jvm可以通過方法表快速激活實例方法。(譯者:這里的方法表與C++中的虛擬函數表一樣,但java方法全都 是virtual的,自然也不用虛擬二字了。正像java宣稱沒有 指針了,其實java里全是指針。更安全只是加了更完備的檢查機制,但這都是以犧牲效率為代價的,個人認為java的設計者 始終是把安全放在效率之上的,所有java才更適合於網絡開發)
舉一個例子
為了顯示jvm如何使用方法區中的信息,我們據一個例子,我們
看下面這個類:
class Lava {
private int speed = 5; // 5 kilometers per hour
void flow() {
}
}
class Volcano {
public static void main(String[] args) {
Lava lava = new Lava();
lava.flow();
}
}
下面我們描述一下main()方法的第一條指令的字節碼是如何被執行的。不同的jvm實現的差別很大,這里只是其中之一。
為了運行這個程序,你以某種方式把“Volcano”傳給了jvm。有了這個名字,jvm找到了這個類文件(Volcano.class)並讀入,它從類文件提取了類型信息並放在了方法區中,通過解析存在方法區中的字節碼,jvm激活了main()方法,在執行時,jvm保持了一個指向當前類(Volcano)常量池的指針。
注意jvm在還沒有加載Lava類的時候就已經開始執行了。正像大多數的jvm一樣,不會等所有類都加載了以后才開始執行,它只會在需要的時候才加載。
main()的第一條指令告知jvm為列在常量池第一項的類分配足夠的內存。jvm使用指向Volcano常量池的指針找到第一項,發現是一個對Lava類的符號引用,然后它就檢查方法區看lava是否已經被加載了。
這個符號引用僅僅是類lava的完整有效名”lava“。這里我們看到為了jvm能盡快從一個名稱找到一個類,一個良好的數據結構是多么重要。這里jvm的實現者可以采用各種方法,如hash表,查找樹等等。同樣的算法可以用於Class類的forName()的實現。
當jvm發現還沒有加載過一個稱為”Lava”的類,它就開始查找並加載類文件”Lava.class”。它從類文件中抽取類型信息並放在了方法區中。
jvm於是以一個直接指向方法區lava類的指針替換了常量池第一項的符號引用。以后就可以用這個指針快速的找到lava類了。而這個替換過程稱為常量池解析(constant pool resolution)。在這里我們替換的是一個native指針。
jvm終於開始為新的lava對象分配空間了。這次,jvm仍然需要方法區中的信息。它使用指向lava數據的指針(剛才指向volcano常量池第一項的指針)找到一個lava對象究竟需要多少空間。
jvm總能夠從存儲在方法區中的類型信息知道某類型對象需要的空間。但一個對象在不同的jvm中可能需要不同的空間,而且它的空間分布也是不同的。(譯者:這與在C++中,不同的編譯器也有不同的對象模型是一個道理)
一旦jvm知道了一個Lava對象所要的空間,它就在堆上分配這個空間並把這個實例的變量speed初始化為缺省值0。假如lava的父對象也有實例變量,則也會初始化。
當把新生成的lava對象的引用壓到棧中,第一條指令也結束了。下面的指令利用這個引用激活java代碼把speed變量設為初始值,5。另外一條指令會用這個引用激活Lava對象的flow()方法。
先說結論,參照OpenJDK1.8的源碼,Class對象應該存在於Heap中。
1. Class對象何時創建——類加載器加載過程中創建,具體參見源碼:
instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,
ClassLoaderData* loader_data,
Handle protection_domain,
KlassHandle host_klass,
GrowableArray<Handle>* cp_patches,
TempNewSymbol& parsed_name,
bool verify,
TRAPS){
/***ignore***/
// LINE: 4077
// Allocate mirror and initialize static fields
java_lang_Class::create_mirror(this_klass, protection_domain, CHECK_(nullHandle));
/***ignore***/}
2. java_lang_Class::create_mirror函數的具體實現(針對Klass是InstanceKlass和ArrayKlass):
oop java_lang_Class::create_mirror(KlassHandle k, Handle protection_domain, TRAPS) {
assert(k->java_mirror() == NULL, "should only assign mirror once");
// Use this moment of initialization to cache modifier_flags also,
// to support Class.getModifiers(). Instance classes recalculate
// the cached flags after the class file is parsed, but before the
// class is put into the system dictionary.
int computed_modifiers = k->compute_modifier_flags(CHECK_0);
k->set_modifier_flags(computed_modifiers);
// Class_klass has to be loaded because it is used to allocate
// the mirror.
if (SystemDictionary::Class_klass_loaded()) {
// Allocate mirror (java.lang.Class instance)
Handle mirror = InstanceMirrorKlass::cast(SystemDictionary::Class_klass())->allocate_instance(k, CHECK_0);
InstanceMirrorKlass* mk = InstanceMirrorKlass::cast(mirror->klass());
java_lang_Class::set_static_oop_field_count(mirror(), mk->compute_static_oop_field_count(mirror()));
// It might also have a component mirror. This mirror must already exist.
if (k->oop_is_array()) {
Handle comp_mirror;
if (k->oop_is_typeArray()) {
BasicType type = TypeArrayKlass::cast(k())->element_type();
comp_mirror = Universe::java_mirror(type);
} else {
assert(k->oop_is_objArray(), "Must be");
Klass* element_klass = ObjArrayKlass::cast(k())->element_klass();
assert(element_klass != NULL, "Must have an element klass");
comp_mirror = element_klass->java_mirror();
}
assert(comp_mirror.not_null(), "must have a mirror");
// Two-way link between the array klass and its component mirror:
ArrayKlass::cast(k())->set_component_mirror(comp_mirror());
set_array_klass(comp_mirror(), k());
} else {
assert(k->oop_is_instance(), "Must be");
// Allocate a simple java object for a lock.
// This needs to be a java object because during class initialization
// it can be held across a java call.
typeArrayOop r = oopFactory::new_typeArray(T_INT, 0, CHECK_NULL);
set_init_lock(mirror(), r);
// Set protection domain also
set_protection_domain(mirror(), protection_domain());
// Initialize static fields
InstanceKlass::cast(k())->do_local_static_fields(&initialize_static_field, CHECK_NULL);
}
return mirror();
} else {
if (fixup_mirror_list() == NULL) {
GrowableArray<Klass*>* list =
new (ResourceObj::C_HEAP, mtClass) GrowableArray<Klass*>(40, true);
set_fixup_mirror_list(list);
}
fixup_mirror_list()->push(k());
return NULL;
}
}
3. 基本類型的Class對象的創建:
void Universe::initialize_basic_type_mirrors(TRAPS) {
assert(_int_mirror==NULL, "basic type mirrors already initialized");
_int_mirror =
java_lang_Class::create_basic_type_mirror("int", T_INT, CHECK);
_float_mirror =
java_lang_Class::create_basic_type_mirror("float", T_FLOAT, CHECK);
_double_mirror =
java_lang_Class::create_basic_type_mirror("double", T_DOUBLE, CHECK);
_byte_mirror =
java_lang_Class::create_basic_type_mirror("byte", T_BYTE, CHECK);
_bool_mirror =
java_lang_Class::create_basic_type_mirror("boolean",T_BOOLEAN, CHECK);
_char_mirror =
java_lang_Class::create_basic_type_mirror("char", T_CHAR, CHECK);
_long_mirror =
java_lang_Class::create_basic_type_mirror("long", T_LONG, CHECK);
_short_mirror =
java_lang_Class::create_basic_type_mirror("short", T_SHORT, CHECK);
_void_mirror =
java_lang_Class::create_basic_type_mirror("void", T_VOID, CHECK);
_mirrors[T_INT] = _int_mirror;
_mirrors[T_FLOAT] = _float_mirror;
_mirrors[T_DOUBLE] = _double_mirror;
_mirrors[T_BYTE] = _byte_mirror;
_mirrors[T_BOOLEAN] = _bool_mirror;
_mirrors[T_CHAR] = _char_mirror;
_mirrors[T_LONG] = _long_mirror;
_mirrors[T_SHORT] = _short_mirror;
_mirrors[T_VOID] = _void_mirror;
//_mirrors[T_OBJECT] = InstanceKlass::cast(_object_klass)->java_mirror();
//_mirrors[T_ARRAY] = InstanceKlass::cast(_object_klass)->java_mirror();
}
oop java_lang_Class::create_basic_type_mirror(const char* basic_type_name, BasicType type, TRAPS) {
// This should be improved by adding a field at the Java level or by
// introducing a new VM klass (see comment in ClassFileParser)
oop java_class = InstanceMirrorKlass::cast(SystemDictionary::Class_klass())->allocate_instance(NULL, CHECK_0);
if (type != T_VOID) {
Klass* aklass = Universe::typeArrayKlassObj(type);
assert(aklass != NULL, "correct bootstrap");
set_array_klass(java_class, aklass);
}
#ifdef ASSERT
InstanceMirrorKlass* mk = InstanceMirrorKlass::cast(SystemDictionary::Class_klass());
assert(java_lang_Class::static_oop_field_count(java_class) == 0, "should have been zeroed by allocation");
#endif
return java_class;
}
4. InstanceMirrorKlass::allocate_instance函數:
instanceOop InstanceMirrorKlass::allocate_instance(KlassHandle k, TRAPS) {
// Query before forming handle.
int size = instance_size(k);
KlassHandle h_k(THREAD, this);
instanceOop i = (instanceOop) CollectedHeap::Class_obj_allocate(h_k, size, k, CHECK_NULL);
return i;
}
5. Class對象堆上分配實現:
oop CollectedHeap::Class_obj_allocate(KlassHandle klass, int size, KlassHandle real_klass, TRAPS) {
debug_only(check_for_valid_allocation_state());
assert(!Universe::heap()->is_gc_active(), "Allocation during gc not allowed");
assert(size >= 0, "int won't convert to size_t");
HeapWord* obj;
assert(ScavengeRootsInCode > 0, "must be");
obj = common_mem_allocate_init(real_klass, size, CHECK_NULL);
post_allocation_setup_common(klass, obj);
assert(Universe::is_bootstrapping() ||
!((oop)obj)->is_array(), "must not be an array");
NOT_PRODUCT(Universe::heap()->check_for_bad_heap_word_value(obj, size));
oop mirror = (oop)obj;
java_lang_Class::set_oop_size(mirror, size);
// Setup indirections
if (!real_klass.is_null()) {
java_lang_Class::set_klass(mirror, real_klass());
real_klass->set_java_mirror(mirror);
}
InstanceMirrorKlass* mk = InstanceMirrorKlass::cast(mirror->klass());
assert(size == mk->instance_size(real_klass), "should have been set");
// notify jvmti and dtrace
post_allocation_notify(klass, (oop)obj);
return mirror;
}
虛擬機棧、本地方法棧、程序計數器、方法區是、棧都只是JVM規范中的概念模型,現實中的虛擬機實現可能不是這么分。
hotspot中,在1.7及以前,permgen承擔了方法區的任務(permgen的任務不止於此),permgen又是在堆上。從1.8開始,permgen被移除,而有了metaspace,metaspace不是借堆實現的。
我只是R大的搬運工:
借助HotSpot SA來一窺PermGen上的對象
http://www.zhihu.com/question/33186690/answer/56347931
http://zhihu.com/question/30301819/answer/47539163
借助HotSpot SA來一窺PermGen上的對象
(Disclaimer:如果需要轉載請先與我聯系;
作者:RednaxelaFX -> rednaxelafx.iteye.com)
接着
前天的與
昨天的帖,今天也來介紹一個
HotSpot的
Serviceability Agent(以下簡稱SA)的玩法例子。
昨天用SA把x86機器碼反匯編到匯編代碼,或許對多數Java程序員來說並不怎么有趣。那么今天就來點更接近Java,但又經常被誤解的話題——HotSpot的GC堆的permanent generation。
要用SA里最底層的API來連接上一個Java進程並不困難,不過SA還提供了更方便的封裝:只要繼承
sun.jvm.hotspot.tools.Tool 並實現一個
run() 方法,在該方法內使用SA的API訪問JVM即可。
(或者更簡單的:可以直接起CLHSDB,attach上之后運行下面這句就好
- jseval "sa.objHeap.iteratePerm(new sapkg.oops.HeapPrinter(java.lang.System.out))"
做的事情跟本文后面的例子一樣用iteratePerm(),而下面這句
- jseval "io = java.io; sa.objHeap.iteratePerm(new sapkg.oops.HeapPrinter(new io.PrintStream(new io.FileOutputStream('perm.log'))))"
直接把PermGen內容輸出到文件里去
)
這次我們就把一個跑在HotSpot上的Java進程的perm gen里所有對象的信息打到標准輸出流上看看吧。
測試環境是32位Linux,x86,Sun JDK 6 update 2
(手邊可用的JDK版本很多,隨便拿了一個來用,呵呵 >_<)
代碼如下:
- import sun.jvm.hotspot.gc_implementation.parallelScavenge.PSPermGen;
- import sun.jvm.hotspot.gc_implementation.parallelScavenge.ParallelScavengeHeap;
- import sun.jvm.hotspot.gc_implementation.shared.MutableSpace;
- import sun.jvm.hotspot.gc_interface.CollectedHeap;
- import sun.jvm.hotspot.memory.Universe;
- import sun.jvm.hotspot.oops.HeapPrinter;
- import sun.jvm.hotspot.oops.HeapVisitor;
- import sun.jvm.hotspot.oops.ObjectHeap;
- import sun.jvm.hotspot.runtime.VM;
- import sun.jvm.hotspot.tools.Tool;
-
-
-
-
-
- public class TestPrintPSPermGen extends Tool {
- public static void main(String[] args) {
- TestPrintPSPermGen test = new TestPrintPSPermGen();
- test.start(args);
- test.stop();
- }
-
- @Override
- public void run() {
- VM vm = VM.getVM();
- Universe universe = vm.getUniverse();
- CollectedHeap heap = universe.heap();
- puts("GC heap name: " + heap.kind());
- if (heap instanceof ParallelScavengeHeap) {
- ParallelScavengeHeap psHeap = (ParallelScavengeHeap) heap;
- PSPermGen perm = psHeap.permGen();
- MutableSpace permObjSpace = perm.objectSpace();
- puts("Perm gen: [" + permObjSpace.bottom() + ", " + permObjSpace.end() + ")");
- long permSize = 0;
- for (VM.Flag f : VM.getVM().getCommandLineFlags()) {
- if ("PermSize".equals(f.getName())) {
- permSize = Long.parseLong(f.getValue());
- break;
- }
- }
- puts("PermSize: " + permSize);
- }
- puts();
-
- ObjectHeap objHeap = vm.getObjectHeap();
- HeapVisitor heapVisitor = new HeapPrinter(System.out);
- objHeap.iteratePerm(heapVisitor);
- }
-
- private static void puts() {
- System.out.println();
- }
-
- private static void puts(String s) {
- System.out.println(s);
- }
- }
很簡單,假定目標Java進程用的是Parallel Scavenge(PS)算法的GC堆,輸出GC堆的名字,當前perm gen的起始和結束地址,VM參數中設置的PermSize(perm gen的初始大小);然后是perm gen中所有對象的信息,包括對象摘要、地址、每個成員域的名字、偏移量和值等。
對HotSpot的VM參數不熟悉的同學可以留意一下幾個參數在HotSpot源碼中的定義:
- product(ccstrlist, OnOutOfMemoryError, "",
- "Run user-defined commands on first java.lang.OutOfMemoryError")
- product(bool, UseParallelGC, false, "Use the Parallel Scavenge garbage collector")
- product_pd(uintx, PermSize, "Initial size of permanent generation (in bytes)")
- product_pd(uintx, MaxPermSize, "Maximum size of permanent generation (in bytes)")
要讓SA連接到一個正在運行的Java進程最重要是提供進程ID。獲取pid的方法有很多,今天演示的是利用OnOutOfMemoryError參數指定讓HotSpot在遇到內存不足而拋出OutOfMemoryError時執行一段用戶指定的命令;在這個命令中可以使用%p占位符表示pid,HotSpot在執行命令時會把真實pid填充進去。
然后來造一個引發OOM的導火索:
- public class Foo {
- public static void main(String[] args) {
- Long[] array = new Long[256*1024*1024];
- }
- }
對32位HotSpot來說,main()方法里的new Long[256*1024*1024]會試圖創建一個大於1GB的數組對象,那么只要把-Xmx參數設到1GB或更小便足以引發OOM了。
如何知道這個數組對象會占用超過1GB的呢?Long[]是一個引用類型的數組,只要知道32位HotSpot中采用的對象布局:
-----------------------
(+0) | _mark |
-----------------------
(+4) | _metadata |
-----------------------
(+8) | 數組長度 length |
-----------------------
(+12+4*0) | 下標為0的元素 |
-----------------------
(+12+4*1) | 下標為1的元素 |
-----------------------
| ... |
-----------------------
(+12+4*n) | 下標為n的元素 |
-----------------------
| ... |
-----------------------
就知道一大半了~
跑一下Foo程序。留意到依賴SA的代碼要編譯的話需要$JAVA_HOME/lib/sa-jdi.jar在classpath上,執行時同理。指定GC算法為Parallel Scavenge,並指定Java堆(不包括perm gen)的初始和最大值都為1GB:
- [sajia@sajia ~]$ java -server -version
- java version "1.6.0_02"
- Java(TM) SE Runtime Environment (build 1.6.0_02-b05)
- Java HotSpot(TM) Server VM (build 1.6.0_02-b05, mixed mode)
- [sajia@sajia ~]$ javac Foo.java
- [sajia@sajia ~]$ javac -classpath ".:$JAVA_HOME/lib/sa-jdi.jar" TestPrintPSPermGen.java
- [sajia@sajia ~]$ java -server -XX:+UseParallelGC -XX:OnOutOfMemoryError='java -cp $JAVA_HOME/lib/sa-jdi.jar:. TestPrintPSPermGen %p > foo.txt' -Xms1g -Xmx1g Foo
- #
- # java.lang.OutOfMemoryError: Java heap space
- # -XX:OnOutOfMemoryError="java -cp $JAVA_HOME/lib/sa-jdi.jar:. TestPrintPSPermGen %p > foo.txt"
- # Executing /bin/sh -c "java -cp $JAVA_HOME/lib/sa-jdi.jar:. TestPrintPSPermGen 23373 > foo.txt"...
- Attaching to process ID 23373, please wait...
- Debugger attached successfully.
- Server compiler detected.
- JVM version is 1.6.0_02-b05
- Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- at Foo.main(Foo.java:5)
- [sajia@sajia ~]$
得到的foo.txt就是要演示的輸出結果。把它壓縮了放在附件里,有興趣但懶得自己實驗的同學也可以觀摩一下~
在foo.txt的開頭可以看到:
- GC heap name: ParallelScavengeHeap
- Perm gen: [0x70e60000, 0x71e60000)
- PermSize: 16777216
這里顯示了GC堆確實是Parallel Scavenge的,其中perm gen當前的起始地址為0x70e60000,結束地址為0x71e60000,中間連續的虛擬內存空間都分配給perm gen使用。簡單計算一下可知perm gen大小為16MB,與下面打出的PermSize參數的值完全吻合。
通過閱讀該日志文件,可以得知HotSpot在perm gen里存放的對象主要有:
- Klass系對象
- java.lang.Class對象
- 字符串常量
- 符號(Symbol/symbolOop)常量
- 常量池對象
- 方法對象
等等,以及它們所直接依賴的一些對象。具體這些都是什么留待以后有空再寫。
接下來挑幾個例子來簡單講解一下如何閱讀這個日志文件里的對象描述。
首先看一個String對象。先看看JDK里java.lang.String對象的聲明是什么樣的:
- package java.lang;
-
-
-
- public final class String
- implements java.io.Serializable, Comparable<String>, CharSequence
- {
-
- private final char value[];
-
-
- private final int offset;
-
-
- private final int count;
-
-
- private int hash;
-
-
- private static final long serialVersionUID = -6849794470754667710L;
-
- private static final ObjectStreamField[] serialPersistentFields =
- new ObjectStreamField[0];
-
- public static final Comparator<String> CASE_INSENSITIVE_ORDER
- = new CaseInsensitiveComparator();
- private static class CaseInsensitiveComparator
- implements Comparator<String>, java.io.Serializable {
-
- }
- }
留意到String對象有4個成員域,分別是:
| 名字 |
類型 |
引用類型還是值類型 |
| value |
char[] |
引用類型 |
| offset |
int |
值類型 |
| count |
int |
值類型 |
| hash |
int |
值類型 |
String類自身有三個靜態變量,分別是:
| 名字 |
類型 |
引用類型還是值類型 |
備注 |
| serialVersionUID |
long |
值類型 |
常量 |
| serialPersistentFields |
java.io.ObjectStreamField[] |
引用類型 |
只讀變量 |
| CASE_INSENSITIVE_ORDER |
java.lang.String.CaseInsensitiveComparator |
引用類型 |
只讀變量 |
回到我們的foo.txt日志文件來看一個String的對象實例:
- "main" @ 0x7100b140 (object size = 24)
- - _mark: {0} :1
- - _klass: {4} :InstanceKlass for java/lang/String @ 0x70e6c6a0
- - value: {8} :[C @ 0x7100b158
- - offset: {12} :0
- - count: {16} :4
- - hash: {20} :0
這是在HotSpot的字符串池里的一個字符串常量對象,"main"。
日志中的“"main"”是對象的摘要,String對象有特別處理顯示為它的內容,其它多數類型的對象都是顯示類型名之類的。
在@符號之后的就是對象的起始地址,十六進制表示。
緊接着后面是對象占用GC堆的大小。很明顯這個String對象自身占用了24字節。這里強調是“占用”的大小是因為對象除了存儲必要的數據需要空間外,為了滿足數據對齊的要求可能會有一部分空間作為填充數據而空占着。
String在內存中的布局是:
-----------------------
(+0) | _mark |
-----------------------
(+4) | _metadata |
-----------------------
(+8) | value |
-----------------------
(+12)| offset |
-----------------------
(+16)| count |
-----------------------
(+20)| hash |
-----------------------
32位HotSpot上要求64位/8字節對齊,String占用的24字節正好全部都是有效數據,不需要填充空數據。
上面的String實例在內存中的實際數據如下:
| 偏移量(字節) |
數值(二進制表示) |
數值(十六進制表示) |
寬度(位/字節) |
| +0 |
00000000000000000000000000000001 |
00000001 |
32位/4字節 |
| +4 |
01110000111001101100011010100000 |
70e6c6a0 |
32位/4字節 |
| +8 |
01110001000000001011000101011000 |
7100b158 |
32位/4字節 |
| +12 |
00000000000000000000000000000000 |
00000000 |
32位/4字節 |
| +16 |
00000000000000000000000000000100 |
00000004 |
32位/4字節 |
| +20 |
00000000000000000000000000000000 |
00000000 |
32位/4字節 |
OK,那我們來每個成員域都過一遍,看看有何玄機。
第一個是_mark。在HotSpot的C++代碼里它的類型是markOop,在SA里以sun.jvm.hotspot.oops.Mark來表現。
它屬於對象頭(object header)的一部分,是個多用途標記,可用於記錄GC的標記(mark)狀態、鎖狀態、偏向鎖(bias-locking)狀態、身份哈希值(identity hash)緩存等等。它的可能組合包括:
| 比特域(名字或常量值:位數) |
標識(tag) |
|
狀態 |
| 身份哈希值:25, 年齡:4, 0:1 |
01 |
|
未鎖 |
| 鎖記錄地址:30 |
00 |
|
被輕量級鎖住 |
| monitor對象地址:30 |
10 |
|
被重量級鎖住 |
| 轉向地址:30 |
11 |
|
被GC標記 |
| 線程ID:23, 紀元:2, 年齡:4, 1:1 |
01 |
|
被偏向鎖住/可被偏向鎖 |
例子中的"main"字符串的_mark值為1,也就是說它:
- 沒有被鎖住;
- 現在未被GC標記;
- 年齡為0(尚未經歷過GC);
- 身份哈希值尚未被計算。
HotSpot的GC堆中許多創建沒多久的對象的_mark值都會是1,屬於正常現象。
接下來看SA輸出的日志中寫為_klass而在我的圖示上寫為_metadata的這個域。
在HotSpot的C++代碼里,oopDesc是所有放在GC堆上的對象的頂層類,它的成員就構成了對象頭。HotSpot在C++代碼中用instanceOopDesc類來表示Java對象,而該類繼承oopDesc,所以HotSpot中的Java對象也自然擁有oopDesc所聲明的頭部。
hotspot/src/share/vm/oops/oop.hpp:
- class oopDesc {
- private:
- volatile markOop _mark;
- union _metadata {
- wideKlassOop _klass;
- narrowOop _compressed_klass;
- } _metadata;
- };
_metadata與前面提過的_mark一同構成了對象頭。
_metadata是個union,為了能兼容32位、64位與開了壓縮指針(CompressedOops)等幾種情況。無論是這個union中的_klass還是_compressed_klass域,它們都是用於指向一個描述該對象的klass對象的指針。SA的API屏蔽了普通指針與壓縮指針之間的差異,所以就直接把_metadata._klass稱為了_klass。
對象頭的格式是固定的,而對象自身內容的布局則由HotSpot根據一定規則來決定。Java類在被HotSpot加載時,其對象實例的布局與類自身的布局都會被計算出來。這個計算規則有機會以后再詳細寫。
現在來看看"main"這個String對象實例自身的域都是些什么。
value:指向真正保存字符串內容的對象的引用。留意Java里String並不把真正的字符內容直接存在自己里面,而是引用一個char[]對象來承載真正的存儲。
從Java一側看value域的類型是char[],而從HotSpot的C++代碼來看它就是個普通的指針而已。它當前值是0x7100b158,指向一個char[]對象的起始位置。
offset:字符串的內容從value指向的char[]中的第幾個字符開始算(0-based)。int型,32位帶符號整數,這從Java和C++來看都差不多。當前值為0。
count:該字符串的長度,或者說包含的UTF-16字符的個數。類型同上。當前值為4,說明該字符串有4個UTF-16字符。
hash:緩存該String對象的哈希值的成員域。類型同上。當前值為0,說明該實例的String.hashCode()方法尚未被調用過,因而尚未緩存住該字符串的哈希值。
String對象的成員域都走過一遍了,來看看value所指向的對象狀況。
- [C @ 0x7100b158 (object size = 24)
- - _mark: {0} :1
- - _klass: {4} :TypeArrayKlass for [C @ 0x70e60440
- - _length: {8} :4
- - 0: {12} :m
- - 1: {14} :a
- - 2: {16} :i
- - 3: {18} :n
這就是"main"字符串的value所引用的char[]的日志。
[C 是char[]在JVM中的內部名稱。
在@符號之后的0x7100b158是該對象的起始地址。
該對象占用GC堆的大小是24字節。留意了哦。
看看它的成員域。
_mark與_klass構成的對象頭就不重復介紹了。可以留意的是元素類型為原始類型(boolean、char、short、int、long、float、double)的數組在HotSpot的C++代碼里是用typeArrayOopDesc來表示的;這里的char[]也不例外。描述typeArrayOopDesc的klass對象是typeArrayKlass類型的,所以可以看到日志里_klass的值寫着TypeArrayKlass for [C。
接下來是_length域。HotSpot中,數組對象比普通對象的頭要多一個域,正是這個描述數組元素個數的_length。Java語言中數組的.length屬性、JVM字節碼中的arraylength要取的也正是這個值。
日志中的這個數組對象有4個字符,所以_length值為4。
再后面就是數組的內容了。於是該char[]在內存中的布局是:
-----------------------
(+0) | _mark |
-----------------------
(+4) | _metadata |
-----------------------
(+8) | 數組長度 length |
-----------------------
(+12) | char[0] | char[1] |
-----------------------
(+16) | char[2] | char[3] |
-----------------------
(+20) | 填充0 |
-----------------------
Java的char是UTF-16字符,寬度是16位/2字節;4個字符需要8字節,加上對象頭的4*3=12字節,總共需要20字節。但該char[]卻占用了GC堆上的24字節,正是因為前面提到的數據對齊要求——HotSpot要求GC堆上的對象是8字節對齊的,20向上找最近的8的倍數就是24了。用於對齊的這部分會被填充為0。
"main"對象的value指向的char[]也介紹過了,回過頭來看看它的_metadata._klass所指向的klass對象又是什么狀況。
從HotSpot的角度來看,klass就是用於描述GC堆上的對象的對象;如果一個對象的大小、域的個數與類型等信息不固定的話,它就需要特定的klass對象來描述。
instanceOopDesc用於表示Java對象,instanceKlass用於描述它,但自身卻又有些不固定的信息需要被描述,因而又有instanceKlassKlass;如此下去會沒完沒了,所以有個klassKlass作為這個描述鏈上的終結符。
klass的關系圖:
(圖片來源)
回到foo.txt日志文件上來,找到"main"對象的_klass域所引用的instanceKlass對象:
- InstanceKlass for java/lang/String @ 0x70e6c6a0 (object size = 384)
- - _mark: {0} :1
- - _klass: {4} :InstanceKlassKlass @ 0x70e60168
- - _java_mirror: {60} :Oop for java/lang/Class @ 0x70e77760
- - _super: {64} :InstanceKlass for java/lang/Object @ 0x70e65af8
- - _size_helper: {12} :6
- - _name: {68} :#java/lang/String @ 0x70e613e8
- - _access_flags: {84} :134217777
- - _subklass: {72} :null
- - _next_sibling: {76} :InstanceKlass for java/lang/CharSequence @ 0x70e680e8
- - _alloc_count: {88} :0
- - _array_klasses: {112} :ObjArrayKlass for InstanceKlass for java/lang/String @ 0x70ef6298
- - _methods: {116} :ObjArray @ 0x70e682a0
- - _method_ordering: {120} :[I @ 0x70e61330
- - _local_interfaces: {124} :ObjArray @ 0x70e67998
- - _transitive_interfaces: {128} :ObjArray @ 0x70e67998
- - _nof_implementors: {268} :0
- - _implementors[0]: {164} :null
- - _implementors[0]: {168} :null
- - _fields: {132} :[S @ 0x70e68230
- - _constants: {136} :ConstantPool for java/lang/String @ 0x70e65c38
- - _class_loader: {140} :null
- - _protection_domain: {144} :null
- - _signers: {148} :null
- - _source_file_name: {152} :#String.java @ 0x70e67980
- - _inner_classes: {160} :[S @ 0x70e6c820
- - _nonstatic_field_size: {196} :4
- - _static_field_size: {200} :4
- - _static_oop_field_size: {204} :2
- - _nonstatic_oop_map_size: {208} :1
- - _is_marked_dependent: {212} :0
- - _init_state: {220} :5
- - _vtable_len: {228} :5
- - _itable_len: {232} :9
- - serialVersionUID: {368} :-6849794470754667710
- - serialPersistentFields: {360} :ObjArray @ 0x74e882c8
- - CASE_INSENSITIVE_ORDER: {364} :Oop for java/lang/String$CaseInsensitiveComparator @ 0x74e882c0
還記得上文提到過的String類的3個靜態變量么?有沒有覺得有什么眼熟的地方?
沒錯,在HotSpot中,Java類的靜態變量就是作為該類對應的instanceKlass的實例變量出現的。上面的日志里最后三行描述了String的靜態變量所在。
這是件非常自然的事:類用於描述對象,類自身也是對象,有用於描述自身的類;某個類的所謂“靜態變量”就是該類對象的實例變量。很多對象系統都是這么設計的。HotSpot的這套oop體系(指“普通對象指針”,不是指“面向對象編程”)繼承自
Strongtalk,實際上反而比暴露給Java的對象模型顯得更加面向對象一些。
HotSpot並不把instanceKlass暴露給Java,而會另外創建對應的java.lang.Class對象,並將后者稱為前者的“Java鏡像”,兩者之間互相持有引用。日志中的_java_mirror便是該instanceKlass對Class對象的引用。
鏡像機制被認為是良好的面向對象的反射與元編程設計的重要機制。Gilad Bracha與David Ungar還專門寫了篇論文來闡述此觀點,參考
Mirrors: Design Principles for Meta-level Facilities of Object-Oriented Programming Languages。
順帶把"main"對象的_klass鏈上余下的兩個對象的日志也貼出來:
- InstanceKlassKlass @ 0x70e60168 (object size = 120)
- - _mark: {0} :1
- - _klass: {4} :KlassKlass @ 0x70e60000
- - _java_mirror: {60} :Oop for java/lang/Class @ 0x70e76f20
- - _super: {64} :null
- - _size_helper: {12} :0
- - _name: {68} :null
- - _access_flags: {84} :0
- - _subklass: {72} :null
- - _next_sibling: {76} :null
- - _alloc_count: {88} :0
所有instanceKlass對象都是被這個instanceKlassKlass對象所描述的。
- KlassKlass @ 0x70e60000 (object size = 120)
- - _mark: {0} :1
- - _klass: {4} :KlassKlass @ 0x70e60000
- - _java_mirror: {60} :Oop for java/lang/Class @ 0x70e76e00
- - _super: {64} :null
- - _size_helper: {12} :0
- - _name: {68} :null
- - _access_flags: {84} :0
- - _subklass: {72} :null
- - _next_sibling: {76} :null
- - _alloc_count: {88} :0
而所有*KlassKlass對象都是被這個klassKlass對象所描述的。
klass對象的更詳細的介紹也留待以后再寫吧~至少得找時間寫寫instanceKlass與vtable、itable的故事。
Java static變量保存在哪?
測試環境:
Microsoft Windows [版本 10.0.17134.165]
java -version
java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)
測試代碼:
import java.io.IOException;
public class Main {
private static String name = "lgh";
private static int age = 26;
public int fun() {
try {
System.out.println(name);
System.out.println(age);
return System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
return 0;
}
public static void main(String[] args) {
new Main().fun();
}
}
編譯&運行:
D:\N3verL4nd\Desktop>javac Main.java
D:\N3verL4nd\Desktop>java -XX:+UseSerialGC -XX:-UseCompressedOops -Xms10m -Xmx10m Main
lgh
26
System.in.read() 的作用等同於斷點。
使用 CLHSDB 連接:
// 查看進程 id
D:\>jps
5792 Jps
7932 Main
D:\>java -cp .;%JAVA_HOME%/lib/sa-jdi.jar sun.jvm.hotspot.CLHSDB
hsdb> attach 7932
Attaching to process 7932, please wait...
運行 universe:
Heap Parameters:
Gen 0: eden [0x0000000012600000,0x00000000127114d0,0x00000000128b0000) space capacity = 2818048, 39.7239507630814 used
from [0x00000000128b0000,0x00000000128b0000,0x0000000012900000) space capacity = 327680, 0.0 used
to [0x0000000012900000,0x0000000012900000,0x0000000012950000) space capacity = 327680, 0.0 usedInvocations: 0
Gen 1: old [0x0000000012950000,0x0000000012950000,0x0000000013000000) space capacity = 7012352, 0.0 usedInvocations: 0
[eden] 0x00000000128b0000 - 0x0000000012600000 = 2B 0000(1260 0000)
[from] 0x0000000012900000 - 0x00000000128b0000 = 5 0000(120 0000)
[to] 0x0000000012950000 - 0x0000000012900000 = 5 0000(120 0000)
可以看到 eden:from:to 大致比例為8:1:1,可以看到新生代的[eden-from-to]內存是連續的。同時可以看新生代和老年代內存是連着的。大概和垃圾回收方式有關。
掃描我們的 Main 實例:
hsdb> scanoops 0x0000000012600000 0x00000000128b0000 Main
0x000000001270afd8 Main
hsdb> whatis 0x000000001270afd8
Address 0x000000001270afd8: In thread-local allocation buffer for thread "main" (1) [0x0000000012703870,0x000000001270b6e8,0x00000000127114b8,{0x00000000127114d0})
hsdb> inspect 0x000000001270afd8
instance of Oop for Main @ 0x000000001270afd8 @ 0x000000001270afd8 (size = 16)
_mark: 1
_metadata._klass: InstanceKlass for Main
hsdb>
可見,Main 實例分配在了線程私有的 TLAB 中。
Main 類沒有實例變量,所以他的大小是 16 字節,Mark Word + Klass 指針(64 位 JVM 關閉壓縮指針的情況下)。
使用 inspect 命令沒有顯示出來 InstanceKlass 也就是類型指針的地址,據說是 HSDB 的bug。我們使用 mem 來獲取更詳細的信息。
hsdb> mem 0x000000001270afd8 2
0x000000001270afd8: 0x0000000000000001 // Mark Word
0x000000001270afe0: 0x0000000013400598 // 類型指針(與Mark Word 一起組成對象頭)
由於 1 個十六進制位代表 4 個二進制位,所以以上 Mark Word 的最后一位 1 代表的二進制序列為0001。

也就是 Main 實例處在無鎖狀態。
查看該類型指針對應的數據:
hsdb> inspect 0x0000000013400598
Type is InstanceKlass (size of 440)
juint Klass::_super_check_offset: 48
Klass* Klass::_secondary_super_cache: Klass @ null
Array<Klass*>* Klass::_secondary_supers: Array<Klass*> @ 0x0000000013000f88
Klass* Klass::_primary_supers[0]: Klass @ 0x0000000013001c00
oop Klass::_java_mirror: Oop for java/lang/Class @ 0x0000000012709dc8 Oop for java/lang/Class @ 0x0000000012709dc8
或者使用 HSDB :

D:\Java\Tools\jol>java -XX:-UseCompressedOops -jar jol-cli.jar internals java.lang.Class
# Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.
# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
Failed to find matching constructor, falling back to class-only introspection.
java.lang.Class object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 16 (object header) N/A
16 8 java.lang.reflect.Constructor Class.cachedConstructor N/A
24 8 java.lang.Class Class.newInstanceCallerCache N/A
32 8 java.lang.String Class.name N/A
40 8 (alignment/padding gap)
48 8 java.lang.ref.SoftReference Class.reflectionData N/A
56 8 sun.reflect.generics.repository.ClassRepository Class.genericInfo N/A
64 8 java.lang.Object[] Class.enumConstants N/A
72 8 java.util.Map Class.enumConstantDirectory N/A
80 8 java.lang.Class.AnnotationData Class.annotationData N/A
88 8 sun.reflect.annotation.AnnotationType Class.annotationType N/A
96 8 java.lang.ClassValue.ClassValueMap Class.classValueMap N/A
104 40 (alignment/padding gap)
144 4 int Class.classRedefinedCount N/A
148 4 (loss due to the next object alignment)
Instance size: 152 bytes
Space losses: 48 bytes internal + 4 bytes external = 52 bytes total
使用 jol 獲得 Class 對象的大小為 152,也就是 19 個字長。
hsdb> inspect 0x0000000012709dc8
instance of Oop for java/lang/Class @ 0x0000000012709dc8 @ 0x0000000012709dc8 (size = 176)
name: "lgh" @ 0x000000001270af80 Oop for java/lang/String @ 0x000000001270af80
age: 26
hsdb> mem 0x0000000012709dc8 22
0x0000000012709dc8: 0x0000002a139a5501 // 1
0x0000000012709dd0: 0x0000000013013ed0 // 2
0x0000000012709dd8: 0x0000000000000000 // 3
0x0000000012709de0: 0x0000000000000000 // 4
0x0000000012709de8: 0x0000000000000000 // 5
0x0000000012709df0: 0x00000000126e5348 // 6
0x0000000012709df8: 0x000000001270a4c8 // 7
0x0000000012709e00: 0x0000000000000000 // 8
0x0000000012709e08: 0x0000000000000000 // 9
0x0000000012709e10: 0x0000000000000000 // 10
0x0000000012709e18: 0x0000000000000000 // 11
0x0000000012709e20: 0x0000000000000000 // 12
0x0000000012709e28: 0x0000000000000000 // 13
0x0000000012709e30: 0x00000000127097d0 // 14
0x0000000012709e38: 0x0000000000000000 // 15
0x0000000012709e40: 0x0000000000000000 // 16
0x0000000012709e48: 0x0000000013400598 // 17 類型指針
0x0000000012709e50: 0x0000000000000000 // 18
0x0000000012709e58: 0x0000001600000000 // 19
0x0000000012709e60: 0x0000000000000001 // 20
0x0000000012709e68: 0x000000001270af80 // 21 "lgh" 的引用
0x0000000012709e70: 0x000000000000001a // 22 "26" 的 16 進制表示
可以看到 static 變量保存在 Class 實例的尾部。
Class 對象確實在堆中。
類型指針保存在 Class 實例 17 * 8 的位置上。
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7017732
Currently static fields are stored in the instanceKlass but when those are moved into native memory we'd have to have a new card mark strategy for static fields. This could be something like setting a flag in the instanceKlass and then rescanning every klass during a GC which seems expensive or marking the card for the java.lang.Class then making sure to scan the instanceKlass when scanning the Class. If we move them into the Class then almost all the existing machinery works exactly as it always has. The only execution difference is which constant is materialized for the field access.
目前靜態字段存儲在instanceKlass中,但當這些字段被移動到本機內存中時,我們必須為靜態字段使用新的卡片標記策略。這可能類似於在instanceKlass中設置一個標志,然后在GC期間重新掃描每個klass(這似乎很昂貴),或者為java.lang.Class標記卡片,然后確保在掃描類時掃描instanceKlass。如果我們把它們放到課堂上,那么幾乎所有現有的機器都會像往常一樣工作。唯一的執行差異是字段訪問具體化了哪個常量。