Java 是个强类型的编程语言,一个变量的类型在编译时就已经决定了,就是最初声明它的类型, 如果想把它当成另一个类型来使用,需要先经过显式的类型转换,转换时可能会因为类型不符而抛出 ClassCastException
。
本篇以 《深入分析 Java Web 技术内幕》 第六章 深入分析 ClassLoader 工作机制 的内容为参考,讲解 Java 中的类型,类加载机制,类加载错误分析 等。
Java 中的类型
如开篇所说,一个 Java 对象的类型在声明的时候就被确定了,但 Java 是面对对象的语言,有丰富的接口和抽象类,一个对象被传来传去的时候,可能就丢失了它的一些类型信息,但 Java 也像 C++ 那样是拥有 RTTI (Run-Time Type Identification) 运行时类型识别 特性的语言。在任何时刻,任何一个对象都清楚地知道自己的具体类型(可以通过反射获取)。
不知道大家有没有想过,Java 中的对象是如何被创造出来的,创造时是否又遵循了一个怎样的规则。其实 Java 中的对象都是根据各自的说明书构建出来的,这个说明书信息就是在 Javac 编译原理与 class 文件结构 中生成的类的 class 字节码文件之中,这些字节码文件又被存储在永久代 (Permenent Generation Java 7 之前) / 元空间 (MetaSpace Java 8 之后) 之中。
当需要创建一个对象时,就会将这个创建任务委托给类加载器来完成,类加载器会找到指定的 class 字节码文件,根据说明书来装配出一个需要的对象。因此任何一个 Java 对象,在运行时都清楚的知道自己是由哪个类的字节码装配出来的,都可以通过 getClass() / instanceof
来获取自己的具体类型。
类加载机制
JVM 类加载机制分为五个部分:加载、验证、准备、解析、初始化,如下所示:
类加载阶段 | 作用 |
---|---|
加载 | 生成 Class 对象,作为方法区这个类的各种数据入口 |
验证 | 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求 |
准备 | 为类变量分配内存空间,并设置变量初始值,非 finanl 是0,final 会赋值 |
解析 | 虚拟机将常量池中的符号引用替换为直接引用的过程 |
初始化 | 执行类构造器方法(静态语句块) |
ClassLoader 类加载器
类加载器 ClassLoader 在 JDK 源码中的 rt.jar (rt 即 runtime) 这个包中,先看下这个类都有哪几个主要的方法:
方法 | 主要作用 |
---|---|
defineClass(byte[], int, int) | 将字节流解析成 JVM 能够识别的 Class 对象 |
findClass(String) | 获取要加载类的字节码(重写以自定义) |
resolveClass(Class<?> c) | 链接类到 JVM(实例化时进行) |
loadClass(String) | 加载某个类(不重新定义加载规则,直接调用) |
因为有 defineClass 方法的存在,构建对象实例的字节码不仅可以是编译后产生的 class 文件,它还可能是从网络上传输过来的字节流,或者是在内存中 动态生成的增强字节码。
双亲委派加载模型
JVM 提供了三层 ClassLoader:
ClassLoader | function |
---|---|
Bootstrap ClassLoader | 加载 JVM 自身工作需要的类(不遵守双亲委派原则)JAVA_HOME\lib |
ExtClassLoader | 扩展类加载器 JAVA_HOME\lib\ext |
AppClassLoader | 应用类加载器,加载用户路径 classpath 下的类 |
加载类对象时,会先委托其 parent 类加载器来加载这个类,如果 parent 类加载器加载成功,则直接返回加载结果了;如果 parent 类加载器加载失败,则再尝试使用当前的类加载器加载这个类,具体可以看 loadClass
方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
// 如果有 parent 加载器,通过 parent 来加载
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// 如果 parent 拒绝加载此类,则会有此类加载器完成加载
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委托加载机制,是出于安全考虑,JVM 核心类都是由最顶端的启动类加载器加载的,不会由下面的类加载器加载,以保证安全。比如我自己写了一个异常的 java.lang.String
,是无法被恶意地加载的,因为应用类加载器会委托其父加载器去加载,而在父加载器的已加载类列表中已经存在了 JDK 的 java.lang.String,此时就会直接返回,而不去加载这个恶意的类。
常见加载类错误分析
类加载异常 | 可能的原因 |
---|---|
ClassNotFoundException | 当前 classpath 下没有指定的文件 |
NoClassDefFoundError | 命令行运行程序时,类前面没有加包名 |
UnsatisfiedLinkError | 不小心把 JVM 中的某个 lib 删除了 |
ClassCastException | 强制类型转换时,类型不匹配 |
ExceptionInInitializerError | 对象初始化时出现异常 |