H__D  

一、对象的实例化

  对象创建的方式  

1.1、new 关键字

  ① 最常见的方式就是直接 new 加构造器的方式创建

  ② 变形一:XXX(类名). 静态方法,本质这种方式还去调用类中构造器,比如说:单例模式、日历类(Calendar) 和一些工具类等等。

  ③ 变形二:XXXBuilder / XXXFactory的静态方法

1.2、Class的newInstance() 反射的方式

  这种方式jdk9就过时了,为什么呢,就是它只能调用空参构造器,权限必须是是public。

1.3、Constructor的newInstance(XXX),也是反射的方式

  这种方式就可以调用空参或带参的构造器,权限没有要求。

1.4、使用clone()(克隆)

  这种方式不调用任何构造器,当前类需要是心啊Cloneable接口,实现clone()方法(通过一个对象去调用clone()方法,然后复制一个新的对象)。

1.5、使用反序列化

  从文件或者网络中获取一个对象的二进制流(序列化机制:可以实现数据从一个程序到另一个程序的传递,可以实基于本地、网络)。

1.6、第三方库Objenesis

  利用字节码技术我们可以动态的生成Constructor类对象。

代码示例如下:

 1 public class Employee implements Cloneable, Serializable {
 2 
 3     private int id;
 4 
 5     private String name;
 6 
 7     public int getId() {
 8         return id;
 9     }
10 
11     public void setId(int id) {
12         this.id = id;
13     }
14 
15     public String getName() {
16         return name;
17     }
18 
19     public void setName(String name) {
20         this.name = name;
21     }
22 
23     @Override
24     public String toString() {
25         return "Employee{" +
26                 "id=" + id +
27                 ", name='" + name + '\'' +
28                 '}';
29     }
30 
31     @Override
32     public Object clone() throws CloneNotSupportedException {
33         return super.clone();
34     }
35 }
Employee.java
 1 public class CreateObjectTest {
 2 
 3     // 1、new 关键字
 4     @Test
 5     public void testNew(){
 6         Employee emp = new Employee();
 7     }
 8 
 9     // 2、Class的newInstance() 反射的方式
10     @Test
11     public void testNewINstance() throws IllegalAccessException, InstantiationException {
12         // 通过反射传教对象
13         Employee emp = Employee.class.newInstance();
14         System.out.println(emp);
15     }
16 
17     // 3、Constructor的newInstance(XXX),也是反射的方式
18     @Test
19     public void testConstructor() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
20         // 通过反射得到构造器
21         Constructor<Employee> constructor = Employee.class.getConstructor();
22         // 通过构造器创建对象
23         Employee emp = constructor.newInstance();
24         System.out.println(emp);
25     }
26 
27     // 4、使用clone()(克隆)
28     @Test
29     public void testClone() throws CloneNotSupportedException {
30         Employee emp = new Employee();
31         emp.setId(1);
32         emp.setName("小白");
33         System.out.println(emp.hashCode());
34 
35         // 克隆对象
36         Object emp2 = emp.clone();
37         System.out.println(emp2);
38         System.out.println(emp2.hashCode());
39     }
40 
41 
42     // 5、使用反序列化
43     @Test
44     public void testSeri() throws IOException, ClassNotFoundException {
45         Employee emp = new Employee();
46         emp.setId(1);
47         emp.setName("小白");
48         System.out.println(emp.hashCode());
49 
50         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("emp.bat"));
51         ObjectInputStream ois  = new ObjectInputStream(new FileInputStream("emp.bat"));
52 
53         oos.writeObject(emp);
54         // 反序列化获取对象
55         Employee emp2 = (Employee) ois.readObject();
56         System.out.println(emp2);
57         System.out.println(emp2.hashCode());
58 
59         // 关闭流
60         ois.close();
61         oos.close();
62     }
63 }
CreateObjectTest.java

二、对象创建的步骤

2.1、从字节码看待对象的创建过程

  示例代码

1 public class ObjectTest {
2     public static void main(String[] args) {
3         Object obj = new Object();
4     }
5 }

  反编译class文件,命令:javap -v

  

  main() 方法对应的字节码:

  • 调用 new 指令后后,加载 Object 类
  • 调用 Object 类的 init() 方法

