Java 对象初始化和类初始化的区别、new一个对象的详细过程、类加载、类初始化、实例初始化 | 您所在的位置:网站首页 › java中实例和对象的区别 › Java 对象初始化和类初始化的区别、new一个对象的详细过程、类加载、类初始化、实例初始化 |
Java 初始化的深认知(第二篇)
Java 类初始化和对象初始化明解Java中new一个对象的详细过程,JVM执行了那些操作new一个对象的详细过程Java 对象的使用:对对象的访问定位类加载类初始化时机类加载过程
首先,大家看一下这段代码的执行顺序,你是否理解的如注释所写:
//父类Animal
class Animal {
/*8、执行初始化*/
private int i = 9;
protected int j;
/*7、调用构造方法,创建默认属性和方法,完成后发现自己没有父类*/
public Animal() {
/*9、执行构造方法剩下的内容,结束后回到子类构造函数中*/
System.out.println("i = " + i + ", j = " + j);
j = 39;
}
/*2、初始化根基类的静态对象和静态方法*/
private static int x1 = print("static Animal.x1 initialized");
static int print(String s) {
System.out.println(s);
return 47;
}
}
//子类 Dog
public class Dog extends Animal {
/*10、初始化默认的属性和方法*/
private int k = print("Dog.k initialized");
/*6、开始创建对象,即分配存储空间->创建默认的属性和方法。
* 遇到隐式或者显式写出的super()跳转到父类Animal的构造函数。
* super()要写在构造函数第一行 */
public Dog() {
/*11、初始化结束执行剩下的语句*/
System.out.println("k = " + k);
System.out.println("j = " + j);
}
/*3、初始化子类的静态对象静态方法,当然mian函数也是静态方法*/
private static int x2 = print("static Dog.x2 initialized");
/*1、要执行静态main,首先要加载Dog.class文件,加载过程中发现有父类Animal,
*所以也要加载Animal.class文件,直至找到根基类,这里就是Animal*/
public static void main(String[] args) {
/*4、前面步骤完成后执行main方法,输出语句*/
System.out.println("Dog constructor");
/*5、遇到new Dog(),调用Dog对象的构造函数*/
Dog dog = new Dog();
/*12、运行main函数余下的部分程序*/
System.out.println("Main Left");
}
}
输出结果:
static Animal.x1 initialized
static Dog.x2 initialized
Dog constructor
i = 9, j = 0
Dog.k initialized
k = 47
j = 39
Main Left
如果你的理解和注释的顺序有出处,那么说明你对Java对象创建的过程还不熟悉.那快看看我接下来写的吧 😄 😄 😄 😄 😄 😄 Java 类初始化和对象初始化明解类的生命周期为:加载、连接(验证、准备、解析)、初始化、使用、卸载。 首先,明确对象初始化和类初始化是不一样的。 对象初始化是什么? 对象初始化是在类的生命周期第三个阶段初始化执行的,这个时期实例构造器<init>会收集初始化代码块,实例变量赋值语句,构造函数,然后进行赋初值操作。 那么,什么是类初始化呢? 类初始化是指,在jvm第一次创建一个对象时,类的构造器<clinit>实际执行的过程。 在连接的准备阶段,类的构造器<clinit>会收集静态代码块和静态成员变量赋值语句,然后进行赋初值,这就是类的初始化。但这样理解有些狭义,因为类里面还有构造方法,所以说类的初始化还应执行实例构造器<init>,完成对象初始化,这样来说,一个完整的类初始化就完成了。 在Java类初始化过程中,要总共执行这些结构,分别是 静态代码块初始化静态成员变量初始化代码块初始化实例变量初始化构造函数初始化 Java中new一个对象的详细过程,JVM执行了那些操作 new一个对象的详细过程 当虚拟机遇到一条new指令时候,首先去检查这个指令的参数是否能 在常量池中能否定位到一个类的符号引用(即类的带路径全名),并且检查这个符号引用代表的类是否已被加载、解析和初始化过,即验证是否是第一次使用该类。如果没有(不是第一次使用),那必须先执行相应的类加载过程(class.forname())。在类加载检查通过后,接下来虚拟机将为新生的对象分配内存 。对象所需的内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来,目前常用的有两种方式,根据使用的垃圾收集器的不同使用不同的分配机制: (1)指针碰撞(Bump the Pointer):假设Java堆的内存是绝对规整的,所有用过的内存都放一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅把那个指针向空闲空间那边挪动一段与对象大小相等的距离。 (2)空闲列表(Free List):如果Java堆中的内存并不是规整的,已使用的内存和空间的内存是相互交错的,虚拟机必须维护一个空闲列表,记录上哪些内存块是可用的,在分配时候从列表中找到一块足够大的空间划分给对象使用。内存分配完后,虚拟机需要将分配到的内存空间中的数据类型都 初始化为零值(不包括对象头);虚拟机要对对象头进行必要的设置,例如这个对象是哪个类的实例(即所属类)、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象的对象头中。至此,从虚拟机视角来看,一个新的对象已经产生了。但是在Java程序视角来看,执行new操作后会接着执行如下步骤: 调用对象的init()方法,根据传入的属性值给对象属性赋值。在线程栈中新建对象引用,并指向堆中刚刚新建的对象实例。对象虽然创建完了,但是在创建对象的过程中,可能会发生一些小意外。比如:在划分可用空间时,如果是在并发情况下,那么划分就不一定是线程安全的。因为有可能出现正在给A对象分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针分配内存的情况,那么,解决这个问题有两种方案: 1. 分配内存空间的动作进行同步处理:实际上虚拟机采用CAS配上失败重试的方式保证了更新操作的原子性。 2.内存分配的动作按照线程划分在不同的空间中进行: 为每个线程在Java堆中预先分配一小块内存 ,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。 Java 对象的使用:对对象的访问定位我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流访问方式有 使用句柄访问(间接访问) 和 直接指针访问 两种: 1. 句柄访问: Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象句柄位置,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 Java类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。其中准备、验证、解析3个部分统称为连接(Linking),如图所示: 那么,什么情况下虚拟机需要开始初始化一个类呢?这在虚拟机规范中是有严格规定的,虚拟机规范指明 有且只有 五种情况必须立即对类进行初始化(而这一过程自然发生在加载、验证、准备之后): (1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令(注意,newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化)时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是: 使用new关键字实例化对象的时候;读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候;调用一个类的静态方法的时候。(2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 (3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 (4) 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 (5) 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。 注意,对于这五种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这五种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式,都不会触发初始化,称为被动引用。 特别需要指出的是,类的实例化与类的初始化是两个完全不同的概念: 类的实例化是指创建一个类的实例(对象)的过程;类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,类生命周期中的一个阶段。 3、被动引用的几种经典场景 1)、通过子类引用父类的静态字段,不会导致子类初始化 对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。 2)、通过数组定义来引用类,不会触发此类的初始化 3)、常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 public class ConstClass{ static{ System.out.println("ConstClass init!"); } public static final String CONSTANT = "hello world"; } public class NotInitialization{ public static void main(String[] args){ System.out.println(ConstClass.CONSTANT); } }/* Output: hello world *///:~上述代码运行之后,只输出 “hello world”,这是因为虽然在Java源码中引用了ConstClass类中的常量CONSTANT,但是编译阶段将此常量的值“hello world”存储到了NotInitialization常量池中,对常量ConstClass.CONSTANT的引用实际都被转化为NotInitialization类对自身常量池的引用了。也就是说,实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译为Class文件之后就不存在关系了。 类加载过程1. 加载: 在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下三件事情: (1). 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等); (2). 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构; (3). 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口; 加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。 特别地,第一件事情(通过一个类的全限定名来获取定义此类的二进制字节流)是由类加载器完成的,具体涉及JVM预定义的类加载器、双亲委派模型等内容,详情请参见我的博文《双亲委派机制》 2. 验证: 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 验证阶段大致会完成4个阶段的检验动作: 文件格式验证:验证字节流是否符合Class文件格式的规范(例如,是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型) 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求(例如:这个类是否有父类,除了java.lang.Object之外); 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的; 符号引用验证:确保解析动作能正确执行。 3. 准备 准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为: public static int value = 123;那么,变量value在准备阶段过后的值为0而不是123。因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器方法()之中,所以把value赋值为123的动作将在初始化阶段才会执行。至于“特殊情况”是指:当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0。 public static final int value = 123;4. 解析 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。 解析需要静态绑定的内容。 5.初始化(先父后子) (1) 为静态变量赋值 (2)执行static代码块 (3)构造函数 注意:static代码块只有jvm能够调用,如果是多线程需要同时初始化一个类,仅仅只能允许其中一个线程对其执行初始化操作, 其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。因为子类存在对父类的依赖,所以类的加载顺序是先加载父类后加载子类,初始化也一样。 不过,父类初始化时,子类静态变量的值也有有的,是默认值。 最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。 补充: 通过实例引用调用实例方法的时候,先从方法区中对象的实际类型信息找,找不到的话再去父类类型信息中找。 如果继承的层次比较深,要调用的方法位于比较上层的父类,则调用的效率是比较低的,因为每次调用都要经过很多次查找。这时候大多系统会采用一种称为虚方法表的方法来优化调用的效率。 所谓虚方法表,就是在类加载的时候,为每个类创建一个表,这个表包括该类的对象所有动态绑定的方法及其地址,包括父类的方法,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。当通过对象动态绑定方法的时候,只需要查找这个表就可以了,而不需要挨个查找每个父类。 |
CopyRight 2018-2019 实验室设备网 版权所有 |