首先我们要知道:
在jvm的内存结构中,对象保存在堆中,而我们在对对象进行操作时,其实操作的是对象的引用。
一个Java对象可以分为三部分存储在内存中,分别是:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
Java虚拟机的底层是使用c++实现,而jvm并没有根据一个Java的实例对象去创建对应的c++对象,而是设计了一个oop-klass model ;
类就是一类事物的抽象概括。
代码:
// 定义了oop共同的基类,其他的类型都是它的子类
/* 表示一个Java类型实例,每当我们new一个对象时,
JVM都会创建一个instanceOopDesc */
// 表示一个Java方法
// 表示一个Java方法中的不变信息
// 记录性能信息的数据结构
/* 定义了数组oops的抽象基类,下面两个类相当于此类的子类,
new一个数组时会建立此对象*/
// 表示持有一个oops数组,对应存储对象的数组
// 表示容纳基本类型的数组,对应存储基本类型的数组
// 表示在class文件中描述的常量池
// 常量池缓存
// 描述一个与Java类对等的C++类
// 表示对象头
OopDesc结构:
class oopDesc {
friend class VMStructs;
private:
/*
* 实际上也是代表了instanceOopDesc、arrayOopDesc和OopDesc
* 包含了markOop _mark和union_matadata两部分
*/
volatile markOop _mark; // 保存锁标记、gc分代等信息
union _metadata { wideKlassOop _klass; // 普通指针,
// 压缩类指针,和普通指针都指向instanceKlass 对象
narrowOop _compressed_klass; } _metadata;
private:
// 实例数据保存的位置
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;
address* address_field_addr(int offset) const; }
/* instanceOopDesc和arrayOopDesc都直接继承了oopDesc,
都没有增加其他的数据结构 */
class instanceOopDesc : public oopDesc {
}
class arrayOopDesc : public oopDesc { }
结构:
// klassOop的一部分,用来描述语言层的类型,其他所有类的父类
// 在虚拟机层面描述一个Java类,每一个已加载的Java类都会创建一个此对象,
// 在JVM层表示Java类
// 专有instantKlass,表示java.lang.Class的Klass
// 专有instantKlass,表示java.lang.ref.Reference的子类的Klass
// 表示arrayKlass的Klass
// 表示constantPoolCacheOop的Klass
功能:
目的:为了实现虚函数多态,提供了虚函数表。
// 类拥有的方法
//描述方法顺序
//实现的接口
//继承的接口
//域
//常量
//类加载器
//protected域
....
HotSpotJVM的设计这把对象一拆为二,分为Klass和oop,其中oop的只能主要在于表示对象的实例数据,所以其中不含有任何虚函数,而klass为了实现虚函数多态,所以提供了虚函数表。所以,关于java的多态,其实也有虚函数的影子在。
JVM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个instanceKlass
对象,用来在JVM层表示Java类。
结构:
//类拥有的方法列表
objArrayOop _methods;
//描述方法顺序
typeArrayOop _method_ordering;
//实现的接口
objArrayOop _local_interfaces;
//继承的接口
objArrayOop _transitive_interfaces;
//域
typeArrayOop _fields;
//常量
constantPoolOop _constants;
//类加载器
oop _class_loader;
//protected域
oop _protection_domain;
....
在JVM中,对象在内存中的基本存在形式就是oop。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。
klassKlass作为oop的klass链的端点。关于对象和数组的klass链大致如下图:
符号引用就是用一组符号来描述所引用的目标,我们都知道在Java中,通常情况下我们写的一个Java类被编译以后都是一个class文件,在编译的时候,Java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
一般一个对象的创建就是从new开始的,而操作这些创建指令的就是jvm了,首先当你开始new的时候,jvm会先去查找一个符号引用,如果找不到这个符号引用就说明这个类还没有被加载,因此jvm就会进行类加载,然后符号引用被解析完成,紧接着jvm会为对象在堆内存中分配内存,也就是说我们这个user对象就在堆内存中有一块内存空间了。
HotSpot虚拟机实现的Java对象包括三个部分:对象头,实例字段和对齐填充。
为对象分配完堆内存之后,jvm会将该内存进行零值初始化。
关于一个Java对象,他的存储是怎样的,一般很多人会回答:对象存储在堆上。稍微好一点的人会回答:对象存储在堆上,对象的引用存储在栈上。今天,再给你一个更加显得牛逼的回答:
对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。
其实如果细追究的话,上面这句话有点故意卖弄的意思。因为我们都知道。方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 所谓加载的类信息,其实不就是给每一个被加载的类都创建了一个 instantKlass对象么。
class Model{
public static int a = 1;
private final int NUMBER = 2;
public int b;
public int c = 3;
public Model(int b){
this.b = b;
}
public static void main(String[] args){
int d = 10;
Model modelA = new Model(2);
Model modelB = new Model(3);
}
}
存储结构:
在Java中,JVM中的对象模型包含两部分:Oop和Klass,在类被加载的时候,JVM会给类创建一个instanceKlass,其中包含了类信息、常量、静态变量、即时编译器编译后的代码等,存储在方法区,用来在JVM层表示该Java类。而使用new一个对象后,JVM就会创建一个instanceOopDesc对象,该对象包含对象头和实例数据,对象头中保存的是锁的状态标志等信息,元数据则实际上是一个指针,指向instanceKlass。
对象自身的运行时数据
这部分存储包括哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据被官方称为Mark Word
,在32位和64位的虚拟机中的大小分别为32bit和64bit。
由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word
被设计成一个非固定的数据结构以提高存储空间的利用率。即这部分数据会根据对象的状态来分配存储空间。
对象的类型指针
即指向对象的类元数据的指针。虚拟机可以通过该指针判定对象实例属于哪个类。
在Java对象中比较特殊的是Java数组,一个数组实例的对象头中必须记录数组的长度。JVM可以通过对象头中的数组长度数据来判定数组的大小,这是访问数组类型的元数据无法得到的。
对象的实例数据
前面提到对象头是对象的额外开销,只有实例数据才是一个对象实例存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。这部分内容同时记录了子类从父类继承所得的各类型数据。
填充
对齐填充在对象数据中并不是必然的,只是起着占位符的作用,没有特别含义。HotSpot要求对象起始地址必须是8字节的整数倍。对象头的大小刚好符合要求,因此当实例数据没有对齐时,就需要通过填充来对齐数据。
获取类的元数据
虚拟机在加载类的时候会将类的信息、常量、静态变量和即时编译器编译后的代码等数据存储在方法区(Method Area)。类的元数据,即类的数据描述,也被存在方法区。我们知道对象头中会存有对象的类型指针,通过类型指针可以获取类的元数据。因此,对象的类型指针其实指向的是方法区的某个存有类信息的地址。
但是,并不是每个对象实例都存有对象的类型指针。根据对象访问定位方法的不同,对象的类型指针被存放在不同的区域。
比较来说:
因此,Java的对象数据存储可以理解为:
对象五种状态:无锁态、轻量级锁、重量级锁、GC标记和偏向锁。
HotSpot中对象头主要包含两部分
第一:
用于存储对象自身的运行时数据,如上表中的对象哈希码,对象分代年龄,偏向线程id,偏向时间戳等。
第二:
类型指针了,我们看表中也有指针字样,那么这部分主要就是杜希昂指向它的类元数据的指针了,虚拟机就是通过这个指针来确定这个对象时那个实例。
偏向锁和重量级锁
参考资料:
《深入理解Java虚拟机》
http://www.cnblogs.com/chenyangyao/p/5245669.html
深入理解多线程(二)—— Java的对象模型-HollisChuang's Blog
深入理解多线程(三)—— Java的对象头-HollisChuang's Blog