一.类的生命周期
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化。
加载:
- 作用:查找并加载类的二进制数据
- 步骤:
1)以类的全限定名来获取此类的二进制字节流
2)将字节流所代表的静态数据存储结构转化为方法区的运行时数据结构
3)在堆中生代表此类的java.lang.class对象,作为方法区中类数据的访问入口
验证:
- 作用:确保加载的类信息复合jvm规范,没有安全问题
- 校验类型:
1)文件格式验证:验证字节流是否符合Class文件格式的规范
2)元数据格式校验:对字节码描述的信息进行语义分析,比如:这个类是否有父类
3)字节码校验:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
4)符号引用校验:确保解析动作能正确执行
准备:
- 作用:为类的静态变量(static)在方法区中分配内存和初始值
- 注意:
1)初始值,数据类型的默认值,如0、0L、null、false,不是被在Java代码中显式赋值的值,如 public static int num = 1;在准备阶段,num的初始值为0
2)同时被static和final修饰的常量,必须在声明是时为其显式赋值,在初始化时会被显式声明为Java代码中赋值的值,即在编译期就将其结果放入了调用它的类的常量池中
3)只被final修饰的常量,即可以在声明时显式赋值,也可以在初始化时显式赋值
4)引用数据类型,如数组、对象的引用,如果没有显式赋值而直接使用,会默认赋值null
解析:
- 作用:将jvm中常量池中的符号引用(变量名)转换为直接引用(地址)
针对类、接口、接口、字段、类方法、接口方法、方法类型等。
初始化:
作用:执行类构造器的过程,对类的静态变量和代码块执行初始化的过程
步骤:
1)执行类构造器<clinit()>方法的过程,clinit()方法由编译器所有类变量的赋值动作和静态代码块中语句合并而成,其合并组成的方法的执行顺序与类变量、静态代码块在类中的先后顺序有关,谁在前谁先被执行
2)初始化一个类时,必须先触发其父类进行初始化
3)jvm会保证类的clinit()方法,会在在多线程环境下加锁类的主动引用一定会引发初始化:
1)jvm启动时,调用main()方法,
2)调用static类变量(非final修饰或非父类静态变量)和静态方法时,
3)利用反射创建对象时,
4)通过new创建对象时,
5)初始化一个类时,一定会初始化它的父类类的被动引用不会引发初始化,
1)通过数据定义该类的引用,不会触发该类的初始化,
2)调用常量,不会触发该类的初始化,因为常量在链接阶段的解析阶段就已存入调用常量池中了,
3)子类调用父类的静态变量时,不会触发子类的初始化,但会触发父类的初始化
触发初始化的执行顺序
1)父类的静态成员变量和static代码块
2)子类的静态成员变量和static代码块
3)父类的普通成员变量和非static代码块
4)父类的构造方法
5)子类的普通成员变量和非static代码块
6)子类的构造方法
结束生命周期:
1)执行System.exit()方法
2)程序正常执行结束
3)程序在执行过程中遇到异常或错误而异常终止
4)操作系统错误导致Java虚拟机进场终止
二.加载机制
Java把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器,启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)
加载机制:
1)全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用的其它Class也将由该类加载器负责载入
2)父类委托:先让父类加载器试图加载该类,只有父类加载器无法加载该类时尝试从自己的类路径中加载该类
3)缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
加载方式:
1)命令行启动应用时候由JVM初始化加载
2)通过Class.forName()方法动态加载
3)通过ClassLoader.loadClass()方法动态加载
双亲委托模型:
- 定义:当一个类在接收到需要加载类的请求时,会先将加载任务委托给父类加载器,依次委托,直到最高的引导类加载器,如果父类可以完成加载任务,则成功返回,如果不能记载,则自己加载
- 目的:
1)保证java核心库的数据安全
2)系统类防止内存中出现多份同样的字节码
自定义加载器:
- 定义:通过继承java.lang.ClassLoader实现自定义的类加载器,自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。如:通过网络来传输的Java类的字节码,为保证安全性,会对这些字节码进行加密处理,这时系统类加载器就无法对其进行加载
- 注意:
1)传递的文件名需要是类的全限定性名称
2)自定义加载器一般继承ClassLoader类,一般只需要重写findClass方法即可,重写loadClass方法,会破坏双亲委托模式
三.常见面试题
1.下面程序的运行结果?
class HelloA {
public HelloA() {
System.out.println("HelloA");
}
{ System.out.println("I'm A class"); }
static { System.out.println("static A"); }
}
public class HelloB extends HelloA {
public HelloB() {
System.out.println("HelloB");
}
{ System.out.println("I'm B class"); }
static { System.out.println("static B"); }
public static void main(String[] args) {
new HelloB();
}
}
执行结果:
static A
static B
I’m A class
HelloA
I’m B class
HelloB解析:
考察类的初始化顺序,父类静态成员变量/静态代码块->子类静态成员变量/静态代码块->父类非静态成员变量/非静态代码块->父类构造方法->子类非静态成员变量/非静态代码块->子类的构造方法
(1)类加载之后,按从上到下(从父类到子类)执行被static修饰的语句;
(2)当static语句执行完之后,再执行main方法;
(3)如果有语句new了自身的对象,将从上到下执行构造代码块、构造器(两者可以说绑定在一起)
class HelloA {
public HelloA() {
System.out.println("HelloA");
}
{ System.out.println("I'm A class"); }
static { System.out.println("static A"); }
}
public class HelloB extends HelloA {
public HelloB() {
System.out.println("HelloB");
}
{ System.out.println("I'm B class"); }
static { System.out.println("static B"); }
public static void main(String[] args) {
System.out.println("-------main start-------");
new HelloB();
new HelloB();
System.out.println("-------main end-------");
}
}
执行结果:
static A
static B
-------main start-------
I’m A class
HelloA
I’m B class
HelloB
I’m A class
HelloA
I’m B class
HelloB
-------main end-------