深入理解Java虚拟机(二)

深入理解Java虚拟机(二)

ClassLoader

文档:https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html

public abstract class ClassLoader extends Object

A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a “class file” of that name from a file system.

Every Class object contains a reference to the ClassLoader that defined it.

Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.

Applications implement subclasses of ClassLoader in order to extend the manner in which the Java virtual machine dynamically loads classes.

Class loaders may typically be used by security managers to indicate security domains.

The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine’s built-in class loader, called the “bootstrap class loader”, does not itself have a parent but may serve as the parent of a ClassLoader instance.

Class loaders that support concurrent loading of classes are known as parallel capable class loaders and are required to register themselves at their class initialization time by invoking the ClassLoader.registerAsParallelCapable method. Note that the ClassLoader class is registered as parallel capable by default. However, its subclasses still need to register themselves if they are parallel capable.
In environments in which the delegation model is not strictly hierarchical, class loaders need to be parallel capable, otherwise class loading can lead to deadlocks because the loader lock is held for the duration of the class loading process (see loadClass methods).

Normally, the Java virtual machine loads classes from the local file system in a platform-dependent manner. For example, on UNIX systems, the virtual machine loads classes from the directory defined by the CLASSPATH environment variable.

However, some classes may not originate from a file; they may originate from other sources, such as the network, or they could be constructed by an application. The method defineClass converts an array of bytes into an instance of class Class. Instances of this newly defined class can be created using Class.newInstance.

The methods and constructors of objects created by a class loader may reference other classes. To determine the class(es) referred to, the Java virtual machine invokes the loadClass method of the class loader that originally created the class.

For example, an application could create a network class loader to download class files from a server. Sample code might look like:

ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
. . .

The network class loader subclass must define the methods findClass and loadClassData to load a class from the network. Once it has downloaded the bytes that make up the class, it should use the method defineClass to create a class instance. A sample implementation is:

class NetworkClassLoader extends ClassLoader {
String host;
int port;

public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}

private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}

Binary names

Any class name provided as a String parameter to methods in ClassLoader must be a binary name as defined by The Java™ Language Specification.

Examples of valid class names include:

"java.lang.String" // 一个类
"javax.swing.JSpinner$DefaultEditor" // 一个内部类
"java.security.KeyStore$Builder$FileBuilder$1" // 内部类的匿名类
"java.net.URLClassLoader$3$1" // 匿名类的匿名类

我们知道类的加载是双亲委派机制,我们先来看一个例子

public class MyTest15 {
public static void main(String[] args) {
ClassLoader loader = MyTest15.class.getClassLoader();
System.out.println(loader);
ClassLoader loader1 = loader.getParent();
System.out.println(loader1);
ClassLoader loader2 = loader1.getParent();
System.out.println(loader2);
}
}

输出

sun.misc.Launcher$AppClassLoader@dad5dc
sun.misc.Launcher$ExtClassLoader@16d3586
null

当为根加载器时,返回null

看了文档,写一个自定义 ClassLoader

/**
* @Author: cuzz
* @Date: 2019/1/28 12:39
* @Description:
*/
public class MyClassLoader extends ClassLoader{
private String classLoaderName;
private final String fileExtension = ".class";

public MyClassLoader(String classLoaderName) {
super(); // 将系统类加载当做该类加载器的父加载器
this.classLoaderName = classLoaderName;
}

public MyClassLoader(ClassLoader parent, String classLoaderName) {
super(parent); // 显示指定该类加载的父加载器
this.classLoaderName = classLoaderName;
}

@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] data = loadClassData(className);
return defineClass(className, data,0, data.length);
}

private byte[] loadClassData(String name) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = null;

try {
this.classLoaderName = this.classLoaderName.replace(".", "/");
is = new FileInputStream(new File(name, this.fileExtension));
baos = new ByteArrayOutputStream();

int ch;
while (-1 != (ch = is.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return data;
}

public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("myLoader");
Class<?> clazz = myClassLoader.loadClass("com.cuzz.jvm.classloader.MyTest01");
Object o = clazz.newInstance(); // 获取实例对象
System.out.println("类加载器:" + clazz.getClassLoader());
System.out.println(o);
}
}