2.2、创建对象的六个步骤

1)判断对象对应的类是否加载、链接、初始化

  1. 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。

  2. 如果该类没有加载,那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为key进行查找对应的.class文件,如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class对象。

2)为对象分配内存

  1. 首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小

  2. 如果内存规整:采用指针碰撞分配内存

    • 如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Point)来为对象分配内存。
    • 意思是所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针往空闲内存那边挪动一段与对象大小相等的距离罢了。
    • 如果垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。
    • 标记压缩(整理)算法会整理内存碎片,堆内存一存对象,另一边为空闲区域
  3. 如果内存不规整:采用的是空闲列表来为对象分配内存

    • 如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表来为对象分配内存。
    • 意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为了 “空闲列表(Free List)”
    • 选择哪种分配方式由Java堆是否规整所决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
    • 标记清除算法清理过后的堆内存,就会存在很多内存碎片。

3)处理并发问题  

  1. 采用CAS+失败重试保证更新的原子性

  2. 每个线程预先分配TLAB - 通过设置 -XX:+UseTLAB参数来设置(区域加锁机制)

  3. 在Eden区给每个线程分配一块区域

4)初始化分配到的内存

  所有属性设置默认值,保证对象实例字段在不赋值可以直接使用

5)设置对象的对象头

  将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

6)执行init方法进行初始化

  1. 在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量

  2. 因此一般来说(由字节码中跟随invokespecial指令所决定),new指令之后会接着就是执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来。

2.3、从字节码角度看 init 方法

给对象属性赋值的顺序:

  1. 属性的默认值初始化

  2. 显示初始化/代码块初始化(并列关系,谁先谁后看代码编写的顺序)

  3. 构造器初始化

  • 示例代码
 1 public class Customer{
 2     int id = 1001;
 3     String name;
 4     Account acct;
 5 
 6     {
 7         name = "匿名客户";
 8     }
 9 
10     public Customer(){
11         acct = new Account();
12     }
13 
14 }
15 class Account{
16 
17 }
  • init() 方法的字节码指令:
    • 属性的默认值初始化:id = 1001;
    • 显示初始化/代码块初始化:name = "匿名客户";
    • 构造器初始化:acct = new Account();

  

  总结: 
  

  代码参考:  

 1 public class ParentTest {
 2     public static String PARENT_STATIC_FIELD = "父类-静态属性";
 3 
 4     // 父类-静态块
 5     static {
 6         System.out.println(PARENT_STATIC_FIELD);
 7         System.out.println("父类-静态代码块");
 8     }
 9 
10     public static String parentField = "父类-非静态属性";
11 
12     // 父类-非静态块
13     {
14         System.out.println(parentField);
15         System.out.println("父类-非静态代码块");
16     }
17 
18     public ParentTest() {
19         System.out.println("父类—无参构造函数");
20     }
21 }
22 
23 class InitOder extends ParentTest {
24     public static String STATIC_FIELD = "子类-静态属性";
25 
26     // 静态块
27     static {
28         System.out.println(STATIC_FIELD);
29         System.out.println("子类-静态代码块");
30     }
31 
32     public String field = "子类-非静态属性";
33 
34     // 非静态块
35     {
36         System.out.println(field);
37         System.out.println("子类-非静态代码块");
38     }
39 
40     public InitOder() {
41         System.out.println("子类-无参构造函数");
42     }
43 
44     public static void main(String[] args) {
45         InitOder test = new InitOder();
46         System.out.println("~~~~~~~~第二次创建对象~~~~~~~");
47         InitOder test2 = new InitOder();
48     }
49 }
View Code

三、对象的内存布局

  对象在堆空间中的布局可划分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

   

