Java类加载

类的加载概述

我们编写的.java扩展名的源代码文件存储着要执行的程序逻辑,这些文件需要经过java编译器编译成.class文件,.class文件中存放着编译后虚拟机指令的二进制信息。当需要用到某个类时,虚拟机将会加载它,并在内存中创建对应的class对象,这个过程称之为类的加载。一个类的生命周期从类被加载、连接和初始化开始,只有在虚拟机内存中,我们的java程序才可以使用它。

1

类的加载

通过类的完全限定名(包名和类名)查找此类的字节码文件,把类的.class文件中的二进制数据读入到内存中,并存放在运行时数据区的方法区中,然后利用字节码文件创建一个Class对象,用来封装类在方法区内的数据结构并存放在堆区内,这个过程是由类加载器完成的。

连接
  • 验证:确保被加载类的正确性。Class文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全。
  • 准备:为类的静态变量分配内存,并将其初始化为默认值,此阶段仅仅只为静态变量(即static修饰的字段变量)分配内存,并且设置该变量的初始值。对于final static修饰的变量,编译的时候就会分配了,也不会分配实例变量的内存。
  • 解析:把类中的符号引用转换为直接引用,符号引用就是一组符号来描述目标,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化

类加载最后阶段,若该类具有父类,则先对父类进行初始化,执行静态变量赋值和静态代码块代码,成员变量也将被初始化。

类加载器

类的加载是由类加载器完成的,类加载器可以分为两种:第一种是Java虚拟机自带的类加载器,分别为启动类加载器、扩展类加载器和系统类加载器。第二种是用户自定义的类加载器,是java.lang.ClassLoader的子类实例。

虚拟机内置类加载器
根类加载器(Bootstrap)

根类加载器是最底层的类加载器,是虚拟机的一部分。它是由C++语言实现的,且没有父加载器,也没有继承java.lang.ClassLoader类。它主要负责加载由系统属性sun.boot.class.path指定的路径下的核心类库,出于安全考虑,根类加载器只加载java、javax、sun开头的类。

1
2
3
4
public static void main(String[] args) {
ClassLoader cl = Object.class.getClassLoader();
System.out.println(cl); //根类加载器打印出来的结果是null
}

这里是打印出null的原因是由于BootStrapClassLoader是用c++写的,使用原生代码来实现,并不继承于java.lang.ClassLoader,所以在返回该ClassLoader时就会返回null

扩展类加载器(Extension)

扩展类加载器是指原SUN公司实现的sun.misc.launcher$ExtClassLoader类(JDK8),它是由java语言编写,父加载器是根类加载器,负责加载<JAVA_HOME>\jre\lib\ext目录下的类库或者系统变量java.ext.dirs指定的目录下的类库。

测试dnsns.jar下类的类加载器,因为该jar包在jre\lib\ext目录下

2

1
2
3
4
public static void main(String[] args) throws Exception {
ClassLoader classLoader1 = DNSNameService.class.getClassLoader();
System.out.println("DNSNameService类的类加载器是: "+classLoader1);
}

3

系统类加载器(System)

系统类加载器也称之为应用类加载器,也是纯Java类,是原SUN公司实现的sun.misc.Launcher$AppClassLoader类(JDK8)。它的父加载器是扩展类加载器,它负责从classpath环境变量或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。一般情况下,该类加载器是程序中默认的类加载器,可以通过ClassLoader.getSystemClassLoader()直接获得

1
2
3
4
5
6
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader classLoader1 = ClassLoaderTest.class.getClassLoader();
System.out.println("ClassLoaderTest类的类加载器是: "+classLoader1);
}
}

自己编写的类使用的类加载器结果为sun.misc.Launcher$AppClassLoader

小结

在程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,同时我们还可以自定义类加载器。需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把加载类的请求交由父加载器处理,它是一种任务委派模式。

类加载器的双亲委派机制

