一个class文件只有被jvm加载到内存中才能被jvm使用,class文件被加载到内存形成可以被jvm使用的java类型的这一过程就成为类加载过程。类的初始化是类加载过程的一个步骤,本质就是按照开发者的意图为类变量赋值。类的实例化是类完全加入到内存以后创建对象的过程。
一、类加载时机
- 类加载的时机jvm没有明确说明
- 类初始化的时机(有且只有五种):
- 使用new、getstatic、putstatic、invokestatic四个指令
- 使用java.lang.reflect包对类进行反射调用时,如果类没有初始化则先对类进行初始化
- 当初始化类其父类还没初始化时,先对父类进行初始化
- 虚拟机启动时,会先对执行的主类(有main函数的那个类)进行初始化
- 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化
以上五类的行为被称为对一个类主动引用,其他均为被动应用,被动引用不会触发类的初始化
二、类加载过程
- 加载
- 验证
- 准备——为变量分配内存并设置变量初始值
- 分配的类变量不包括实例变量
- 初始值通常情况下是数据类型的零值
- 解析
- 初始化——执行类构造方法<clinit>()
- <clinit>()方法由类变量的赋值定义语句及静态代码块合并而成
- 同一个类加载器中,类初始化只能被执行一次,即<clinit>()方法只能被执行一次
- 在执行子类的clinit方法前一定会先执行父类的clinit方法
- <clinit>()方法语句顺序是有在源码中的静态语句或代码块顺序确定的,定义在前面的代码块只能赋值定义后面的类变量,不能访问后面的类变量.
- 虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。
一、对象创建过程
- 根据new指令后面的参数,在常量池中定位到一个类的符号引用,检查这个符号引用代表的是否已经加载,如果没有加载先执行类的加载过程
- 为对象分配内存
- 指针碰撞
- 空闲列表
- 对象分配的空间设置空值
- 对象头信息设置
- 对象的初始化——执行<init>()方法
- <init>()方法由实例变量、实例代码块和构造函数三部分组成
- 实例变量和实例代码块的顺序在构造函数前面,实例变量和代码块直接的顺序由在源码中的顺序决定
- 在执行子类的构造函数之前必然先会执行父类的构造函数,如果我们没有在子类中调用父类的构造函数,编译器自动帮我们生成一个对超类构造函数的调用
- 实例化一个类的构造过程是一个递归过程
二、实例化过程的顺序
父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数
三、<clinit>() 与 <init>()区别
- 类构造器<clinit>()与实例构造器<init>()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()执行完毕
- 在一个类的生命周期中,类构造器<clinit>()最多会被虚拟机调用一次,而实例构造器<init>()则会被虚拟机调用多次,只要程序员还在创建对象
需要注意的是,类的实例化不一定发生在类的初始化完成之后,类初始化的过程中就可能会实例化对象(实际情况下程序员应该尽量避免出现这种情况的,使用为初始化完全的类实例化对象会引起一个意想不到的result......)
question:一个实例变量在对象初始化过程中最多可被赋值几次:
首先在类加载过程准备阶段会被设置零值;
然后如果声明实例变量的同时为其赋值,第二次;
然后在实例代码块中被赋值,第三次;
最后如果在构造函数中被赋值,为第四次,因此在实例化过程中最多会被赋值四次。
ps 面试中的许多这类问题,本质上就是在考察类的加载过程和实例化过程,弄清其原理机制,这类问题就不在话下..........