3.1、对象头(Header)

  HotSpot 虚拟机的对象头包括两部分(非数组对象)信息,如下图所示:

  

  • 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。

  • 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;

  • 如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。

  这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64bit。

  例如,在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么 Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0,如下表所示:

  

  在 32 位系统下,存放 Class 指针的空间大小是 4 字节,Mark Word 空间大小也是4字节,因此头部就是 8 字节,如果是数组就需要再加 4 字节表示数组的长度,如下表所示:

  

  在 64 位系统及 64 位 JVM 下,开启指针压缩,那么头部存放 Class 指针的空间大小还是4字节,而 Mark Word 区域会变大,变成 8 字节,也就是头部最少为 12 字节,如下表所示:

   

  压缩指针:开启指针压缩使用算法开销带来内存节约,Java 对象都是以 8 字节对齐的,也就是以 8 字节为内存访问的基本单元,那么在地理处理上,就有 3 个位是空闲的,这 3 个位可以用来虚拟,利用 32 位的地址指针原本最多只能寻址 4GB,但是加上 3 个位的 8 种内部运算,就可以变化出 32GB 的寻址。

  什么是java对象的指针压缩?

  1.jdk1.6 update14开始,在64位操作系统中,JVM支持指针压缩,32位不支持 
  2.jvm配置参数:UseCompressedOops,compressed--压缩、oop--对象指针
  3.启用指针压缩:-XX:+UseCompressedOops,禁止指针压缩:-XX:-UseCompressedOops

  代码验证(使用jol工具)依赖如下:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

 

  • 情况一:64位操作系统,启用指针压缩的情况:对比 Object 对象 与 数组对象 的差别
 1 public class ObjectTest {
 2     public static void main(String[] args) {
 3 
 4         // 普通对象
 5         Object obj = new Object();
 6         System.out.println(ClassLayout.parseInstance(obj).toPrintable());
 7 
 8         // 数组对象
 9         int[] arr = new int[1];
10         System.out.println(ClassLayout.parseInstance(arr).toPrintable());
11     }
12 }

  结果:

  

  • 情况二:64位操作系统,禁止指针压缩:-XX:-UseCompressedOops 的情况:对比 Object 对象 与 数组对象 的差别

  

3.2、实例数据(Instance Data)

  实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

  这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响

3.3、对齐填充(Padding)

  对齐填充不是必然存在的,没有特别的含义,它仅起到占位符的作用。

  由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,也就是说对象的大小必须是 8 字节的整数倍。对象头部分是 8 字节的倍数,所以当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3.4、估算对象大小

  32 位系统下,当使用 new Object() 时,JVM 将会分配 8(Mark Word+类型指针) 字节的空间,128 个 Object 对象将占用 1KB 的空间。

  如果是 new Integer(),那么对象里还有一个 int 值,其占用 4 字节,这个对象也就是 8+4=12 字节,对齐后,该对象就是 16 字节。

  以上只是一些简单的对象,那么对象的内部属性是怎么排布的?

1 Class A {
2     int i;
3     byte b;
4     String str;
5 }

  其中对象头部占用 ‘Mark Word’4 + ‘类型指针’4 = 8 字节;byte 8 位长,占用 1 字节;int 32 位长,占用 4 字节;String 只有引用,占用 4 字节;
  那么对象 A 一共占用了 8+1+4+4=17 字节,按照 8 字节对齐原则,对象大小也就是 24 字节。

  这个计算看起来是没有问题的,对象的大小也确实是 24 字节,但是对齐(padding)的位置并不对:

  在 HotSpot VM 中,对象排布时,间隙是在 4 字节基础上的(在 32 位和 64 位压缩模式下),上述例子中,int 后面的 byte,空隙只剩下 3 字节,接下来的 String 对象引用需要 4 字节来存放,因此byte 和对象引用之间就会有 3 字节对齐,对象引用排布后,最后会有 4 字节对齐,因此结果上依然是 7 字节对齐。此时对象的结构示意图,如下图所示:

    

3.5、图解内存布局

  

4、对象的访问定位

  JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?

  

  对象的两种访问方式:句柄访问和直接指针

4.1、句柄访问  

  1. 缺点:在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间;通过两次指针访问才能访问到堆中的对象,效率低

  2. 优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改

  

4.2、直接指针(HotSpot采用)

  1. 优点:直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据

  2. 缺点:对象被移动(垃圾收集时移动对象很普遍)时需要修改 reference 的值

  

 

posted on 2020-12-30 18:47  H__D  阅读(192)  评论(0)    收藏  举报