输出

类加载器:sun.misc.Launcher$AppClassLoader@dad5dc
com.cuzz.jvm.classloader.MyTest01@16d3586

我们编写的类加载器不起作用,因为双亲委派机制,当我们尝试使用自己编写的类加载器去加载时,它会委派自己的双亲去加载,刚好系统类加载器(应用类加载器)就能加载,所以不会使用我们自己编写的类加载器,而使用系统类加载器

如果我们把路径换一下,把项目路径下 classes 中的 MyTest01.class 文件移动在别的地方,让系统类加载器找不到,然后它就会调用我们自己编写的类加载器加载

public class MyClassLoader extends ClassLoader{
private String classLoaderName;
private final String fileExtension = ".class";
private String path;

public MyClassLoader(String classLoaderName) {
super(); // 将系统类加载当做该类加载器的父加载器
this.classLoaderName = classLoaderName;
}

public MyClassLoader(ClassLoader parent, String classLoaderName) {
super(parent); // 显示指定该类加载的父加载器
this.classLoaderName = classLoaderName;
}

public void setPath(String path) {
this.path = path;
}

@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] data = loadClassData(className);
return defineClass(className, data,0, data.length);
}

private byte[] loadClassData(String name) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = null;

name = name.replace(".", "\\");
try {
is = new FileInputStream(new File(this.path + name + this.fileExtension));
baos = new ByteArrayOutputStream();

int ch;
while (-1 != (ch = is.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return data;
}

public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("myLoader");
// Class<?> clazz = myClassLoader.loadClass("com.cuzz.jvm.classloader.MyTest01");
String path = "C:/Users/my/Desktop/";
myClassLoader.setPath(path);
Class<?> clazz = myClassLoader.loadClass("com.cuzz.jvm.classloader.MyTest01");
Object o = clazz.newInstance(); // 获取实例对象
System.out.println("类加载器:" + clazz.getClassLoader());
System.out.println("父类加载器:" + myClassLoader.getParent());
System.out.println(o);
}
}

输出

类加载器:com.cuzz.jvm.classloader.MyClassLoader@16d3586
父类加载器:sun.misc.Launcher$AppClassLoader@dad5dc
com.cuzz.jvm.classloader.MyTest01@a14482

defineClas

java.lang.ClassLoader#defineClass(java.lang.String, byte[], int, int)

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

通过一个字节数组返回一个 Class 的实例

loadClass

java.lang.ClassLoader#loadClass(java.lang.String, boolean)

文档:

java.lang.ClassLoader
protected Class<?> loadClass(String name,
boolean resolve)
throws ClassNotFoundException
Loads the class with the specified binary name. The default implementation of this method searches for classes in the following order:

  1. Invoke findLoadedClass(String) to check if the class has already been loaded.
  2. Invoke the loadClass method on the parent class loader. If the parent is null the class loader built-in to the virtual machine is used, instead.
  3. Invoke the findClass(String) method to find the class.

If the class was found using the above steps, and the resolve flag is true, this method will then invoke the resolveClass(Class) method on the resulting Class object.
Subclasses of ClassLoader are encouraged to override findClass(String), rather than this method.
Unless overridden, this method synchronizes on the result of getClassLoadingLock method during the entire class loading process.
Parameters:

  • name - The binary name of the class
  • resolve - If true then resolve the class
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;
}
}

命名空间

每一个类加载器斗鱼自己的命名空间,命名空间由该加载器及所有父类加载器所加载的类组成,在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类,在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("myLoader");
MyClassLoader myClassLoader1 = new MyClassLoader("myLoader1");
String path = "C:/Users/my/Desktop/";
myClassLoader.setPath(path);
myClassLoader1.setPath(path);
Class<?> clazz = myClassLoader.loadClass("com.cuzz.jvm.classloader.MyTest01");
Class<?> clazz1 = myClassLoader1.loadClass("com.cuzz.jvm.classloader.MyTest01");
System.out.println("clazz: " + clazz.hashCode());
System.out.println("clazz1: " + clazz1.hashCode());
}

