之前的博客中提到了类加载的过程,提到了双亲委托机制,提到了关于类加载器的概念,这篇博客就来给大家分享一下什么是JVM的类加载器。通过实战的方式来了解一下类加载器器到底是什么。
类加载器就是在类加载的过程中负责对于class文件进行加载的对象。也就是说通过这类加载器来确定每个类与JVM的唯一性关系。对于任何一个对象在JVM都是唯一存在的。
在JVM中类加载器主要分为三类,按照接近内存接近底层的顺序可以分为,Bootstrap ClassLoader,ExtClassLoader,ApplicationClassLoader,以及自定义类加载器。
对于这个类加载器来说,是作为比较底层的一个类加载器,这个类加载器几乎就是操作到内存层面上,主要是通过由C++语言来编写的。可以通过-Xbootclasspath参数来指定这个加载器器的路径。例如
public class BootstrapClassLoaderTest {
public static void main(String[] args) {
System.out.println("Bootstrap:"+String.class.getClassLoader());
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
在这里我们会发现第一个输出为空,第二个输出是我们类路径下的几个jar包的位置。而这些jar包就是我们操作BootstrapClassLoader的jar包。
正如上面提到的一样,扩展类加载器其实是BootStrapClassLoader的子类,也就是说到扩展类加载器开始就是使用Java语言来编写。那么首先我们就来看一下关于扩展类加载器的一些信息。首先ExtClassLoader是作为java.long.URLClassLoader的子类出现的可以看到他的全类名在下面结果中也有输出。通过系统属性java.ext.dirs来获取类加载器库的内容。
public class ExtClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> test = Class.forName("com.example.charp10.Test");
System.out.println(test.getClassLoader());
System.out.println(System.getProperty("java.ext.dirs"));
}
}
这个类加载器在Java中是比较常用的一个加载器,它的作用就是对classpath的类资源进行加载,按照上面的说法,系统类加载器的父类是扩展类加载器器,如果在项目中引入了第三方的jar包是通过双亲委派机制找到扩展类加载器进行加载,当然系统类加载器也是我们自定义类加载器的父类,它可以由系统参数java.class.path来进行获取。
public class ApplicationClassLoaderTest {
public static void main(String[] args) {
System.out.println(System.getProperty("java.class.path"));
System.out.println(ApplicationClassLoaderTest.class.getClassLoader());
}
}
首先我们要了解一下自定义类加载器为什么要实现自定义的类加载器,因为在有些场景下我们不希望我们写的代码可以被别人直接反编译之后直接使用,所以我们通过自定义类加载器的方式实现对于字节码的加密和解密操作。这样可以保证我们程序的安全性。那么怎么实现一个自定义的类加载器首先我们先来看一下ClassLoader类是什么情况
ClassLoader类
按照之前类加载器过程来说,首先第一步应该有一个获取类的字节码文件的过程,而这个过程需要调用一系列方法,在ClassLoader中就提供了这样一系列的方法由于方法太多也就不一个一个查看了。完成第一步加载到资源之后,第二步的操作就是连接初始化操作,也看到了在ClassLoader中提供了一些方法去实现初始化操作。
基于这样一个过程,我们按照之前类加载的过程来实现一个自己的类加载器。
public class MyClassLoader extends ClassLoader {
//设置默认的class文件存放路径target/classes/com/example/charp10
private final static Path DEFAULT_CLASS_DIR= Paths.get("target/classes/com/example/","charp10");
private final Path classDir;
public MyClassLoader() {
super();
this.classDir = DEFAULT_CLASS_DIR;
}
//允许通过参数传入类路径
public MyClassLoader(String classDir) {
super();
this.classDir = Paths.get(classDir);
}
public MyClassLoader(ClassLoader parent, Path classDir) {
super(parent);
this.classDir = 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 name) throws ClassNotFoundException {
String classPath = name.replace(".","/");
Path classFullPath = classDir.resolve(Paths.get(classPath+".class"));
if (!classFullPath.toFile().exists()){
throw new ClassNotFoundException("The class "+name+"not fund");
}
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
Files.copy(classFullPath,baos);
return baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("load the class "+name+" occur error.",e);
}
}
@Override
public String toString() {
return "My ClassLoader";
}
}
需要加载的类
public class Test {
static {
System.out.println("Test Class init");
}
public String sayHello(){
return "hello";
}
}
测试代码
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class<?> mClass = classLoader.loadClass("com.example.charp10.Test");
System.out.println(mClass.getClassLoader());
Object test = mClass.newInstance();
System.out.println(test);
Method sayHelloMethod = mClass.getMethod("sayHello");
String result = (String) sayHelloMethod.invoke(test);
System.out.println("Result:"+result);
}
}
测试结果
当然上面这只是一个简单的类加载器实现机制,还可重写很多的方法实现其他的特殊的功能的自定义类加载器,可以使用自定义的类加载器去加载一些关键的Class文件对其进行加密操作。
如上图,当一个类需要被加载器的时候首先调用的自定类加载器,如果没有自定义的类加载器就调用系统类加载器,调用系统类加载器之后,系统类加载器会委托调用扩展类加载器,扩展类委托根类加载器进行加载,加载完成之后,返回给扩展类加载器,扩展类在告诉系统类加载器加载成功,系统类通知自定义类加载器加载成功,整个过程算是加载成功,如果到中间没有找到对应的类就会加载失败。这个过程的调用是从我们ClassLoader的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);
} 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;
}
}
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
private native final Class<?> findLoadedClass0(String name);
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
我们可以看到整个的类加载过程是线程安全的,首先找到需要加载的类是否存在,如果存在就找对应的父类记载在方法进行加载。如果没有父类加载器,就直接调用根类加载器进行加载,如果都没有加载成功,则尝试使用findClass方法进行加载,而这个方法就是自定义类加载器重写的方法
由于loadCla指定resolve为false,所以不会进行连接阶段的继续,也就是解释了为什么通过类加载器加载的类并不会导致类的初始化,因为到链接阶段它已经停止了。
既然我们了解了关于类加载过程以及类加载器的过程,那么我们可以知道如果找到对应的class文件并将其替换掉就可以对Java程序进行破坏了。在实际工作中有时候就需要破坏这种机制,例如之前提到的对自己写的代码进行加密。
JDK提供的双亲委托机制并不是强制的执行,所以这就允许开发人员对这种机制进行破坏。当然这里的破坏并不是去做黑客,而是在这个基础上开发新功能。也就是常说的热部署,在不用停止应用的情况下对应用进行改变。但是对于JVM内置的三大类加载器我们是没有办法去改变的我们所能改变的就是我们自己实现的类加载器。我们之前实现的类加载器是对findClass方法进行了重写,但是我们知道真正控制整个类加载的是loadClass,所以要对于loadClass方法进行重写才会起作用。
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> klass = findLoadedClass(name);
if (klass == null) {
if (name.startsWith("java.") || name.startsWith("javax.")) {
try {
klass = getSystemClassLoader().loadClass(name);
} catch (Exception e) {
throw e;
}
} else {
try {
klass = this.findClass(name);
} catch (Exception e) {
e.printStackTrace();
}
if (klass == null) {
if (getParent() != null) {
klass = getParent().loadClass(name);
} else {
klass = getSystemClassLoader().loadClass(name);
}
}
}
}
if (null == klass) {
throw new ClassNotFoundException("The class " + name + " not found.");
}
if (resolve) {
resolveClass(klass);
}
return klass;
}
}
代码解释
还是之前的问题,既然我们可以破坏双亲委托机制那么我们可以不可以使用自定义的类加载器加载属于我们的String类的class文件呢?这个就是加载完成之后在连接时候为什么会出现验证准备然后才会进行解析操作的原因
1.类的名称空间
在每个类加载的时候都有属于自己的名称空间,这个名称空间是由父类加载器所构成的。也就是说每个class在加载的时候都是独一无二的存在。例如
public class NameSpace {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = NameSpace.class.getClassLoader();
Class<?> aClass = classLoader.loadClass("com.example.charp10.Test");
Class<?> bClass = classLoader.loadClass("com.example.charp10.Test");
System.out.println(aClass.hashCode());
System.out.println(bClass.hashCode());
System.out.println(aClass == bClass);
}
}
分析一下类加载完成之后的内存图
会发现无论使用多少次的类加载都会是同一份的Class对象,这也就是为什么每个类在加载完成之后只有一个并且是唯一的对象在内存中。
那么如果使用不同的类加载器去加载同一个实例,或者使用同一个类加载器加载不同实例那么在堆栈方法区中会产生多少个对象呢?
使用不同类加载器加载同一个class
public static void main(String[] args) throws Exception {
BrokerDelegateClassLoader brokerclassLoader = new BrokerDelegateClassLoader();
MyClassLoader classLoader = new MyClassLoader();
Class<?> mClass = classLoader.loadClass("com.example.charp10.Test");
Class<?> bClass = brokerclassLoader.loadClass("com.example.charp10.Test");
System.out.println(mClass.getClassLoader());
System.out.println(bClass.getClassLoader());
System.out.println(mClass.hashCode());
System.out.println(bClass.hashCode());
System.out.println(mClass==bClass);
}
public static void main(String[] args) throws Exception {
MyClassLoader aclassLoader = new MyClassLoader("target/classes/",null);
MyClassLoader bclassLoader = new MyClassLoader("target/classes/",null);
Class<?> aClass = aclassLoader.loadClass("com.example.charp10.Test");
Class<?> bClass = bclassLoader.loadClass("com.example.charp10.Test");
System.out.println(aClass.getClassLoader());
System.out.println(bClass.getClassLoader());
System.out.println(aClass.hashCode());
System.out.println(bClass.hashCode());
System.out.println(aClass==bClass);
}
分析源码可以知道,在类加载器进行加载过程中,首先加载的就是在缓存中的,如果该类在缓存中已经存在,就说明被加载过了,就不需要重新加载,否则就是第一次加载。如图,相同的对象被不同的类加载器加载之后的内存情况,会在内存中出现多个实例。这个就是应为在同一个Class实例在同一个类加载器的名称空间之下是唯一的。在不同的类加载器的名称空间下是不唯一的。
2.运行时包
在我们开发程序的时候都需要给类起一个包名,有了包名防止了在同一个包下的Class的冲突。还可以起到封装的作用。而我们知道Class的名称就是有包名加类名的全类名构成,这个也为我们提供了一个权限控制机制,也就是我们提到的public、producted、和private等权限修饰符
3.初始类加载器
在运行时环境下我们怎么知道哪些类有哪些访问权限呢,这个就需要使用到根类加载器,这个加载器可以加载任何的JDK包下的class。对于第三方的jar则是由我们的系统类加载器来加载。
根据JVM规范指出,在类加载过程中所有的类加载器包括自定义的类加载器,即使是没有加载过该类,也会被标记为该类的初始加载器。
4.类的卸载
在JVM启动的时候会有很多的类被加载,但是这些类被加载完成时候什么时候被卸载呢,我们知道类的卸载其实就是GC垃圾回收机制,如果在JVM内存中没有足够的空间则会被GC回收掉一部分类。那么这部分类在满足什么条件的时候被回收呢
遇见上面的情况就表示该类在JVM中已经被卸载了
这里主要说了关于Java虚拟机的类加载器,以及类加载过程,但是这些都是在单个线程下面执行的。没有涉及到多线程的操作。从下一篇文章开始就要开始在多线程下讨论问题了。通过上面对于单线程的类加载机制的了解,也深刻的理解了双亲委派机制。为后面使用自定义的类加载器打下基础。