除了根类加载器之外,其他的类加载器都需要有自己的父加载器。从JDK1.2开始,类的加载过程采用双亲委派机制,这种机制能够很好的保护Java程序的安全,除了虚拟机自带的根类加载器之外,其余的类加载器都有唯一的父加载器,比如,如果需要ClassLoader加载一个类时,该ClassLoader先委托自己的父加载器先去加载这个类,若父加载器能够加载,则由父加载器加载,否则才由ClassLoader自己加载这个类。真正加载类的加载器我们叫做启动类加载器,注意,双亲委派机制的父子关系并非面向对象程序设计中的继承关系,而是通过使用组合模式来复用父加载器代码,这种机制如下图所示

4

测试自定义类的类加载器的父子关系,通过getParent()来获取父类加载器

1
2
3
4
5
6
7
8
9
10
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println("ClassLoaderTest类的类加载器是: "+classLoader);
while (classLoader!=null){
System.out.println(classLoader);
classLoader = classLoader.getParent();
}
}
}

打印出的结果为

1
2
3
ClassLoaderTest类的类加载器是: sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
双亲委派机制的好处
  • 可以避免类的重复加载,当父类加载器已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 考虑到安全因素,Java核心API种定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Object的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在Java核心API发现这个名字的类,发现该类已经被加载,并不会重新加载网络传递过来的java.lang.Object,而直接返回已加载过的Objec.class,这样便可以防止核心API库被随意篡改。

ClassLoader

所有的类加载器(除了根类加载器)都必须继承java.lang.ClassLoader,它是一个抽象类,主要的方法如下

loadClass

ClassLoader的源码中,有一个方法loadClass(String name, boolean resolve),这里就是双亲委派模式的代码实现。从源码中我们可以观察到它的执行顺序,需要注意的是,只有父类加载器加载不到类时,会调用findClass方法进行类的查找,所以在定义自己的类加载器时,不要覆盖掉该方法,而应该覆盖掉findClass方法。

ClassLoader类的loadClass源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
}
}

在源码中,首先会通过findLoadClass方法检查类是否已经被加载。如果没有被加载,就会执行双亲委派模式,通过父类去加载,即在父类上调用loadClass方法,如果父类加载不到的话,则使用虚拟机的内置类加载器,如果都没有加载成功,就会通过自己的findClass方法去加载。

findClass

在自定义类加载器时,一般我们需要覆盖这个方法,且ClassLoader中给出了一个默认的错误实现。如果我们覆盖了这个方法,则会调用我们自己写的方法

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass

该方法用来将byte字节解析成虚拟机能够识别的Class对象,defineClass()方法通常与findClass()方法一起使用。在自定义类加载器时,会直接覆盖ClassLoaderfindClass()方法获取要加载类的字节码,然后调用defineClass()方法生成Class对象

1
2
3
4
5
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
resolveClass

连接指定的类,类加载器可以使用此方法来连接类

1
2
3
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}

URLClassLoader

java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上指定的位置加载类,我们可以使用该类作为自定义的类加载器使用。

构造方法

1
2
3
4
5
6
7
8
9
10
public URLClassLoader(URL[] urls) {
super();
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc);
}

指定要加载的类所在的URL地址,父类加载器默认认为系统类加载器

1
2
3
4
5
6
7
8
9
10
public URLClassLoader(URL[] urls, ClassLoader parent) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc);
}

指定要加载的类所在的URL地址,并指定父类加载器。

使用URLClassLoader加载本地类

创建一个Demo类如下,路径为/Users/caoyifan/

1
2
3
4
5
6
package elssm.test;
public class Demo{
public Demo(){
System.out.println("demo instance");
}
}

编译该类

1
javac -d . Demo.java

接着使用自己写的类去加载Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package test;

import java.io.File;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;


public class ClassLoaderDemo2 {
public static void main(String[] args) throws Exception {
File file = new File("/Users/caoyifan/");
URI uri = file.toURI();
URL url = uri.toURL();

URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
System.out.println("父类加载器:"+classLoader.getParent());
Class<?> clazz = classLoader.loadClass("elssm.test.Demo");
clazz.newInstance();

}
}

输出如下

1
2
父类加载器:sun.misc.Launcher$AppClassLoader@7f31245a
demo instance
使用URLClassLoader加载网络上的类

Mac上Apache服务器默认的web根目录在:/Library/WebServer/Documents,我们可以将Demo;类放在该目录下进行加载