输出

clazz: 24324022
clazz1: 21685669

说明类被加载了两次,这就是由不同的命名空间导致的

如果我们给 myClassLoader1 添加一个父加载器

public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("myLoader");
// 把 myClassLoader 当做父加载器
MyClassLoader myClassLoader1 = new MyClassLoader(myClassLoader,"myLoader1");
String path = "C:/Users/my/Desktop/";
myClassLoader.setPath(path);
myClassLoader1.setPath(path);
Class<?> clazz = myClassLoader.loadClass("com.cuzz.jvm.classloader.MyTest01");
Class<?> clazz1 = myClassLoader1.loadClass("com.cuzz.jvm.classloader.MyTest01");
System.out.println("clazz: " + clazz.hashCode());
System.out.println("clazz1: " + clazz1.hashCode());
}

输出

clazz: 10568834
clazz1: 10568834

由于父加载器已经加载过了,所以就不会加载了

类的卸载

由 Java 虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面已经介绍过,Java 虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java 虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载类的 Class 对象,因此这些 Class 对象始终是可触及的

而由用户自定义的类加载器所加载的类是可以被卸载的

类加载器命名空间深度解析

通过一个例子来分析

MyCat

public class MyCat {
public MyCat() {
System.out.println("MyCat is loaded by: " + this.getClass().getClassLoader());
}
}

MySample

public class MySample {
public MySample () {
System.out.println("MySample is loaded by:" + this.getClass().getClassLoader());
new MyCat ();
}
}

MyClassLoader

public class MyClassLoader extends ClassLoader{
private String classLoaderName;
private final String fileExtension = ".class";
private String path;

public MyClassLoader(String classLoaderName) {
super(); // 将系统类加载当做该类加载器的父加载器
this.classLoaderName = classLoaderName;
}

public MyClassLoader(ClassLoader parent, String classLoaderName) {
super(parent); // 显示指定该类加载的父加载器
this.classLoaderName = classLoaderName;
}

public void setPath(String path) {
this.path = path;
}

@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] data = loadClassData(className);
return defineClass(className, data,0, data.length);
}

private byte[] loadClassData(String name) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = null;

name = name.replace(".", "\\");
try {
is = new FileInputStream(new File(this.path + name + this.fileExtension));
baos = new ByteArrayOutputStream();

int ch;
while (-1 != (ch = is.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return data;
}

public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("myLoader");
String path = "C:/Users/my/Desktop/";
myClassLoader.setPath(path);
Class<?> clazz = myClassLoader.loadClass("com.cuzz.jvm.classloader.MySample");
Object object = clazz.newInstance();
}
}

输出

MySample is loaded by: sun.misc.Launcher$AppClassLoader@dad5dc
MyCat is loaded by: sun.misc.Launcher$AppClassLoader@dad5dc

我们知道我们自己写的 ClassLoader 与委托父类加载器去加载,所以是系统加载器加载的

现在我们把项目下 classes 路径中的 MySample.class 和 MyCat.class 删除,并复制一份到桌面

则输出

MySample is loaded by: com.cuzz.jvm.classloader.MyClassLoader@16d3586
MyCat is loaded by: com.cuzz.jvm.classloader.MyClassLoader@16d3586

由于委托父类加载器加载不到就用自己加载器加载

如果我们只把当前类路径下 MySample.class 这给文件删掉,保留 MyCat.class 文件,则输出

MySample is loaded by: com.cuzz.jvm.classloader.MyClassLoader@16d3586
MyCat is loaded by: sun.misc.Launcher$AppClassLoader@dad5dc

我们知道 MySample 是我们自定义类加载加载出来的,MyCat 是有系统类加载加载的

public class MySample {
public MySample () {
System.out.println("MySample is loaded by: " + this.getClass().getClassLoader());
new MyCat ();
System.out.println(MyCat.class);
}
}

输出

MySample is loaded by: com.cuzz.jvm.classloader.MyClassLoader@16d3586
MyCat is loaded by: sun.misc.Launcher$AppClassLoader@dad5dc
class com.cuzz.jvm.classloader.MyCat

