Java 是一门纯面向对象的语言,因此对象在 Java 中的地位相当之高,大部分的操作都是围绕着对象进行展开,对 Java 对象的深入理解也是很有必要的,不能仅仅停留在关键字 new上,对于 new 的过程也应该了如指掌,清除了这个,才能更好的理解 Java 的对象。Java 对象一般实例化在堆上,所以首先了解一下在 Java 堆中的对象到底是如何的创建、分配空间、以及对象的布局和访问的过程
对象的创建
先来看一个简单的代码
public class User {
int age;
String name;
public User(String name, int age) {
this.age = age;
= name;
}
public User() {
}
...
// 省略 getter 和 setter 代码
}
User user1 = new User();
User user2 = new User("张三",16);上面的代码很简单,定义一个类并且实例化一个子类,那在 new 的过程中到底会有多少动作产生?
首先我 new 的是一个 User,那么我会先检查这个 User类是否存在,那么就会去常量池中去找是不是有这个类的符号引用,找到了说明你定义过这个类,基本上可以消除编译错误了,没有就会有编译错误。找到了之后只能说明从程序员的角度是已经成功了,但是对于虚拟机来说只是第一步。虽然说找到这个类的定义了,但是并不意味着就可以直接拿过来进行实例化,我得检查这个类是不是加载、解析、初始化过,比如这个类有哪些字段和方法,这些不经过加载解析初始化是无法得知的,那我实例化时就不知道该如何安排这个实例。
现在假设对应的类已经初始化过了,我就要给实例分配在堆上划出一块空间分配给这个实例(这个空间的大小会在类的加载解析初始化时计算出来,所以不必担心)。等等!如果多个实例同时分配内存时,分配的地址冲突了怎么办?说实话,如果不看书的话根本不会想到这个问题,由于多线程的存在,多个线程同时进行对象的创建的时候,就可能在相同的地址分配不同的对象,虚拟机采用的方法是 CAS 配上失败重试。或者还有一个方法,我们可以给每个线程预先分配一小块堆空间,当发生对象创建的时候,优先使用这一块区域进行分配,从而能够降低冲突的概率,这个操作可以通过虚拟机参数 -XX:+/-UseTLAB 进行开启或关闭。TLAB 全程是 Thread Local Allocation Buffer 意为本地线程分配缓冲。
下一步,元信息好了,该设置一些必要的属性信息了,我这里提供了两个构造函数,分别用他们创建了实例
User user1 = new User();
User user2 = new User("张三",16);对于第一个创建出来的实例 user1,现在我进行如下的操作
System.out.println(user1.getName());
System.out.println(user1.getAge());出来的结构应该是 ""和 0 ,而对于 user2 调用相同的方法,结果应该是 "张三" 和 16,这就说明我在未赋值的情况下系统给我默认的初始值不至于让我访问的时候报错,这就是第三步的作用,对于类的一些变量进行默认的赋值保证我能正常使用。
到这,对象已经有了安身立命的场所,也有了初始的身份,但是我创建这个对象出来得管着它不是,我得知道这个对象的所属类、对象的哈希码(简单理解就是地址)、GC 分代信息(好对它进行垃圾回收或者晋升老年代等操作)等等,这些信息统统保存在对象头里面(一般提到头,保存的都是元数据,即对这个类的信息进行描述的信息)。最后一步就是将用户设置的值进行赋值,这就是构造函数的作用,同时也是最后一步干的事。
至此,一个对象的创建过程就展示完毕,当然这个过程还不是最完善的,就好比你简单的将浏览器输入一个网址的过程访问分析了一遍,但是中间其实还有很多的地方你并没有提及到,没有提及的原因可能是了解的不够多,这个也一样,当学到后面的时候,就会发现中间其实很多地方都能展开一大段,比如类的加载、解析、初始化等。学到一定的知识后当能融会贯通,从原理分析,不是死记硬背,要做到看山不是山(当然最高境界还是看山还是山)。

对象的布局
一个对象的内存布局包含了三块内容:对象头、实例数据、对齐填充。对象头听起来很陌生,看起来是描述类的信息,所以我们先从简单的讲起,陌生留在后面一起解决
首先是实例数据,很好理解,就是一个类的实例数据,但是这里要注意的是,这里的实例数据不仅仅是是本类定义的数据字段,还包括了从父类继承下来的数据字段。对齐填充,学过 C 语言的可能知道内存对齐,这个概念和那个差不多,因为 Java 虚拟机要求对象的起始地址是 8 的倍数,所以有时候就需要进行对齐填充进行补全,对齐填充也有好处,因为地址都是 8 的倍数,访问起来就比较方便,地址定位也比较容易。
剩下的就是对象头,刚接触这个对象头时也是很懵逼的,对里面很对参数也是不了解的,慢慢的随着对虚拟机的了解以及 Java 语言层面的了解,回过头再看的时候也不是那么晦涩了。
为什么会有对象头呢,因为创建一个对象要对它进行管理,而一个对象参与的面很多:从线程的角度进行讲,对象肯定是在某个线程中进行创建的,那么线程的信息得记录下来;从内存的角度讲,这个对象分配在哪个地方我得记录下来,这就是哈希码(哈希码可以简单的理解为地址,但是打印后发现和地址格式不太一样,应该是系统的堆地址经由虚拟机管理后使用虚拟机的描述方法对内存地址进行描述的);我们在进行多线程编程的时候,会对对象进行加锁,这就得对对象的锁信息进行记录;这个对象是哪个类实例化的,以后通过这个实例找到这个类我可能用得找,所以类信息也得记录下来。。。所以基于上述的一些方面,对象头的存在很有必要。
而对上面列举出的方面进行划分,可以将对象头划分为两个部分:一个就是类型指针,即对象指向该类的指针,剩余的部分组成另一部分,我们称它为运行时数据,使用称为 Mark Word 的 32bit 或者 64bit 的结构进行存储
存储内容 | 标志位 | 状态 |
对象哈希码、分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC 标志 |
偏性锁ID、偏向时间戳、分代年龄 | 01 | 可偏向 |
有关 Mark Word 的信息可以参考其他的资料,这里不再赘述,毕竟都是死的知识。
对象的访问
Java 虚拟机使用的是直接指针访问,即创建一个对象之后,变量引用指向的地方就是堆中的地址。而另一种通过在引用和堆地址之间增加一层句柄池。
总结
我们在理解虚拟机的时候,不能陷入书中提供的知识,我们得跳出来,毕竟虚拟机设计之初是为 Java 语言提供服务的,那么在语言层面上一定有对底层原理的体现,要将语言和虚拟机结合起来看,这样才能理解的更加透彻,语言层面不扎实,学习虚拟机很容易遗忘,切忌人云亦云,邯郸学步。
参考《深入理解Java虚拟机》
















