目录
一.动态加载类和静态加载类
1.静态加载
在编译阶段加载类,我们称为静态加载。
new创建对象的方式,就属于静态加载。
静态加载在编译时就需要加载所有可能用到的类。如果没有就报错,依赖性太强。
2.动态加载
在运行阶段加载类,我们称为动态加载。
通过Class.forName("类的全路径"),(Class.forName不仅表示了类的类类型,还表示了动态加载。)
动态加载在运行时加载需要的类,哪怕这个类不存在,但是运行的时候不用该类,则不报错,降低了依赖性。
3.演示说明
看如下代码:
package com.senxu.ClassLoad_;
import com.senxu.Person;
import java.util.Scanner;
public class ClassLoad_ {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Scanner scanner = new Scanner(System.in);
System.out.println("你要投胎了,请输入1-2抽奖选择你下辈子的物种");
String species = scanner.next();
switch (species){
case "1":
Cat cat = new Cat();//静态加载,编译的时候就会报错,因为我们没有这个类
break;
case "2":
System.out.println("恭喜你,投胎成功,准备出生中...");
Class<?> cls = Class.forName("com.senxu.ClassLoad_.Dog");//动态加载
Object dog = cls.newInstance();
break;
default:
System.out.println("暂未为您解锁其他物种,请充值解锁...");
}
}
}
java: 找不到符号
符号: 类 Cat
位置: 类 com.senxu.ClassLoad_.ClassLoad_
我们并没有Cat这个类,在编译阶段就报错了,同样的,我们也没有Dog类,但是我们发现,编译阶段并没有给Dog类报错,说明我们在编译阶段去加载了Cat类,也就是用new创建对象,会在编译阶段加载类。
我们给代码加上Cat类,但是不加Dog类:
package com.senxu.ClassLoad_;
import com.senxu.Person;
import java.util.Scanner;
public class ClassLoad_ {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Scanner scanner = new Scanner(System.in);
System.out.println("你要投胎了,请输入1-2抽奖选择你下辈子的物种");
String species = scanner.next();
switch (species){
case "1":
Cat cat = new Cat();//静态加载,编译的时候就会报错,因为我们没有这个类
break;
case "2":
System.out.println("恭喜你,投胎成功,准备出生中...");
Class<?> cls = Class.forName("com.senxu.ClassLoad_.Dog");//动态加载
Object dog = cls.newInstance();
break;
default:
System.out.println("暂未为您解锁其他物种,请充值解锁...");
}
}
}
class Cat {
Cat(){
System.out.println("恭喜你,如果你能出生,你将是一个高贵的喵星人了");
}
}
你要投胎了,请输入1-2抽奖选择你下辈子的物种
1
恭喜你,如果你能出生,你将是一个高贵的喵星人了
你要投胎了,请输入1-2抽奖选择你下辈子的物种
2
Exception in thread "main" java.lang.ClassNotFoundException: Dog
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at com.senxu.ClassLoad_.ClassLoad_.main(ClassLoad_.java:19)
我们可以看到,在加入Cat类之后,编译并无报错,在运行时,输入1,程序正常运行,输入2,程序报错,找不到Dog类,可以说明,在编译阶段静态加载类,在运行阶段动态加载类(Dog类的对象,编译的时候不报错,运行时加载,报错)
我们加入Dog类:
package com.senxu.ClassLoad_;
import com.senxu.Person;
import java.util.Scanner;
public class ClassLoad_ {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Scanner scanner = new Scanner(System.in);
System.out.println("你要投胎了,请输入1-2抽奖选择你下辈子的物种");
String species = scanner.next();
switch (species){
case "1":
Cat cat = new Cat();//静态加载,编译的时候就会报错,因为我们没有这个类
break;
case "2":
System.out.println("恭喜你,投胎成功,准备出生中...");
Class<?> cls = Class.forName("com.senxu.ClassLoad_.Dog");//动态加载
Object dog = cls.newInstance();
break;
default:
System.out.println("暂未为您解锁其他物种,请充值解锁...");
}
}
}
class Cat {
Cat(){
System.out.println("恭喜你,如果你能出生,你将是一个高贵的喵星人了");
}
}
class Dog {
Dog(){
System.out.println("恭喜你,如果你能出生,你将是一个高贵的汪星人了");
}
}
你要投胎了,请输入1-2抽奖选择你下辈子的物种
2
恭喜你,投胎成功,准备出生中...
恭喜你,如果你能出生,你将是一个高贵的汪星人了
程序正常运行。
二.类加载
1.类加载时机
- 遇到new、getstatic、putstatic或invokestatic时
- 当子类被加载时
- 虚拟机启动,加载主类时
- 调用类中的静态成员时
- 通过反射Class.forName("全类名")
2.类的生命周期
类的生命周期分为五个阶段:加载、连接(验证、准备、解析)、初始化、使用、卸载。
值得注意的是,加载。验证、准备、初始化和卸载这5个阶段的顺序是确定的,但是解析过程顺序不一定,这是由于Java支持运行时绑定,所以它某些情况下可能初始化阶段后再开始(也称为动态绑定或晚期绑定)。
其它阶段在下面类的加载过程中都会详细介绍,这边先补充一下卸载阶段:
- 执行System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
3.类的加载过程
(1)加载(Loading)
加载主要是将*.class文件通过二进制字节流读入到JVM中。“加载”是“类加载”过程的第一个阶段,该阶段由虚拟机完成(准确来说,是由类加载器完成,JVM提供的类加载器叫做系统类加载器,此外还可以通过继承ClassLoad基类来自定义类加载器):
- 通过ClassLoader在classpath中获取*.class文件,并将其以二进制流的形式读入内存。
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
通常可以用这几种方法加载类的二进制文件:
- 从本地文件系统加载class文件
- 从jar、zip等归档文件中加载class文件
- 通过网络加载class文件
- 从数据库中提取.class文件
- 把一个Java源文件动态编译并执行加载
(2)连接(Linking)
当类被加载后,系统生成一个对应的Class对象(注意,一个类对应唯一一个Class对象),接着进入连接阶段,连接阶段负责把类的二进制文件合并到jre中。类的连接阶段分为三个阶段:
- 验证(verification):确保加载的类信息符合JVM规范,无安全方面的问题。
- 准备(Preparation):为类的静态变量分配内存,并设置初始值。
- 解析(Resolution):将类的二进制数据中的符号引用替换成直接引用。
a.验证
主要包括以下几方面的验证:
- 文件格式验证:验证字节流是否符合Class文件格式的规范。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
对虚拟机的类加载机制来说,验证重要但非必要。如果所运行的全部代码都已经被反复使用和验证过,那么在实施阶段就可以考虑使用-Xverify: none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。(至于效果,在之前谈反射的时候做过测试,可以移步我的另一篇帖子去看:Java 反射机制--框架设计的灵魂(详解))
b.准备
该阶段正式为类变量(静态变量)默认初始化并分配内存(这些内存都将在方法区中分配)。
该阶段有以下几点需要注意:
- 没被static修饰的不是静态变量,在准备阶段,不分配内存
- 对static修饰的静态变量进行内存分配、并赋初始值
- 被final static修饰的是常量,进行内存分配,直接赋值
注意:内存分配仅包括类变量,不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。且默认初始化时的初始值通常是数据类型的零值:
数据类型 | 零值 |
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
c.解析
将常量池中的符号引用替换为直接引用(内存地址)的过程。
符号引用(Symbolic References):以一组符号来描述所引用的目标,符号可以是任何形式的字面量。符号引用引用的目标不一定已经加载到了内存中。
直接引用(Direct References):直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。直接引用引用的目标一定已经存在在内存中。
解析地址主要针对:类或接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、类方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_IntrfaceMethodref_info)、方法类型(CONSTANT_MethodType_info)、方法句柄(CONSTANT_MethodHandle_info)、调用点限定符(CONSTANT_InvokeDynaic_info)。
(3)初始化(initialization)
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,是由JVM主导和控制,到了这个阶段,才真正执行类中定义的Java程序代码,此阶段是执行<clinit>()方法的过程。
<clinit>()方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。该阶段静态变量赋初值的两种方式:
- 定义静态变量时指定初始值
- 静态代码块里为静态变量赋值
注意:虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时区初始化一个类,那么只会有一个类去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。也正是因为有了这个机制,才能保证某个类在内存中,只有一份Class对象。