说明自定义类加载加载的类,可以访问系统类加载加载的类

如果我们在系统类加载的类中访问自定义类加载器加载的类

public class MyCat {
public MyCat() {
System.out.println("MyCat is loaded by: " + this.getClass().getClassLoader());
System.out.println(MySample.class);
}
}

输出

MySample is loaded by: com.cuzz.jvm.classloader.MyClassLoader@16d3586
MyCat is loaded by: sun.misc.Launcher$AppClassLoader@dad5dc
Exception in thread "main" java.lang.NoClassDefFoundError: com/cuzz/jvm/classloader/MySample
at com.cuzz.jvm.classloader.MyCat.<init>(MyCat.java:6)
at com.cuzz.jvm.classloader.MySample.<init>(MySample.java:6)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at java.lang.Class.newInstance(Class.java:442)
at com.cuzz.jvm.classloader.MyClassLoader.main(MyClassLoader.java:68)
Caused by: java.lang.ClassNotFoundException: com.cuzz.jvm.classloader.MySample
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 8 more

报错,说明系统加载器加载的类不能访问自定义加载器加载的类

说明当我现在加载 MySample 这个类时,使用的是我们自己定义的类加载器,然后初始实例化这个类时,需要初始化 MyCat 这个类,所以会先委托父加载器(系统加载器)去加载

但是如果我们把当前路径下的 MyCat.class 文件删掉,保留 MySample.class 文件,则报错

Exception in thread "main" MySample is loaded by: sun.misc.Launcher$AppClassLoader@dad5dc
java.lang.NoClassDefFoundError: com/cuzz/jvm/classloader/MyCat
at com.cuzz.jvm.classloader.MySample.<init>(MySample.java:6)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at java.lang.Class.newInstance(Class.java:442)
at com.cuzz.jvm.classloader.MyClassLoader.main(MyClassLoader.java:68)
Caused by: java.lang.ClassNotFoundException: com.cuzz.jvm.classloader.MyCat
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 7 more

我要加载 MySample 先委托系统类加载加载,发现能加载到,然后再想加载 MyCat 这个类,此时它会调用系统加载器的父类去加载,发现加载不到,自己也不能加载,就报错了。

通过上面的例子,我们可以得出以下结论:

  1. 子类加载器所加载的类能够访问到父加载器所加载的类
  2. 父类加载器所加载的类无法访问到子加载器所加载的类

类加载器的双亲委托模型的好处

可以确保 Java 核心库的类型安全:所有的 Java 应用都至少会引用 java.lang.Object 类,也就是说在运行期,java.lang.Object 这个类会被加载到 Java 虚拟机中;如果这个加载过程是由 Java 应用自己的类加载所完成的,那么很可能就会在 JVM 中存在多个版本的 java.lang.Object 了,而这些类之间还是不兼容的,相互不可见(正是命名空间发挥着作用)。可以确保 Java 核心类库所提供的类不会被自定义的类所取代。不同的类加载器可以为相同的名称(binary name)的类创建额外的命名空间。相同的名称的类可以并存在 Java 虚拟机中,只要用不同的类加载器来加载它们即可(可是是不同的类加载器,也可以是相同类加载器的不同实例)。不同的类加载器所加载的类之间是不兼容的,就相同于在 Java 虚拟机内部创建了一个又一个相互隔离的 Java 类空间,这类技术在很多框架中都得到了实际的应用。

内建于 JVM 中的启动类加载器会加载 java.lang.ClassLoader 以及其他的 Java 平台类,当 JVM 启动时,一块特殊的机器码会运行,它会加载扩展类加载器和系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstrap),启动类加载器并不是 Java 类,而其它加载器则都是 Java 类,启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。启动类加载器还会负责加载 JRE 正常运行所需要的基本组件,这包括 java.util 与 java.lang 包中的类等等。

Launcher 类源码分析

前面我们分析类 ClassLoader,里面有一个静态方法 getSystemClassLoader,发现 ClassLoader 是 Launcher 中一个成员变量

 @CallerSensitive
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader(); // 初始化
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}

