Java对象创建的具体过程

来源:互联网 发布:拍拍贷网络最新黑名单 编辑:程序博客网 时间:2024/05/23 20:37

Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢?

一、几个概念

1、运行时常量池

运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。内存不够会抛出OutOfMemoryError异常。

2、字面量:

文本字符串、声明为final的常量值等

3、符号引用:

包括了下面三类常量:

  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符

虚拟机加载Class文件需要进行动态连接。虚拟机运行时,需要从常量池中获取对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。


符号引用理解:

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在J中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

4、表

常量池中每一项常量都是一个表。JDK1.7中共有14中表,每个表的第一位是一个ul类型的标志位(tag,取值为每个常量的标志),代表当前这个常量是属于哪一种类型。比如创建对象过程中涉及到的CONSTANT__Class_info(类或接口的符号引用),它的结构为:

CONSTANT_Class_info型常量的结构    类型名称数量          u1                      tag       1          u2               name_index       1

tag是标志位,用于区分常量类型;name_index是一个索引值,指向常量池中一个CONSTANT__Utf8_info类型常量,此常量代表了这个类或者接口的全限定名。

二、创建过程

1、类加载检查

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,具体加载过程这里略过。

2、分配内存(指针碰撞空闲列表的选择)

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

(1)指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。

(2)空闲列表:如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

(3)选择依据:
选择哪种分配方式由 Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决 定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

HotSpot采取G1垃圾回收器,其具有压缩整理功能,系统采用的分配算法是指针碰撞。

3、并发处理

对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来 分配内存的情况。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理 ——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分 配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内 存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。 虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

4、零值初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作解释了对象的实例字段在Java代码中为什么可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

5、由对象头信息设置对象

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对 象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但从Java程 序的视角来看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段都还为零。

6、执行初始化和构造器

class是从子类到基类依次查找,有关静态初始化的动作从基类到子类依次执行。在为所创建对象的存储空间清零后,找到继承链中最上层的基类: 然后从基类到子类依次执行以下这两步操作。

(1)执行其出现在域定义处的初始化动作 ;
(2)然后再执行其构造器 。

7、例子

以创建Pernson对象为例,分析对象创建过程中在内存中的分配情况

/** * Created by wqh on 2017/7/9. */public class Person{    //成员属性    private String name;    private int age;    private static String Country = "CN";    DemoTest dTest = new DemoTest();    //构造函数    public Person(String name, int age)    {        System.out.println("这是person的构造函数");        this.name = name;        this.age = age;    }    //构造代码块    {        System.out.println("这是person的构造代码块");    }    //静态代码块    static    {        System.out.println("这是person类的静态代码块");    }    public static void main(String[] args)    {        Person p = new Person("wqh",24);    }}class DemoTest{    public DemoTest()    {        System.out.println("这是一个测试的类");    }}

输出结果为:
这是person类的静态代码块这是一个测试的类这是person的构造代码块这是person的构造函数

过程分析:

首先要明确的一点是: 类的成员变量在不同对象中各不相同,都有自己的存储空间(存储在各自对象所占用的堆内存空间中)。
类的方法却是该类的所有对象共享的,它们存放在了方法区中,各方法的引用存储在了常量池中。方法区优先于对象存在而且对象只保存了成员变量和一些地址信息。

首先栈中的main函数执行Person = new Person("wqh",24);这个简单的语句会涉及到如下几个步骤:
1、由于是要创建Person类对象,java虚拟机(JVM)先去找Person.class文件,如果有的话,将其加载到内存。
2、将类型信息(包括静态变量,方法等)加载进方法区。
3、执行该类中static代码块。
4、到这时才进行堆内存空间的开辟,并为对象分配首地址。
5、在堆内存中建立对象的成员属性,并对其进行初始化(先进行默认初始化再进行显示初始化)。
6、进行构造代码块的初始化,由此看出构造代码块初始化的优先级要高于对象构造函数的初始化
7、对象的构造函数进行初始化。
8、将堆内存中的地址(引用)赋给栈内存中的p变量。


原创粉丝点击