背景
从 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。如果我们把它们放到课堂上,那么几乎所有现有的机器都会像往常一样工作。唯一的执行差异是字段访问具体化了哪个常量。