1
2
3
4
5
6
7
8
9
10
11
12
13
package test;

import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderDemo3 {
public static void main(String[] args) throws Exception {
URL url = new URL("http://localhost:80/");
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
Class<?> clazz = classLoader.loadClass("elssm.test.Demo");
clazz.newInstance();
}
}

自定义类加载器

自定义文件类加载器
  • 继承ClassLoader
  • 覆盖findClass方法

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package test;
import java.io.*;

public class MyFileClassLoader extends ClassLoader {
private String directory;
public MyFileClassLoader(String directory){
this.directory = directory;
}
public MyFileClassLoader(String directory,ClassLoader parent){
super(parent);
this.directory = directory;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//把类名转换为目录
String file = directory + File.separator+name.replace(".",File.separator)+".class";
//构建输入流
InputStream in = new FileInputStream(file);
//构建字节输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte buf[] = new byte[1024];
int len = -1;
while((len = in.read(buf))!= -1){
baos.write(buf,0,len);
}
byte data[] = baos.toByteArray(); //读取到的字节码的二进制数据
in.close();
baos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) throws Exception {
MyFileClassLoader classLoader = new MyFileClassLoader("/Users/caoyifan/");
Class clazz = classLoader.loadClass("elssm.test.Demo");
clazz.newInstance();
}
}
自定义网络类加载器

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class MyURLClassLoader extends ClassLoader{
private String url;
public MyURLClassLoader(String url){
this.url = url;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String path = url+"/"+name.replace(".","/")+".class";
URL url = new URL(path);
InputStream in = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte buf[] = new byte[1024];
int len = -1;
while ((len=in.read(buf))!=-1){
baos.write(buf,0,len);
}
byte[] data = baos.toByteArray();
in.close();
baos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) throws Exception{
MyURLClassLoader classLoader = new MyURLClassLoader("http://localhost:80");
Class<?> clazz = classLoader.loadClass("elssm.test.Demo");
clazz.newInstance();

}
}
热部署类加载器

当我们调用loadClass方法加载类时,会采用双亲委派模式,即如果类已经被加载,就从缓存中获取,不会重新加载,如果同一个class被同一个类加载器加载多次,则会报错。因此我们要实现热部署让同一个class文件被不同的类加载器重复加载即可,但是不能调用loadClass方法,而应该调用findClass方法,避开双亲委派模式,从而实现同一个类被多次加载,实现热部署。

具体测试

使用loadClass加载,输出的hashCode是相同的,说明没有被重复加载。

1
2
3
4
5
6
7
8
9
10
11
12
package test;

public class ClassLoaderDemo4 {
public static void main(String[] args) throws Exception{
MyFileClassLoader classLoader1 = new MyFileClassLoader("/Users/caoyifan/");
MyFileClassLoader classLoader2 = new MyFileClassLoader("/Users/caoyifan/",classLoader1);
Class<?> clazz1 = classLoader1.loadClass("elssm.test.Demo");
Class<?> clazz2 = classLoader2.loadClass("elssm.test.Demo");
System.out.println(clazz1.hashCode());
System.out.println(clazz2.hashCode());
}
}

使用findClass加载,输出的hashCode是不同的,说明被重复加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
package test;

public class ClassLoaderDemo4 {
public static void main(String[] args) throws Exception{
MyFileClassLoader classLoader1 = new MyFileClassLoader("/Users/caoyifan/");
MyFileClassLoader classLoader2 = new MyFileClassLoader("/Users/caoyifan/",classLoader1);
Class<?> clazz1 = classLoader1.findClass("elssm.test.Demo");
Class<?> clazz2 = classLoader2.findClass("elssm.test.Demo");
System.out.println(clazz1.hashCode());
System.out.println(clazz2.hashCode());

}
}

类的显式与隐式加载

类的加载方式是指虚拟机将class文件加载到内存的方式。

显式加载是指在Java代码中通过调用ClassLoader加载class对象,比如Class.forName(String name)或者this.getClass().getClassLoader().loadClass()加载类

隐式加载不需要在Java代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中,比如在加载某个class时,该class引用了另外一个类的对象,那么这个对象的字节码文件就会被虚拟机自动加载到内存中。