private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
// 获取一个 Launcher 类
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// scl 表示 SystemClassLoader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
...
}
}
}
}

我们在idea里边看到的 sun.misc.Launcher.getLauncher() 的实现是反编译工具给出的,oracle并没有给出源码,可以到网上查找相关代码

private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");

public static Launcher getLauncher() {
return launcher;
}

private ClassLoader loader;

public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader");
}

// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader");
}

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);

// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
......
}

/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}

可以看到 Launcher 类初始化时,先初始化了个 ExtClassLoader,然后又初始化了个 AppClassLoader,然后把ExtClassLoader 作为 AppClassLoader的父 loader,ExtClassLoader 没有指定父类,即表明,父类是BootstrapClassLoader。把初始化 的AppClassLoader 作为全局变量保存起来,并设置到当前线程contextClassLoader,每个线程实例可以设置一个 contextClassLoader 。

先回到 initSystemClassLoader 方法中,有这一段代码

try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}

我们把系统加载传入到 doPrivileged 中的 SystemClassLoaderAction 中又返回了系统加载器,我们看看 SystemClassLoaderAction 这个类

class SystemClassLoaderAction
implements PrivilegedExceptionAction<ClassLoader> {
private ClassLoader parent;

SystemClassLoaderAction(ClassLoader parent) {
this.parent = parent;
}

public ClassLoader run() throws Exception {
String cls = System.getProperty("java.system.class.loader");
if (cls == null) {
return parent;
}

Constructor<?> ctor = Class.forName(cls, true, parent)
.getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
ClassLoader sys = (ClassLoader) ctor.newInstance(
new Object[] { parent });
Thread.currentThread().setContextClassLoader(sys);
return sys;
}
}

这块逻辑的作用是看看是否设置了系统属性 java.system.class.loader,即自定义的系统类加载器,如果设置了那么实例化自定义的系统类加载器返回,替代之前获取的系统类加载器,如果没有设置直接返回默认的系统类加载器。

Class.forName()

java.lang.Class#forName(java.lang.String, boolean, java.lang.ClassLoader)

文档:

java.lang.Class
public static Class<?> forName(@NonNls String name,
boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
Returns the Class object associated with the class or interface with the given string name, using the given class loader. Given the fully qualified name for a class or interface (in the same format returned by getName) this method attempts to locate, load, and link the class or interface. The specified class loader is used to load the class or interface. If the parameter loader is null, the class is loaded through the bootstrap class loader. The class is initialized only if the initialize parameter is true and if it has not been initialized earlier.
If name denotes a primitive type or void, an attempt will be made to locate a user-defined class in the unnamed package whose name is name. Therefore, this method cannot be used to obtain any of the Class objects representing primitive types or void.
If name denotes an array class, the component type of the array class is loaded but not initialized.
For example, in an instance method the expression:
Class.forName(“Foo”)
is equivalent to:
Class.forName(“Foo”, true, this.getClass().getClassLoader())
Note that this method throws errors related to loading, linking or initializing as specified in Sections 12.2, 12.3 and 12.4 of The Java Language Specification. Note that this method does not check whether the requested class is accessible to its caller.
If the loader is null, and a security manager is present, and the caller’s class loader is not null, then this method calls the security manager’s checkPermission method with a RuntimePermission(“getClassLoader”) permission to ensure it’s ok to access the bootstrap class loader.
Parameters:

  • name - fully qualified name of the desired class
  • initialize - if true the class will be initialized. See Section 12.4 of The Java Language Specification.
  • loader - class loader from which the class must be loaded

代码:

public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
// 获取调用 forName 方法的的那个类
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}

线程上下文类加载器分析与实现

接下来我们来分析一下线程上下文类加载的作用

前言

看一个程序来一下感性的认识:

public class MyTest24 {
public static void main(String[] args)
System.out.println(Thread.currentThread().getContextClassLoader());
System.out.println(Thread.class.getClassLoader());
}
}

这个程序的输出是:

sun.misc.Launcher$AppClassLoader@18b4aac2
null

解析:
第一行当前的线程是运行MyTest24 的线程,而MyTest24 是由系统类加载器加载,所以打印的是系统类加载器
第二行Thread类是java核心库的类,是由启动类加载器加载,所以不打印 null

