对象的创建过程
仅仅是创建:
1.calss loading加载class文件
2.linking
- verification 检查class文件是否符合JVM规定
- resolution 将类、属性,转换成直接引用原有的是符号引用
- preparation 仅仅是给静态成员变量赋默认值
3.Initializing 调用初始化方法给静态成员变量赋初始值
前面三步 基本class已经加载完毕!
4.new 申请对象内存(在堆里开辟空间)
5.成员变量赋默认值
6.调用构造方法< init >
- 成员变量顺序赋初始值
- 执行构造方法语句
7.调用父类的构造方法
以上对象就基本创建成功。
对象在内存中的布局
首先对象包括普通对象及数组对象
普通对象
普通对象是由4个部分组成:
- markword:没有具体的翻译哈,用来记录锁信息,GC的分代年龄(经过多少次垃圾回收还没有被回收掉的次数),还记录hashCode();
- classPorinter:记录当前对象的class类型,例如T.class; data(实例数据):成员变量之类的属性;
- padding(对齐):
由于现代计算机都是由16,32,64位组成,为了提高效率不会一位一位的读取,而是一行或者8位的去读,所以对齐就是把对象位数对齐为被8整除的数。
数组对象
跟上面一样,就是多了个4位的数组长度。
证明:
public class Test {
public static void main(String[] args) {
System.out.println("Object普通对象:"+ObjectSizeAgent.sizeOf(new Object()));
System.out.println("int数组对象:"+ObjectSizeAgent.sizeOf(new int[] {}));
System.out.println("带实例数据:"+ObjectSizeAgent.sizeOf(new Y()));
}
private static class Y {
//8 _markword
//4 _oop指针
int id; //4
String name; //4
int age; //4
byte b1; //1
byte b2; //1
Object o; //4
byte b3; //1
}
}
输出结果:
Object普通对象:16
int数组对象:16
带实例数据:32
普通对象Object 中的markword 8位 classPorinter 4位 实例数据空的,8+4=12 ,所以padding对齐4位=16 被8乘除。后面都是一样的
注意:这里是开启了压缩指针,和实例数据的引用类型,默认都是开启的,可以指定。Oops跟压缩指针不是一个东西
-XX:+UseCompressedClassPointers 为4位 不开启为8位、
-XX:+UseCompressedOops 为4位 不开启为8位
把参数设置成关闭的运行一波:-XX:-UseCompressedClassPointers
输出结果:
Object普通对象:16
int数组对象:24
带实例数据:40
//普通对象的话没有变 因为没有开启压缩指针 所以classProinter = 8位 加markword的8 其他都没有8+8=16
//其他同比
继续关闭Oops:-XX:-UseCompressedOops
,这里就是把成员变量的引用类型关闭压缩了。两个同时都是关闭的哦
输出结果:
Object普通对象:16
int数组对象:24
带实例数据:48
//主要看Y这个类:markword 8,classPorinter 8,int id 4,String name(这里关闭压缩所以是8),
int age 4,byte b1 1 byte b2 1 Object o 没有开启压缩 所以是4 byte b3 1
8+8+4+8+4+1+1+8 +1=41 最后padding对齐7位 =48
注:一个是classProinter,一个是对象里的引用类型的压缩,不用搞混了。
Hotspot开启内存压缩的规则(64位机)
- 4G以下,直接砍掉高32位
- 4G - 32G,默认开启内存压缩 ClassPointers Oops
- 32G,压缩无效,使用64位
- 内存并不是越大越好
附带查看对象大小的类:Agent
是在类加载的时候做的拦截做的一个操作。
1.创建项目:
package com.yedi;
import java.lang.instrument.Instrumentation;
public class ObjectSizeAgent {
private static Instrumentation inst;
public static void premain(String agentArgs, Instrumentation _inst) {
inst = _inst;
}
public static long sizeOf(Object o) {
return inst.getObjectSize(o);
}
}
2.src目录下创建META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: com.yedi.ObjectSizeAgent //必须指定好Class
然后打成jar放到要运行的项目中加上vm参数:-javaagent:D:\java\code\ObjecSize\out\artifacts\ObjecSize_jar\ObjecSize.jar
指定生成的jar,就是先去这个jar。
之后就可以用了:
对象头具体包括什么?
对象头:接上部分的markword
hotspot/src/share/vm/oops/markOop.hpp中的说明:
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
可以看到有两个 32 64位的 看64位的:
哈希码(hash) GC分代年龄(age) 锁标识位:
- 01 无锁
- 01 偏向锁
- 00 轻量级锁
- 10 重量级锁
偏向锁标识位(biased_lock)
- 0 无锁
- 1 偏向锁
偏向线程ID(JavaThread*) 偏向时间戳(epoch)
这里就可以看到其实锁信息是3个位
锁状态 | 存储信息 | 偏向锁标志(是否是偏向锁) | 锁标志 |
---|---|---|---|
无锁状态 | hashCode,GC分代年龄 | 0 | 01 |
偏向锁 | 线程ID、偏向时间戳、GC分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 无 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 Monitor | 10 | 无 |
GC标记 | 无 | 11 | 无 |
锁这些都是在synchronized 的锁升级的机制介绍了这里不做笔记。
对照上面的注释就可以看到很明白了!
这里指的主要的是:
当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。
请注意:用户自定义的hashCode()方法所返回的值跟这里讨论的不是一回事。
对象怎么定位
主流的访问方式有两种:
- 句柄
- 直接指针
如果是句柄访问的话,java堆里会开辟两块空间来划分句柄池,引用中存储的就是句柄的地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
使用句柄来访问的最大好处就是引用中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要被修改。
如果是直接指针的话,引用就直接指向堆里的实例数据,在间接指向数据类型。
直接引用只需要考虑到java堆中怎么存放数据类型,引用中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本