什么是类?
类是Java中一个很重要的概念,区别于面向过程编程,面向对象编程把一些现实中的东西抽象成一个类,如手机类、电脑类,他们都会有一些属性(CPU、内存等),也会有一些方法(开机、关机等)
类加载又是什么?
前面提到的类实际上就是一个 .java 为后缀的文件,但是如果想要将这个类放到其它机器上运行,就需要先将其编译成 .class 文件,利于计算机理解和网络传输,目标主机在运行程序的时候将会加载这个 .class 文件,获得类的信息,这就是类加载
.class文件有什么?
既然 .class 文件是 .java 编译过来的,自然就能想到一些方法、字段的元数据信息(存放在常量池中),除此之外还有一些文件信息
- 文件信息:如开头的魔数确认这是个 java 文件,以及其它的还可以确认 JDK 的版本号(当前值 - 54 = 当前 JDK 版本号)
- 常量池:存放着符号常量,如方法名、字段名、类名等类元数据信息
类加载过程
经常会看到加载、连接、初始化,更详细些就是加载、验证、准备、解析、初始化
- 加载:类加载器会把 jar 包里面的 .class 文件通过二进制流的方式加载到方法区,同时在内存生成一个 Class 对象指向这个类元数据
- 连接:
- 验证:验证这个 .class 文件是否符合规范、是否对 JVM 有危害
- 准备:将类属性(static修饰的字段)进行赋初值,引用属性 = null;普通属性 = 0;如果是 final ,直接赋程序内的值
- 解析:将常量池的符号引用(不一定加载到 JVM 中)转化为直接引用(已经加载到 JVM 中,有对应的字段或者方法的地址);由于类的动态绑定,这个过程可能在初始化之后执行
- 初始化:执行 JVM 创建的初始化函数 init(),将 static 代码块或者 static 变量的初始化执行,和构造器不同,只会执行一次
什么时候会进行类加载?
- 创建类的实例,new对象
- 调用静态方法或者静态字段
- 调用反射
- JVM启动加载一些系统类
类加载的几种方式
// 只会加载class文件
getClassLoader().loadClass("com.xxx.xxx.D");
// 还会进行连接操作
Class.forName("com.xxx.xxx.D");
// 加载class文件并且初始化
D d = new D();
类加载器
在加载的时候,我们依靠内存中的类加载器进行类的加载
三个重要的类加载器
- BootstrapClassLoader:启动类加载器,C++实现,获取的结果是null,<JAVA_HOME>\lib
- ExtensionClassLoader:拓展类加载器,<JAVA_HOME>\lib\ext
- AppClassLoader:应用类加载器,java -classpath(即在main包下的 .class 文件);一般情况下,该类加载器是程序的默认类加载器,我们可以通过ClassLoader.getSystemClassLoader()方法可以直接获取到它。
自定义类加载器
如果需要自定义类加载器,那么只需要继承ClassLoader
类即可,但继承ClassLoader
需要自己重写findClass()
方法并编写加载逻辑
所以如果一般没有太过复杂的需求,可以直接继承URLClassLoader
类,可以省略自己编写findClass
方法以及文件加载转换成字节码流的步骤
- 如果不想打破双亲委派机制,就重写findClass()
- 如果想打破双亲委派机制,还需重写loadClass()
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
// class文件路径
String path = "E:\\" + name + ".class";
// 根据路径读取文件到字节输出流
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
byte[] bytes = os.toByteArray();
// 加载类
return defineClass(name, bytes, 0, bytes.length);
}
}
双亲委派模型
只有父加载器反馈无法加载,才会让子加载器自己加载
目的是尽量让最顶层的BootstrapClassLoader来创建当前的类
双亲委派模型的好处
避免核心类冲突,因为都是启动类加载
避免类的重复加载(从同一个Class文件加载,类名相同,但是类的类加载器不同也是不同的类)
双亲委派模型的核心代码
重写loadClass()打破双亲委派机制
- 先检查类是否被加载过
- 调用父加载器加载
- 调用自己的加载器加载,可以通过重写findClass()自定义类加载器
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
// 如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
// 当父类的加载器不为空,则通过父类加载器的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
// 当父类的加载器为空,则调用启动类加载器BootstrapClassLoader来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
// 当父类加载器无法加载时,则调用findClass方法来加载该类
// 用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
打破双亲委派模型的例子
Tomcat
数据库驱动