**当前类加载器(Current ClassLoader) **

每个类都会使用自己的类加载器(即加载自身的类加载器) 来去加载其他类(指的是所依赖的类) ,如果ClassA引用了ClassY,那么ClassX的类加载器就会加载ClassY(前提是ClassY尚未被加载)

线程上下文加载器(Context ClassLoader)
线程上下文类加载器是从jdk1.2开始引入的,类Thread中的 getContextCLassLoader() 与setContextClassLoader(ClassLoader classloader) 分别用来获取和设置上下文类加载器,如果没有通过与setContextClassLoader(ClassLoader classloader)进行设置的话,线程将继承其父线程的上下文类加载器。

Java应用运行时的初始线程的上下文加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源。

我们在使用jdbc的时候,不同的数据库的驱动都是由每个厂商自己去实现,开发者在使用的时候,只需要把驱动jar包 ,放到当前path下边就可以了,这些驱动是由系统类加载器加载,而 java.sql 下边的一些Class在使用的时候不可避免的 ,要去使用厂商自定义的实现的逻辑,但是这些 java.sql 下的类的加载器是由启动类加载器完成的加载,由于父加载器(启动类加载器)加载的类无法访问子加载器(系统类加载器或者应用类加载器)加载的类,所以就无法在有些 java.sql 的类去访问具体的厂商实现,这个是双亲委托模型尴尬的一个局面。

线程上下文加载器的重要性:

SPI (Service Provider Interface)

父 ClassLoader 可以使用当前线程 Thread.currentThread().getContextClassLoader() 所指定的 classloader 加载的类。 这就改变了父 ClassLoader 不能使用子 ClassLoader 或是其他没有直接父子关系的 CLassLoader 加载的类的情况,即改变了双亲委托模型。

线程上下文加载器就是当前线程的 Current ClassLoader 在双亲委托模型下,类加载器由下至上的,即下层的类加载器会委托上层进行加载。但是对于 SPI 来说,有些接口是 java 核心库所提供的,而java核心库是由启动类加器来加载的,而这些接口的实现来自于不同的jar包(厂商提供),java 的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求,而通过给当前线程设置上下文加载器,就可以设置上下文类加载器来实现对于接口实现类的加载。

线程上下文的一般使用模式

线程上下文的一般使用模式分为3步,获取、使用和还原,下面是伪代码

// 获取
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
// 使用
Thread.currentThread().setContextClassLoader(targetClassLoader);
method();
} finally {
// 还原
Thread.currentThread().setContextClassLoader(classLoader);
}

method 里面调用了 Thread.currentThread().getContextClassLoader(),获取当前线程上下文类加载器做某些事情。如果一个类由类加载器 A 加载,那么这个类的依赖也是有相同的类加载器加载的(如果该依赖类之前没有加载过的话),ContextClassLoader 的作用就是为了破坏 Java 的类加载委托机制。

当高层提供了统一的接口让底层去实现,同时又要在高层加载(或实例化)底层类时,就必须要通过线程上下文类加载器来帮助高层的 ClassLoader 找到并加载该类。

ServiceLoader

我们先引入驱动依赖

group 'com.cuzz.jvm'
version '1.0'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
mavenCentral()
}

dependencies {
compile (
"mysql:mysql-connector-java:5.1.34"
)
}

我们先来看一个例子

/**
* @Author: cuzz
* @Date: 2019/2/1 14:46
* @Description:
*/
public class MyTest26 {
public static void main(String[] args) {
ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = serviceLoader.iterator();

while(iterator.hasNext()){
Driver driver = iterator.next();
System.out.println("driver: "+driver.getClass() + "loader: "+ driver.getClass().getClassLoader() );
}

System.out.println("当前线程上下文类加载器: " + Thread.currentThread().getContextClassLoader());

System.out.println("ServiceLoader的类加载器: "+ServiceLoader.class.getClassLoader());

}
}

输出

