测试类加载过程程序
/**
* 测试类加载过程
*/
public class Singleton {
// 2
private static Singleton singleton = new Singleton();
// 1
private static int x = 4;
private static int y;
private Singleton(){
x++;
y++;
}
public static Singleton getSingleton(){
return singleton;
}
public static void main(String[] args) {
Singleton test = Singleton.getSingleton();
System.out.println(test.x+","+test.y);
}
// x = 4 在上面时输出
// 5,1
// x = 4 在下面时输出
// 4,1 x被重新赋值
}
类的主动使用和被动使用
主要是知道什么时候类主动使用
- 使用new 关键字生成对象会初始化,如果数组,集合等容器对象不会初始化
- 访问类的静态变量,静态方法会初始化,但是静态变量还用final修改饰(private final static int x = 4;),直接有类.的方式是不会初始化,这个一定不能经过复杂运算(取随机数 private final static int y = new Random().nextInt()😉
- 使用反射机制生成对象
- 初始化子类也会导致父类初始化,但是只是子类访问父类的静态方法或变量时,只有父类会初始化,子类不会。
- 启动类,执行main函数所在的类会初始化
类加载过程
一般分为三个比较大的阶段,加载过程,连接过程,初始化过程。
加载过程
查找并加载类的二进制数据文件(class文件)
一般来源 为堆区的class对象,还有其他地方 只要是二进制就可以进入加载过程。
- 动态代理生成的二进制信息
- 通过网络获取的二进制信息
- 通过读取zip压缩文件获取的二进制信息
- 读取数据库存储的二进制信息
- 运行时才生成的class二进制信息
连接过程
连接过程为重点 主要分为 验证二进制信息是否正确 为类的静态变量分配内存和初始值(初始值不是代码设置而是类型固定值例如int为0) 解析类,接口,字段,方法的符号引用是否正确
验证阶段
- 验证文件格式是否正确
查看二进制文件头部魔术因子是不是class的魔术因子
查看java版本,低版本的java不能兼容高版本的class
通过class的MD5加密信息来查看二进制流信息是否被修改
查看常量的引用和值不能为不支持的类型
- 验证元数据信息
查看 类是否有父类或接口,有的话看其是否存在和合法
查看 final修饰的类是不能继承 final的方法也不能被重写
查看 该类是不是抽象类的实现 其对象的接口是否实现全部
查看 该类的重载的方法是否正确 方法名和参数类型顺序都一致,只是返回值不一样 这种是不正确的
- 字节码验证
主要验证程序的控制流程是否正确,如循环,分支等
- 符合引用是否正确
主要验证各个类的引用是否正确,是否存在.
准备阶段
为类的静态变量分配内存和初始值(初始值不是代码设置而是类型固定值例如int为0)
解析阶段
主要是针对类接口,字段,类方法,接口方法这四个类进行,从其对应的常量池取值
- 类接口解析
查看类是否存在,是否正确,不正确就抛出异常 - 字段解析
查看类接口中的字段 是否存在,是否正确,不正确也抛出异常 - 类方法解析
类的静态方法可以直接通过类名获取,而接口静态方法,必须要有实现类才可以通过类名获取。 - 接口方法解析
和类方法一样 检查是否存在,是否正确,不正确则抛出异常。
初始化阶段
给所有的类变量重新赋予正确的值,设置为代码中的值而不是默认初始值。
class initialize方法是编译阶段生成的,是包含在class文件中的。包含类变量的赋值动作和静态预计块的执行代码。当类既没有静态变量也没有静态方法,那么class initialize方法则没有数据。
以上为类加载过程,后面同JVM加载器来保证类在JVM中的唯一性。
JVM类加载器
- 根类加载器:BootstrapClassLoader 用来加载虚拟机的核心类库,用C++编写实现
- 扩展类加载器:ExtClassLoader 用来加载JAVA_HOME目录下的jre/lb/ext目录下的类库,用java编写实现
- 系统类加载器:ApplicationClassLoader 用来加载CLASS_PATH目录下的类库和jar包,用java编写实现,一般为自定义加载器的父类加载器(这个父类不是指extends关键字来实现,而是使用组合方式实现即类中有变量保持父类加载器)
- 自定义加载器:一定为ClassLoader类的子类,通过重写findClass方法来实现加载其他的二进制class类,或者通过重写loaderClass方法来破坏双亲委派机制(先判断自己找到没有,找到直接返回,而不去父类加载器再找),加载指定的class文件。
自定义类加载器实现
- 自定义类加载器实现类
/**
* 自定义 类加载器
*/
// 自定义类加载器必须为ClassLoader的子类
public class MyClassLoader extends ClassLoader {
// 设置class存放路径
private final static Path DEFAULT_CALSS_DIR = Paths.get("C:/Java/classloader");
private final Path classDir;
public MyClassLoader() {
super();
this.classDir = DEFAULT_CALSS_DIR;
}
// 可以设置class存放路径的构造
public MyClassLoader(String classDir){
super();
this.classDir = Paths.get(classDir);
}
// 可以设置class存放路径和 指定父类加载器的构造
public MyClassLoader(String classDir,ClassLoader parent){
super(parent);
this.classDir = Paths.get(classDir);
}
// 重写父类的findClass方法 通过这个方法查找class 二进制信息
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 读取二进制信息
byte[] bytes = this.readClassBytes(name);
// 如果为null 或者没有信息时 抛出异常
if(bytes == null || bytes.length == 0){
throw new ClassNotFoundException(name+" 这个class文件不存在或为空白");
}
// 加载class
return this.defineClass(name,bytes,0,bytes.length);
}
// 将class文件读进内存
private byte[] readClassBytes(String name) throws ClassNotFoundException{
// 将包名分隔符 . 换成文件分分隔符 /
String classPath = name.replace(".","/");
// 测试数据时 直接拷贝HelloWorld.class 到类加载目录 所以要截取最后类的信息 包名信息忽略
int index = classPath.lastIndexOf("/");
String className= classPath.substring(index+1);
Path classFullPath = classDir.resolve(Paths.get(className + ".class"));
if(!classFullPath.toFile().exists()){
throw new ClassNotFoundException("类名为:"+name+" 不存在");
}
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()){
Files.copy(classFullPath,outputStream);
return outputStream.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("读取class二进制信息报错 名称:"+name,e);
}
}
@Override
public String toString() {
return "My ClassLoader";
}
}
- 生成测试的HelloWorld.class文件信息
public class HelloWorld {
static {
System.out.println("Hello World Class is Initialized");
}
public String welcome(){
return "Hello World";
}
}
- 将生成的class文件拷贝到自定义类加载器的目录中
- 将HelloWorld类的打印信息改下 后面查看自定义类加载器加载的系统类加载器的class还是其文件目录的class
public class HelloWorld {
static {
System.out.println("测试是否会被加载");
}
public String welcome(){
return "测试方法";
}
}
- 编写自定义类加载器测试类
// 测试自定义加载器是否可以加载helloWorld
class MyClassLoaderTest{
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
// 情况1 使用自定义类加载器 没有重写loaderClass方法
MyClassLoader myClassLoader = new MyClassLoader();
// 当前类的加载器 为系统类加载器(ApplicationClassLoader)即 系统配置的ClassPath路径都可以加载到 jar包等 默认是自定义加载器的父类加载器
ClassLoader appClassLoader = MyClassLoader.class.getClassLoader();
// 情况2 可以指定扩展类加载器(ExtClassLoader)做为父类加载器 由纯java代码编写 加载JAVA_HOME下的jre/lb/ext的类库
ClassLoader ExtClassLoader = appClassLoader.getParent();
myClassLoader = new MyClassLoader("C:/Java/classloader",ExtClassLoader);
// 情况3 也可以指定根类加载器(BootstrapClassLoader)为父类加载器 设置为null 就是指定为根类加载器 有C++编写 是用来加载虚拟机核心类库
myClassLoader = new MyClassLoader("C:/Java/classloader",null);
// 情况4 使用破坏双亲委派机制的自定义类加载器
myClassLoader = new BrokerClassLoader();
// 加载HelloWorld.class 并不会执行static块代码
Class<?> aClass = myClassLoader.loadClass("com.fenggf.simple.classLoader.HelloWorld");
System.out.println("------------1-------------");
System.out.println(aClass.getClassLoader());
// 初始化对象 才会执行static块代码
System.out.println("------------2-------------");
Object helloWorld = aClass.newInstance();
System.out.println(helloWorld);
// 使用对象的方法
System.out.println("------------3-------------");
Method method = aClass.getMethod("welcome");
Object result = method.invoke(helloWorld);
System.out.println("输出结果:"+result);
}
}
执行情况
情况1 使用自定义类加载器 但没有破坏双亲委派规则 执行系统类加载器找到的class
------------1-------------
sun.misc.Launcher$AppClassLoader@18b4aac2
------------2-------------
测试是否会被加载
com.fenggf.simple.classLoader.HelloWorld@677327b6
------------3-------------
输出结果:测试方法
情况2 指定父类加载器为扩展类加载器 执行自定义加载器目录的class
------------1-------------
My ClassLoader
------------2-------------
Hello World Class is Initialized
com.fenggf.simple.classLoader.HelloWorld@7f31245a
------------3-------------
输出结果:Hello World
情况3 指定父类加载器为根类加载器 执行自定义加载器目录的class
------------1-------------
My ClassLoader
------------2-------------
Hello World Class is Initialized
com.fenggf.simple.classLoader.HelloWorld@7f31245a
------------3-------------
输出结果:Hello World
情况4 使用破坏双亲委派机制的自定义加载器 执行自定义加载器目录的class
------------1-------------
My ClassLoader
------------2-------------
Hello World Class is Initialized
com.fenggf.simple.classLoader.HelloWorld@6d6f6e28
------------3-------------
输出结果:Hello World
注意:每个类加载器找到class文件时,自己都会存放一份数据,即使相同类加载的不同对象实例,他们都各自存放一份数据(不同class对象)。