对象的创建过程 new指令过程
跟类的加载其实差不多
- 检查new指令参数对应的类是否加载,若无则执行类加载
- 给对象分配内存
- 将对象的实例变量自动初始化为其变量类型的默认值
- 初始化对象,给实例变量赋予正确的初始值
检查new指令参数对应的类是否加载,若无则执行类加载
当java虚拟机遇到一条字节码new指令时,首先去检查这个指令而的参数是否能定位到一个类的符号引用,并且检查该类是否已被加载,解析和初始化过。如果没有,那必须先执行相应的类加载过程。
给对象分配内存
- 对象所需内存的大小在类加载后便可确定
- 给对象分配内存实际上就是把一块确定大小的内存块从Java堆中划分出来
- 对象头设置
将对象的实例变量自动初始化为其变量类型的默认值
- 将对象的实例变量自动初始化为其变量类型的默认值
- 当给对象分配内存之后,这块存储空间会被清零,这就自动地将对象中的所有基本类型数据都设置成了缺省值(对数字来说就是 0,对 boolean 和 str 也相同),而引用则被设置成了 null;
初始化对象,给实例变量赋予正确的初始值
- 执行构造函数,也叫做<init>()方法
- Java 在编译之后会在字节码文件中生成 init 方法,称之为实例构造器,该实例构造器会将语句块,变量初始化,调用父类的构造器等操作收敛到 init 方法中,收敛顺序为:
- 父类变量初始化
- 父类语句块
- 父类构造函数
- 子类变量初始化
- 子类语句块
- 子类构造函数
对象的内存布局
对象头
- 第一部分用于存储自身运行时的数据例如GC分代年龄、哈希码、锁状态等信息。
当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。(我还没学这些锁是啥。。)
- 第二部分存放指向方法区类型元数据的指针。Java虚拟机通过这个指针来确定该对象是那个类的实例。如果是数组那么还会有一块用于记录数组长度的数据。
实例数据
对象的实例数据就是在java代码中能看到的属性和他们的值。
对齐填充字节
JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。
对象的访问定位
句柄
Java堆中将会划分出一块内存来作为句柄池,reference中 存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,有两个指针。
直接指针
使用直接指针访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,指针放在对象实例的存储中,也就是上面提到的类型指针
(1)使用句柄来访问的最大好处就是reference中存储的是稳 定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中 的实例数据指针,而reference本身不需要修改。
(2)使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销, 由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。(HotSpot采用的就是这个)
对象的内存分配
1.对象优先在Eden区分配,如果Eden区没有足够空间分配将发起一次MinorGC
2.如果对象很大,可以通过PretenureSizeThreshold设置,会直接进入老年代
3.如果对象年龄增加到一定程度,默认15就会进入老年代
4.如果相同年龄的所有对象大小总和大于Survivor空间一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代。(Survivor区空间如果设置太小,就会导致对象会导致全部移到老年代) --很重要的调优重点
5.如果MinorGC完后大量对象存活超过了Survivor空间,那么就会把多出的对象放入老年代