上一节JVM拾遗(3): 类装载机制讲了JVM如何将类装载到虚拟机以供后续使用
那么JVM是如何创建类的实例呢?该对象是如何分配内存的?
1. 实例化
1.1 创建对象的方式
Java对象的创建, 有多种方式,最简单就是new XXClass, 还可以通过反射,xx.clone(),反序列化以及黑科技Unsafe.allocateInstance等方法.
new和反射创建对象实例的时候,会初始化实例字段.
如果类没有构造器,会默认添加构造器,并且编译成<init>方法.
默认生成的构造器里,如果父类有无参构造器, 会隐式递归调用父类的构造器.
如下类:
public class TestClass {
public void test() {
TestClass t = new TestClass();
}
}
生成的字节码如下(用javap -v TestClass可以得到):
public TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class TestClass
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
那两行invokespecial指令是用来调用类构造器或者类的私有方法的.
可以看到,TestClass类添加了默认的构造器,生成了<init>方法,同时调用了Object类的<init>方法,也就是java.lang.Object类的构造方法.
也可以显式调用父类的构造器, 使用super(a,b,c)这样的形式即可
1.2 虚拟机如何处理new指令
new指令会实例化一个对象, JVM如何处理new指令生成具体的对象并不是规范的一部分,所以这里指的JVM实现指的是Hotspot的实现
下面是new指令实例化对象的过程:
- JVM会判断对应的类是否被加载,链接,初始化, 判断的依据是根据符号引用找方法区是否有类的数据.如果没有需要进行类的加载过程
- 为对象分配内存, 有2种方式
- 指针提升(bump pointer, 注意没翻译错), 如果
堆内存是规整的, 那么只需要把指针移动一个对象大小的距离就完成了分配 - 空闲列表(free list), 如果
堆内存是不连续的,碎片很多, JVM会维护一个可用内存队列, 从中查出可用内存分配给对象.
具体使用哪种方式依据堆内存的情况而定,而堆内存的情况很大受到gc的影响,后面会细谈
- 指针提升(bump pointer, 注意没翻译错), 如果
- 初始化内存, JVM会将分配到的内存初始化为0值.
- 设置对象的元数据信息到
对象头, 例如对象的hashcode,gc分带年龄,偏向锁状态等信息
一般来讲,new指令后面都会跟着invokespecial来执行<init>方法,也就是执行类构造器里的逻辑.
对象创建的并发安全问题
JVM处理对象创建的并发安全一般有2种方法:
CAS操作加上失败重试- 内存分配的动作按照线程划分在不同的内存空间区域进行,这些预先分配的内存仅属于该线程私有,也称为
本地线程分配缓冲(Thread Local Allocation Buffer, TLAB), 可以通过-XX:UseTLAB来设置
2. 对象的内存布局
对象的内存布局也不是JVM规范的一部分,属于实现的细节,Hotspot将对象分成3部分,分别是:
- 对象头(Object Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
2.1 Object Header
对象头由标记字段(Mark Word)和类型指针(Klass Pointer)组成
Mark Word用于存储该对象的运行时数据, 包括:
- 对象
hashcode,gc分代年龄 - 锁记录指针, 重量级锁指针
- 偏向线程id, 偏向时间戳
在64位的虚拟机上标记字段一般是8个字节,类型指针也是8个字节,总共就是16个字节.
可以使用-XX:UseCompressedOops来开启压缩指针, 以减少对象的内存使用量, 默认是开启的.
而类型指针指向的是对象的元数据信息, 也就是对象所属类的信息.
HotSpot实现
Hotspot一般用OOP-Klass二分模型来实现, OOP(Ordinary Object Pointer)是普通对象指针,Klass用来描述具体的类型.
以下是openjdk的实现:
// hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
friend class VMStructs;
private:
// mark word真身
volatile markOop _mark;
// 元数据指针
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
...
public:
markOop mark() const { return _mark; }
Klass* klass() const;
narrowKlass* compressed_klass_addr();
private:
// field addresses in oop
void* field_base(int offset) const;
jbyte* byte_field_addr(int offset) const;
jchar* char_field_addr(int offset) const;
jboolean* bool_field_addr(int offset) const;
jint* int_field_addr(int offset) const;
jshort* short_field_addr(int offset) const;
jlong* long_field_addr(int offset) const;
jfloat* float_field_addr(int offset) const;
jdouble* double_field_addr(int offset) const;
Metadata** metadata_field_addr(int offset) const;
...省略非关键代码...
其中OOP的实现就是instanceOopDesc和arrayOopDesc,分别是普通对象实现和数组对象实现, 均继承自上面的oopDesc,
数组对象比普通对象多一个长度字段.oopDesc存放有_mark和_metadata, _mark就是标记字段(mark word),
而_metadata里就有一个指针指向Klass.
限于篇幅,markOop.hpp的代码就不贴了,大致就是上面提到的信息。
类型指针(上面_meta里__klass指针)指向的Klass信息, 即类在JVM的真身, 看看就好.
// hotspot/src/share/vm/oops/instanceKlass.hpp
class InstanceKlass: public Klass {
...
// See "The Java Virtual Machine Specification" section 2.16.2-5 for a detailed description
// of the class loading & initialization procedure, and the use of the states.
// 类的加载状态, JVM规范里的各个阶段可能会穿插进行
enum ClassState {
allocated, // allocated (but not yet linked)
loaded, // loaded and inserted in class hierarchy (but not linked yet)
linked, // successfully linked/verified (but not initialized yet)
being_initialized, // currently running class initializer
fully_initialized, // initialized (successfull final state)
initialization_error // error happened during initialization
};
...
}
// hotspot/src/share/vm/oops/klass.hpp
class Klass : public Metadata {
friend class VMStructs;
protected:
jint _layout_helper;
juint _super_check_offset;
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;
TRACE_DEFINE_KLASS_TRACE_ID;
// 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)
...省略不重要的部分...
类的各种数据非常多,不过都是实现细节,为了更好的存储JVM规范里的class file format对应的数据,如类的名字符号,符号引用,常量池数据, 方法引用等等
而继承的类Metadata如下:
// hotspot/src/share/vm/oops/metadata.hpp
class Metadata : public MetaspaceObj {
public:
NOT_PRODUCT(Metadata() { _valid = 0; })
NOT_PRODUCT(bool is_valid() const volatile { return _valid == 0; })
...一堆辅助函数...
int identity_hash() { return (int)(uintptr_t)this; }
...
没错,这个identity_hash函数就是hashcode()函数的真身, 可以看到返回了对象地址:).
而Metadata继承的MetaspaceObj则是8以后才有的, java8移除了永久代(PermGen),可能因为经常溢出(Spring: 你们看我干啥?), 生活在元空间的对象都要继承这个类,这个放后面聊.
2.2 Instance Data
实例数据存在instanceOopDesc的父类oopDesc类的实例里,就是上面那堆私有属性, JVM还会对字段重排序,相同的宽度可能被分到一起,比如long/double.所以父类的变量可能出现在子类之前,子类中较窄的变量也可能插入到父类的间隙。
Hotspot采用的方法是直接指针访问对象, 如图:
计算对象的大小可以单独讲一小节,也比较有意思,后面再说。
2.3 Padding
对齐填充是最常见的优化手段,CPU一次寻址一般是2的倍数,所以一般会按照2的倍数来对齐提高CPU效率.这个似乎没什么好讲的.
此外,JVM上对齐填充也方便gc, JVM能直接计算出对象的大小, 就能快速定位到对象的起始终止地址.
3. 总结
本节回顾了java对象的创建方式,以及hotspot执行new指令的一系列操作,包括寻找加载类,分配初始化内存,设置对象的元数据信息到header。
同时我们通过代码分析了hotspot中对象的内存布局,分为Object Header, Instance Data和Padding, 其中对象头包括mark word和klass pointer, mark word里有丰富的对象运行时信息,比如hashcode,gc分代年龄等,而klass pointer指向一个c++对象Klass的实例,也就是JVM类的真身.存放的类的各种信息,如类的名称,继承的父类,实现的接口,字段信息,方法信息等.而Instance Data存放了运行时类的实例数据值,
字段数据的地址会被重排序优化掉。
instanceKlass对象放在了方法区,instanceOop放在了堆,instanceOop的引用放在了JVM栈.
希望大家有所收获:).