1. 引言
我们日常开发的Java代码都是保存在以.java
为后缀的文件当中。想要执行java代码的话,首先需要将.java
文件编译成以.class
为后缀的字节码文件,然后类加载器将.class
字节文件加载到JVM
当中,最后在JVM
中运行我们编写的代码。
2. 类的加载
2.1. 触发类加载的时机
在代码运行过程中,使用到哪个类就会触发哪个类的加载。具体来说,当遇到以下6种情况会触发:
- 通过
new
关键字创建对象; - 访问类的静态变量;
- 访问类的静态方法;
- 对某个类进行反射,如
Class.forName("")
; - 加载子类时会导致父类的加载;
- 包含main方法的启动类。
当首次遇到上面6中情况之时,会导致类的加载(包含类加载的一系列过程)。
2.2. 类加载的过程
类的加载主要包含三个阶段:加载、连接、初始化
,其中连接阶段分为:验证、准备、解析。
各个阶段主要的工作如下
- 加载:查找并加载class文件到 JVM内存;
- 验证:检查class文件是否符合JVM的规范;
- 准备:1.给类以及类中的静态变量分配内存空间;2. 给静态变量
设置默认的初始值
; - 解析:将符号引用替换成直接引用,即内存地址;
- 初始化:执行静态代码块,并给
静态变量赋值
。
public static Integer num=10
类变量num在准备阶段的值会设置成0,初始化阶段才会设置为10。
加载阶段可以和连接阶段交叉执行,即在加载阶段开始之前,验证阶段开始进行验证。
class被加载之后的内存情况,如下图所示:
class被加载后的内存情况3. 类加载器
JVM为我们提供了三大内置类加载器,不同的类加载器负责将不同的类加载的JVM内存中。三大内置加载器分别是:
- BootstrapClassLoader(根类加载器);
- ExtClassLoader(扩展类加载器);
- ApplicationClassLoader(应用类加载器);
3.1. 根类加载器
根类加载器是最顶层的类加载器,没有父类。它是用C++编写的,主要用来加载JDK安装目录下jre/lib/
中用于支撑Java系统运行的核心类库,例如:java.lang
包下的类。
可以通过-Xbootclasspath
来指定根加载器的路径,也可以通过系统属性来得知当前JVM的根加载器都加载了哪些资源,如以下程序:
public class BootStrapClassLoaderTest {
public static void main(String[] args) {
/**
* 输出 Bootstrap:null
* 根类加载器获取不到引用,所以打印出来为null
*/
System.out.println("Bootstrap:"+String.class.getClassLoader());
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
3.2. 扩展类加载器
扩展类加载器主要用于加载JAVA_HOME下的jre/lib/ext
子目录下的类库,它的父类是根类加载器。扩展类加载器是由纯Java代码实现的,它的完整类名是sun.misc.Launcher$ExtClassLoader
。扩展类加载器所加载的路径,可以通过java.ext.dir
系统属性获得。
3.3. 系统类加载器
系统类加载器主要负责classpath
下的类资源加载,我们开发过程中所依赖的第三方jar包默认就是系统类加载器加载的。系统类加载器的父类是扩展类加载器,其类全名是sun.misc.Launcher$AppClassLoader
。系统类加载器的加载路径一般通过-classpath或者-cp指定,同样也可以通过系统属性java.class.path
进行获取。实例代码如下:
public class ApplicationClassLoaderTest {
public static void main(String[] args) {
System.out.println("classloader: "+ApplicationClassLoaderTest.class.getClassLoader());
System.out.println(System.getProperty("java.class.path"));
}
}
3.4. 自定义类加载器
除了上面介绍的三大内置类加载器,我们还能根据需求实现自己的类加载器。所有的自定义类加载器都需要直接或者间接继承ClassLoader
类,这个类是一个抽象类,但是没有抽象方法,但是其中的findClass(String name)
方法必须得重写,因为其默认实现会抛出一个异常。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
下面的代码,演示了如何自定义类加载器
public class MyClassLoader extends ClassLoader{
private final static Path DEFAULT_CLASS_DIR = Paths.get("/Users/classloader");
private final Path classDir;
public MyClassLoader () {
super();
this.classDir = DEFAULT_CLASS_DIR;
}
public MyClassLoader (String classDir) {
super();
this.classDir = Paths.get(classDir);
}
public MyClassLoader (String classDir, ClassLoader parentClassLoader) {
super(parentClassLoader);
this.classDir = Paths.get(classDir);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes = this.readClassBytes(name);
if (null == classBytes || classBytes.length == 0 ){
throw new ClassNotFoundException("Can not load the class "+name);
}
return this.defineClass(name,classBytes,0,classBytes.length);
}
private byte[] readClassBytes(String className) throws ClassNotFoundException {
String classPath = className.replace(".","/");
Path classFullPath = classDir.resolve(Paths.get(classPath+".class"));
if (!classFullPath.toFile().exists()) {
throw new ClassNotFoundException("The class "+className+" not found");
}
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
Files.copy(classFullPath,baos);
return baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("load the class "+className+" occur error");
}
}
}
使用自定义类加载器来加载类:
private static void testMyClassLoader() throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class<?> aClass = classLoader.loadClass("thread.classloader.Hello");
System.out.println(aClass.getClassLoader());
Object hello = aClass.newInstance();
System.out.println(hello);
Method method = aClass.getMethod("hello", String.class);
String result = (String) method.invoke(hello, "tomcat");
System.out.println("Result:"+result);
}
完整的代码请访问:https://github.com/codingXcong/Java-Guide/tree/master/Thread/src/main/java/thread/classloader
上面的示例中,我们通过自定义类加载器对Hello.java
进行加载,运行示例的时候,需要先将java文件编译成字节码文件,然后将字节码文件放入MyClassLoader
类中指定的classDir
路径下。
PS:如果在IDEA环境中,还需要将Hello.java
文件删除,不删除的话,根据类加载器的双亲委派模型,会有MyClassLoader
的父类加载器对Hello类进行加载。
3.5. 双亲委派模式
JVM的类加载器是有亲子层级结构的,如下图所示:
基于这个亲子层级结构,有个双亲委派的机制。在进行类加载的时候,当一个类被调用loadClass之后,它并不会直接去加载,而是交给当前类加载器的父加载器尝试加载,直到传递到最顶层的父加载器。
例如,现在JVM需要加载A
类,此时系统类加载器会问问自己的爸爸,也就是扩展类加载器,你能加载到A
类吗?
然后扩张类加载器会直接问自己的爸爸,启动类加载器,你能加载A
类吗?
启动类加载器在Java安装目录下没有找到这个类,就告诉扩张类加载器,我没法加载这个类,你自己加载去吧。
扩展类加载器就尝试自己加载,它在jre/lib/ext
下也没找到这个类,就会通知系统类加载器,没有加载到类A
,你自己去加载。
最后系统类加载器在自己负责加载的范围内找到了类A
,然后就将其加载到内存中。
为啥要设计成双亲委派模式么?防止同一个类被重复加载。
我们再通过源码来看看双亲委派的工作方式,其逻辑主要封装在ClassLoader.loadClass(String name)
中:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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);
} 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);
// 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;
}
}
网友评论