driver: class com.mysql.jdbc.Driverloader: sun.misc.Launcher$AppClassLoader@dad5dc
driver: class com.mysql.fabric.jdbc.FabricMySQLDriverloader: sun.misc.Launcher$AppClassLoader@dad5dc
当前线程上下文类加载器: sun.misc.Launcher$AppClassLoader@dad5dc
ServiceLoader的类加载器: null

我们可以看到 ServiceLoader 找到了 mysql 的两个驱动,这两个驱动都是由系统类加载器加载的,当前线程的上下文加载器默认也是系统类加载器,ServiceLoader是由启动类加载器加载,但是程序是怎样找到 mysql 的两个驱动的呢?我们没有在程序里边设置任何的属性或者路径之类的东西让程序能找到 mysql 的驱动,那么我们只能研究一下 ServiceLoader 的源码和文档看一下他们的原理:

public final class ServiceLoader<S>
extends Object
implements Iterable<S>

A simple service-provider loading facility.
A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers can be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers can also be made available by adding them to the application’s class path or by some other platform-specific means.
For the purpose of loading, a service is represented by a single type, that is, a single interface or abstract class. (A concrete class can be used, but this is not recommended.) A provider of a given service contains one or more concrete classes that extend this service type with data and code specific to the provider. The provider class is typically not the entire provider itself but rather a proxy which contains enough information to decide whether the provider is able to satisfy a particular request together with code that can create the actual provider on demand. The details of provider classes tend to be highly service-specific; no single class or interface could possibly unify them, so no such type is defined here. The only requirement enforced by this facility is that provider classes must have a zero-argument constructor so that they can be instantiated during loading.
A service provider is identified by placing a provider-configuration file in the resource directory META-INF/services. The file’s name is the fully-qualified binary name of the service’s type. The file contains a list of fully-qualified binary names of concrete provider classes, one per line. Space and tab characters surrounding each name, as well as blank lines, are ignored. The comment character is ‘#’ (‘\u0023’, NUMBER SIGN); on each line all characters following the first comment character are ignored. The file must be encoded in UTF-8.
If a particular concrete provider class is named in more than one configuration file, or is named in the same configuration file more than once, then the duplicates are ignored. The configuration file naming a particular provider need not be in the same jar file or other distribution unit as the provider itself. The provider must be accessible from the same class loader that was initially queried to locate the configuration file; note that this is not necessarily the class loader from which the file was actually loaded.
Providers are located and instantiated lazily, that is, on demand. A service loader maintains a cache of the providers that have been loaded so far. Each invocation of the iterator method returns an iterator that first yields all of the elements of the cache, in instantiation order, and then lazily locates and instantiates any remaining providers, adding each one to the cache in turn. The cache can be cleared via the reload method.
Service loaders always execute in the security context of the caller. Trusted system code should typically invoke the methods in this class, and the methods of the iterators which they return, from within a privileged security context.
Instances of this class are not safe for use by multiple concurrent threads.
Unless otherwise specified, passing a null argument to any method in this class will cause a NullPointerException to be thrown.
Example Suppose we have a service type com.example.CodecSet which is intended to represent sets of encoder/decoder pairs for some protocol. In this case it is an abstract class with two abstract methods:
public abstract Encoder getEncoder(String encodingName);
public abstract Decoder getDecoder(String encodingName);
Each method returns an appropriate object or null if the provider does not support the given encoding. Typical providers support more than one encoding.
If com.example.impl.StandardCodecs is an implementation of the CodecSet service then its jar file also contains a file named
META-INF/services/com.example.CodecSet
This file contains the single line:
com.example.impl.StandardCodecs # Standard codecs
The CodecSet class creates and saves a single service instance at initialization:`

private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);

To locate an encoder for a given encoding name it defines a static factory method which iterates through the known and available providers, returning only when it has located a suitable encoder or has run out of providers.

public static Encoder getEncoder(String encodingName) {
for (CodecSet cp : codecSetLoader)
Encoder enc = cp.getEncoder(encodingName);
if (enc != null)
return enc;
}
return null;
}

A getDecoder method is defined similarly.
Usage Note If the class path of a class loader that is used for provider loading includes remote network URLs then those URLs will be dereferenced in the process of searching for provider-configuration files.
This activity is normal, although it may cause puzzling entries to be created in web-server logs. If a web server is not configured correctly, however, then this activity may cause the provider-loading algorithm to fail spuriously.
A web server should return an HTTP 404 (Not Found) response when a requested resource does not exist. Sometimes, however, web servers are erroneously configured to return an HTTP 200 (OK) response along with a helpful HTML error page in such cases. This will cause a ServiceConfigurationError to be thrown when this class attempts to parse the HTML page as a provider-configuration file. The best solution to this problem is to fix the misconfigured web server to return the correct response code (HTTP 404) along with the HTML error page.

我们先看源码

public final class ServiceLoader<S> implements Iterable<S> {
// 前缀
private static final String PREFIX = "META-INF/services/";

// The class or interface representing the service being loaded
private final Class<S> service;

// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// The current lazy-lookup iterator
private LazyIterator lookupIterator;
...
}

该类中有个常量 PREFIX ,根据文档我们可以知道这是一个目录,我们看看 mysql-connnector-java 中也有

其下的文件名字就是服务的名字,比如数据库驱动的服务是java.sql.Drive,我们在mysql的jar包下可以看到这个文件,文件里边的内容是具体的实现类的全限定名:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

与前面打印出来的驱动是一样的

ServiceLoader 是由启动类加载器加载的,为什么 mysql 的驱动是由系统类加载器加载呢?

前面代码中 ServiceLoader serviceLoader = ServiceLoader.load(Driver.class); 这段代码是怎么起作用的呢,跟进源码

public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前上下文类加载,并使用上下文类加载器去加载
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
// 调用一个构造方法
return new ServiceLoader<>(service, loader);
}

既然 ServiceLoader 是由启动类加载器加载,那么 ServiceLoader 里边的类都会用启动类加载器去加载,但是呢我们的 mysql 驱动不在启动类加载器加载的目录下边,我们的 mysql 驱动位于 classpath 下边,无法用启动类加载器加载,这个时候,我们可以看到 load 方法使用了线程上下文加载器,线程上下文加载器默认是系统类加载器

我们来看看这个构造方法

   private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
// 调用reload() 方法
public void reload() {
// 清空缓存 providers = new LinkedHashMap<>();
providers.clear();
// 懒加载
lookupIterator = new LazyIterator(service, loader);
}

LazyIterator 类

java.util.ServiceLoader.LazyIterator

private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}

private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}

public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

public void remove() {
throw new UnsupportedOperationException();
}
}

这样就把驱动加载出来了,则前面代码输出

driver: class com.mysql.jdbc.Driverloader: sun.misc.Launcher$AppClassLoader@dad5dc
driver: class com.mysql.fabric.jdbc.FabricMySQLDriverloader: sun.misc.Launcher$AppClassLoader@dad5dc
当前线程上下文类加载器: sun.misc.Launcher$AppClassLoader@dad5dc
ServiceLoader的类加载器: null

如果我们把前面代码改一下,设置当前线程的上下文类加载器为扩展类加载器

public class MyTest26 {
public static void main(String[] args) {
// 把当前线程设置为扩展类加载器
Thread.currentThread().setContextClassLoader(MyTest26.class.getClassLoader().getParent());
ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = serviceLoader.iterator();

while(iterator.hasNext()){
Driver driver = iterator.next();
System.out.println("driver: "+driver.getClass() + "loader: "+ driver.getClass().getClassLoader() );
}

System.out.println("当前线程上下文类加载器: " + Thread.currentThread().getContextClassLoader());

System.out.println("ServiceLoader的类加载器: "+ServiceLoader.class.getClassLoader());
}
}

则输出

当前线程上下文类加载器: sun.misc.Launcher$ExtClassLoader@a14482
ServiceLoader的类加载器: null

可以看到循环没有去执行,上下文类加载器是扩展类加载器没啥问题,因为系统类加载器的上级是扩展类加载器,但是为什么循环是空的呢?原因就是扩展类加载器无法加载 classpath下边的类,mysql 的 jar 包是放在 classpath下边的。

Comments