温故而知新,可以为师矣

前不久又复习了一下 JVM 的类加载机制,并对上一篇类加载的博客进行了部分修正,也添加了部分知识点,第二篇也是对第一篇类加载机制博客的延伸,总结与实践;

上篇类加载机制博客指路地址:JVM 中类的加载

延伸

Klass 模型

  • Java 在 JVM 中的内存形式
  • Java 类在 C++ 的类 Klass

Klass 又可以分为两类:

  • 非数组类

    • InstanceKlass

      这是普通的类在 C++ 源码中的体现形式,类的元信息在 JVM 中对应的就是 InstanceKlass,存在于方法区中,用于存储该类的访问权限,方法信息等。

    • InstanceMirrorKlass

      这是类的实例在 C++ 源码中的体现形式,存在于堆区,类的静态属性也是存储在 MirrorKlass 中的,处于运行时内存中的。

  • 数组类

    • 基本数据类型

      • boolean
      • char
      • byte
      • short
      • int
      • long
      • float
      • double

      以上的基础类型对应的是 TypeArrayKlass

    • 引用数据类型

      对应的是 ObjArrayKlass

总结

链接阶段

从上一篇博客中可知,类的链接阶段分为:验证,准备和解析三个阶段。

  • 验证阶段无非就是检查你这个代码有没有问题,满不满足 JVM 的约束条件;
  • 准备阶段中,会给静态字段分配内存,但不会对静态字段进行初始化,静态字段的初始化会在初始阶段进行;其次要注意,如果字段被 final 修饰了,编译的时候就会给字段加上 ConstantValue 属性,就算同时被 static 修饰,也不再遵循前面的规则,会直接在准备阶段完成赋值,待会进行验证。
  • 解析阶段,简单来说就是将符号引用解析成直接引用,符号引用就是静态常量池的索引,直接引用就是内存地址。

初始化阶段

这里把之前我写的那些玩意儿再精炼一下:

初始化阶段就是对静态字段进行赋值,静态字段和静态代码块会被编译到一个叫做 clinit 的方法中,就算写了再多的静态代码块和静态属性,一个字节码文件最终只有一个 clinit(下文验证)。

初始化时按照静态字段和静态代码块的顺序,在 clinit 中初始化各个静态字段的值。

验证

C++ 中的 Klass 模型

一下代码来自 HotSpot 源码的 klass.hpp 文件,这个就是 Java 类在 C++ 中的模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class Klass : public Metadata {
friend class VMStructs;
protected:
// note: put frequently-used fields together at start of klass structure
// for better cache behavior (may not make much of a difference but sure won't hurt)
enum { _primary_super_limit = 8 };

// The "layout helper" is a combined descriptor of object layout.
// For klasses which are neither instance nor array, the value is zero.
//
// For instances, layout helper is a positive number, the instance size.
// This size is already passed through align_object_size and scaled to bytes.
// The low order bit is set if instances of this class cannot be
// allocated using the fastpath.
//
// For arrays, layout helper is a negative number, containing four
// distinct bytes, as follows:
// MSB:[tag, hsz, ebt, log2(esz)]:LSB
// where:
// tag is 0x80 if the elements are oops, 0xC0 if non-oops
// hsz is array header size in bytes (i.e., offset of first element)
// ebt is the BasicType of the elements
// esz is the element size in bytes
// This packed word is arranged so as to be quickly unpacked by the
// various fast paths that use the various subfields.
//
// The esz bits can be used directly by a SLL instruction, without masking.
//
// Note that the array-kind tag looks like 0x00 for instance klasses,
// since their length in bytes is always less than 24Mb.
//
// Final note: This comes first, immediately after C++ vtable,
// because it is frequently queried.
jint _layout_helper;

// The fields _super_check_offset, _secondary_super_cache, _secondary_supers
// and _primary_supers all help make fast subtype checks. See big discussion
// in doc/server_compiler/checktype.txt
//
// Where to look to observe a supertype (it is &_secondary_super_cache for
// secondary supers, else is &_primary_supers[depth()].
juint _super_check_offset;

// Class name. Instance classes: java/lang/String, etc. Array classes: [I,
// [Ljava/lang/String;, etc. Set to zero for all other kinds of classes.
Symbol* _name;

// Cache of last observed secondary supertype
Klass* _secondary_super_cache;
// Array of all secondary supertypes
Array<Klass*>* _secondary_supers;
// Ordered list of all primary supertypes
Klass* _primary_supers[_primary_super_limit];
// java/lang/Class instance mirroring this class
oop _java_mirror;
// Superclass
Klass* _super;
// First subclass (NULL if none); _subklass->next_sibling() is next one
Klass* _subklass;
// Sibling link (or NULL); links all subklasses of a klass
Klass* _next_sibling;

// All klasses loaded by a class loader are chained through these links
Klass* _next_link;

// The VM's representation of the ClassLoader used to load this class.
// Provide access the corresponding instance java.lang.ClassLoader.
ClassLoaderData* _class_loader_data;

jint _modifier_flags; // Processed access flags, for use by Class.getModifiers.
AccessFlags _access_flags; // Access flags. The class/interface distinction is stored here.

// Biased locking implementation and statistics
// (the 64-bit chunk goes first, to avoid some fragmentation)
jlong _last_biased_lock_bulk_revocation_time;
markOop _prototype_header; // Used when biased locking is both enabled and disabled for this type
jint _biased_lock_revocation_count;

JFR_ONLY(DEFINE_TRACE_ID_FIELD;)

// Remembered sets support for the oops in the klasses.
jbyte _modified_oops; // Card Table Equivalent (YC/CMS support)
jbyte _accumulated_modified_oops; // Mod Union Equivalent (CMS support)
...
}

被 final 修饰的字段不会加入 clinit

先来段示例代码:

1
2
3
4
5
6
7
public class Test001 {
private int a = 10;
private static int b = 20;
private static int c;
private static final int d = 20;
private final int e = 20;
}

接下来在 IDEA 中通过 jclasslib 工具查看 JVM 字节码:

JVM-final-clinit

这里可以看到 静态常量d常量e 被加上了 ConstantValue 属性,而静态常量d 则没有被加入到 clinit 中,说明字段d 并不会在初始化中进行赋值,而是在链接阶段中的准备阶段就已经赋值完成了。

同时可以看到这里一共有两个静态变量 b 和 c,但是只有一个 clinit;