全局视图
类加载子系统作用:
- 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由E xecution Engine决定。
- 加载的类信息存放于一块称为方法区(Metadata Space)的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器子系统就是从网络、文件等地方加载 .class
文件到 JVM 内存中,这一块内存被称为方法区。主要阶段有:
加载阶段
简而言之就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型 — 类模板对象,即 java.lang.Class
。
类模板对象:Java 类在 JVM 内存中的一个快照,JVM 将字节码文件中解析出的常量池、类字段、类方法等信息存储到模板中,这些 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用。反射机制基于这一基础。
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。相当提供一个 Visitor,通过这个 Visitor 来访问这个对象的所有元数据信息。方法区在 JDK1.8 之前是永久代,JDK 1.8 之后被称为元空间。
java.lang.Class
实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过java.lang.Class
类提供的接口,可以获得目标所关联的.class
文件中具体的数据结构:方法、字段等信息。
数组类加载
创建数组类的情况稍微有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建 Person[]
类过程如下:
- 如果数组的元素类型是引用类型。那么就遵循定义的加载过程并递归加载和创建数组 A 的元素类型。
-
链接阶段
1. 验证
目的是保证加载的字节码是合法、合理并符合 JVM 虚拟机规范。其中包含:
文件格式验证。
- 元数据验证。
- 字节码验证。这是最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是否是合法的、符合逻辑的。JVM 通过在编译期生成
StackMapTable
来加速字节码验证,但不能保证 100%。 -
2. 准备
准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段。
注意: 准备阶段不包含基本数据类型的字段用
static final
修饰的情况,因为 final 在编译时就会分配了,准备阶段会显示赋值。- 这一步不会为实例变量分配初始值,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。
- 在这个阶段不会像初始化阶段那样会有初始化或者代码被执行。
3. 解析
- 将常量池内的符号引用转换为直接引用的过程。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info 等
字面量(Literal):字面量主要包括字符串字面量、整型字面量和声明为 final 的常量值等。 符号引用(Symbolie Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量。 直接引用(Direct References):可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
对于 invokedynamic
指令比较复杂,前面解析的结果可能本次并不适用。在 JDK 9 引入模块化后,权限验证变得更加复杂。
JVM 给每个类都准备了一张方法表,将其所有的方法都记录在方法表中。当需要调用一个类的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。
解析字符串
当在 Java 代码中直接使用字符串常量时,就会在类中出现 CONSTANT_String
,它表示字符串常量,并且会引用一个 CONSTANT_UTF8
的常量项。JVM 内部运行中的常量池会维护一张字符串拘留表(intern),它会保存所有出现过的字符串常量,并且没有重复项。只要以 CONSTANT_String
形式出现的字符串也会出现在这张表中。使用 String.intern()
方法可以得到一个字符串在拘留表中的引用。
在 Java 的内存分配中,总共有 3 类常量池,分别是:
- Class 常量池
- 运行时常量池
- 字符串常量池
在 HotSpot VM 中字符串常量池是通过一个 StringTable
类实现的,它是一个 Hash 表,默认值大小长度是 1009。这个对象在每个 HotSpot VM 的实例中只有一份,被所有的类共享。在 JDK6 及以前,字符串常量池是放在 Perm Gen区(即方法区)中。在JDK7 版本中,字符串常量池被移动了堆中。在 JDK6 及之前版本中,String Pool 里放的都是字符串常量;在 JDK7.0 中,由于 String.intern()
发生了改变,因此 String Pool 中也可以存放放于堆内的字符串对象的引用。
由于字符串使用场景非常多,JVM 为了提高性能和减少内存开销,在实例化字符串时进行一些优化:使用字符串常量池。当使用 ""
创建字符串常量时,JVM 首先会检查字符串常量池是否有相同的字符串对象(equals()
),如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于 String 的不可变性,常量池中一定不存在两个相同的字符串。
对于 +
而言,底层 JVM 会实例化一个 StringBuilder
对象并通过 append()
追加字符串,不要在 for 循环体中使用 +
,这样会导致 StringBuilder
不断创建和销毁。
编译期确定
System.out.println("Hello" + "World");
// 反编译后
System.out.println("HelloWorld");
/**
* 编译期确定
* 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
* 所以此时的"a" + s1和"a" + "b"效果是一样的。故结果为true。
*/
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = true
/**
* 编译期无法确定
* 这里面虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定
* 因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
*/
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = false
public String getS1() {
return "b";
}
new String(“a”)
通过 new
关键字创建字符串对象:一概在堆中创建对象,无论字符串字面值是否相等(要么创建一个,要么创建两个对象,关键要看常量池有没有)。
String s = new String("abc");
// 等价于
String original = "abc";
String s = new String(original);
通过 new
操作产生一个字符串(”abc”)时,会先去常量池中查找是否有 “abc” 对象,如果没有,则创建一个此字符串对象并放入常量池中。然后,在堆中再创建 “abc” 对象,并返回该对象的地址。所以,对于 String str=new String(“abc”):如果常量池中原来没有 “abc”,则会产生两个对象(一个在常量池中,一个在堆中);否则,产生一个对象。
初始化阶段
为类变量赋予正确的初始值,执行 clinit()
方法(静态代码块或被 static 修饰的变量)。
Java 类的主动使用情况,会导致类的初始化:
- 创建类的实例。
- 访问某个类或接口的静态变量,或者对该静态变量赋值。
- 调用类的静态方法。
- 反射(
Class.forName("xxx")
)。 - 初始化一个类的子类。
- Java 虚拟机启动时被标明为启动类的类。
- JDK7 开始提供的动态语言支持:
java.lang.invoke.MethodHandle
实例的解析结果 REF_getStatic、REF putStatic、REF_invokeStatic 句柄对应的类没有初始化,则初始化。
除了以上七种情况,其他使用 Java 类的方式都被看作是对类的被动使用(不会导致类的初始化,即不会执行初始化阶段,不会调用 clinit()
方法和 init()
方法)。
还需要知道:
- 引用该类的静态常量时不会导致初始化。但对于那些需要计算才能得出结果的常量就会导致该类的初始化。
- 构造某个类的数组时不会导致该类的初始化。
- 常量在编译阶段会存入调用方法所在类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
- 初始化一个类的子类这条规则不适用于接口。只有当首次使用该接口的静态变量时,才会初始化。
clinit()
- 初始化阶段就是执行类构造器方法
<clinit>()
的过程。 - javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块)中的语句合并而来。也就是说,当我们代码中包含 static 变量的时候,就会有
<clinit>()
方法。 - 里面的指令按语句在源文件中出现的顺序执行。
<clinit>()
不同于类的构造器。(即<init>()
)。- 若该类具有父类,JVM 会保证子类的
<clinit>()
执行前,父类的<clinit>()
已经执行完毕。 - 虚拟机必须保证一个类的
<clinit>()
方法在多线程下被同步加锁。 clinit()
方法可能会出现死锁情况,这种情况排查问题十分困难。类的卸载
一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期。ClassLoader 类加载器
概述
在 Java 语言中,提供有一个系统的环境变量叫做 classpath,这个环境属性的作用主要是在 JVM 进程启动的时候进行类加载路径的定义。在 JVM 里面可以根据类加载器而后可以根据指定路径中类的加载,也就是说找到了类的加载器就意味着找到了类的来源。类加载器层次结构
ExtClassLoader
在 JDK9 中被移除,取而代之的是 PlatformClassLoader
,移除的原因有以下几点:
ExtClassLoader
允许用户将公共的类放到安装路径下的/ext
目录,JVM 启动时就会主动加载这些类。但是这会存在严重安全漏洞。- 为了与系统类加载器和应用类加载器之间保持设计平衡,提供
PlatformClassLoader
。 - JDK9 的模块化。
PlatformClassLoader
是 JDK9提供的,以前叫 ExtClassLoader
,因为在 JDK 的安装目录下提供一个 ext
目录,开发者可以将 .jar
文件拷贝到目录里面,这样就可以直接执行了。但是这样存在安全隐患,所以 JDK9 就彻底废除。。
双新委派机制
Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 class 对象。而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
- 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常。
从 Class 类中获取类加载器
java.lang.Class
类是对某个被 JVM 完成加载的类的透视视图,可以从这个对象中获取关于此类的所有信息,比如:
- 是否含有注解
- 获取类加载器
- 包名、方法名
- ….
通过 java.lang.Class#getClassLoader
可以获取加载该类的类加载器。
ClassLoader classLoader = te.getClass().getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
// OUTPUT
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
自定义类加载器
Tomcat 破坏双亲委派模型
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
// Log access to stopped class loader
checkStateForClassLoading(name);
// 1、tomcat缓存中是否已存在?
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
///2、jvm缓存中是否已存在?
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// Swallow all exceptions apart from those that must be re-thrown
ExceptionUtils.handleThrowable(t);
tryLoadingFromJavaseLoader = true;
}
// 3、尝试使用ExtClassLoader加载
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name, true);
// 4、是否设置了代理,则委托父类先加载
// 没有打破双亲委派机制
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
// 5、尝试使用AppClassLoader加载
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 6、没有代理,则尝试使用当前类加载器(WebappClassLoader)加载
// 此时打破了双亲委派的机制,没有委托父类加载!
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 7、即使没设置代理,但是还没有加载到,则尝试使用AppClassLoader加载
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
在 JDK1.2 之前,在自定义类加载器时,总会去继承 ClassLoader 类并重写 loadClass() 方法,从而实现自定义的类加载类,但是在 JDK1.2 之后已不再建议用户去覆盖 loadClass() 方法,而是建议把自定义的类加载逻辑写在 findclass() 方法中
- 对于各个 webapp 中的 class 和 lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的 lib 以便不浪费资源。与 jvm 一样的安全性问题。使用单独的 classloader 去装载 tomcat 自身的类库,以免其他恶意或无意的破坏;
热部署。相信大家一定为 tomcat 修改文件不用重启就自动重新装载类库而惊叹吧。
JDBC 示例
当我们加载 jdbc.jar 用于实现数据库连接的时候。
我们现在程序中需要用到 SPI 接口,而 SPI 接口属于 rt.jar 包中 Java 核心 API。
- 然后使用双亲委派机制,Bootstrap ClassLoader 把 rt.jar 包加载进来,而 rt.jar 只是提供接口,具体的实现类由不同厂商提供。而实现类是由 AppClassLoader 加载,这里就是出现冲突的根本原因。
类的主动使用和被动使用
实验一:静态变量
public class Person {
public static String H = "Person";
public final static String F_H = "Final Person";
public Person() {
System.out.println("Construct");
}
{
System.out.println("{}");
}
static {
System.out.println("static");
}
}
// 并不会初始化 Person 类
String s1 = Person.F_H;
// 这一步才会主动初始化Person类
System.out.println(s1);
// 这一步会主动初始化Person类
String s2 = Person.H;
// 这一步也会主动触发类的初始化,如果该类已经被初始化了,就跳过
Person p = new Person();
子父类
如果实例化一个子类,首先需要对父类进行初始化操作,然后才轮到子类。当两个类完成初始化操作后,再轮到实例化。
数组
// 这个不会导致Person类初始化
Person[] person = new Person[2];
forName 和 loadClass 有什么区别
forName()
会导致类的主动加载,而getClassLoader()
不会导致类的主动加载。Class.forName()
是一个静态方法。该方法将 Class 文件加载到内存的同时会执行类的初始化。如果需要请求的类型在装载时就初始化(包括连接),就必须使用forName
。ClassLoader.loadClass()
这是一个实例方法,需要一个 ClassLoader 实例对象。这个方法将 Class 文件加载到内存时并不会执行类的初始化,直接该类第一次使用时才进行初始化。需要从一些特定的途径加载类(比如网络、数据库、加密文件中),甚至是动地创建它们,这里就需要一个类加载器。
如果没有特别要使用类加载器的要求,应该使用 forNmae()
,因为它是动态扩展最直接的方法。