网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
系统类加载器(Application ClassLoader)
系统类加载器的父加载器是扩展类加载器,它是一种常见的类加载器,负责加载classpath下的类库资源,比如开发过程中引入的第三方jar包。
自定义类加载器(Custom ClassLoader)
除上面三种类加载器以外,还有自定义类加载器,它的默认父加载器是系统类加载器。下面给一个自定义类加载器的代码,如下
package com.hust.zhang.classloader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MyClassLoader extends ClassLoader {
private final static Path DEFAULT_CLASS_DIR = Paths.get("/Users/kaizhang/repository", "myClassLoader");
private Path classDir;
//默认类加载路径
public MyClassLoader() {
super();
this.classDir = DEFAULT_CLASS_DIR;
}
//允许传入指定路径的class路径
public MyClassLoader(String classDir) {
super();
this.classDir = Paths.get(classDir);
}
//指定class路径的同时,指定父类加载器
public MyClassLoader(String classDir, ClassLoader parent) {
super(parent);
this.classDir = Paths.get(classDir);
}
//重写父类的findClass方法(importance)
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//读取class的二进制数据
byte[] classBytes = this.readClassBytes(name);
//如果数据为null,或者没有读到任何信息,则抛出异常
if (null == classBytes || classBytes.length == 0) {
throw new ClassNotFoundException("Can not load the class " + name);
}
//调用defineClass方法定义class
return this.defineClass(name, classBytes, 0, classBytes.length);
}
//将class文件读入内存
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 found.");
}
//此处try()括号的代码会自动资源释放,前提是可关闭的接口实现java.lang.AutoCloseable接口,比如inputStream和outputStream。
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";
}
}
双亲委托机制
当一个类加载器被调用了loadClass之后,它并不会直接将其加载,而是先交给当前类加载器的父加载器尝试加载到最顶层的父加载器,然后依次向下加载。
父委托机制的逻辑主要是由loadClass来控制,有时候我们需要打破JDK提供的这种双亲委托的机制,打破双亲委托机制的方法有以下两种:
- 绕过系统类加载器,直接将扩展类加载器作为MyClassLoader的父加载器。
- 构造MyClassLoader的时候指定其父类加载器为null。
这里引申出来一个问题:获取类加载器方法getClassLoader()和获取上下文类加载器方法getContextClassLoader()有啥区别?可以看下面自己写的小栗子:
- 可以看到CompareClassLoader对象类的类加载器和主线程的上下文类加载器是同一对象。
- 子线程的类加载器也属于系统类加载器,它的父类加载器是扩展类加载器。
什么时候该用上下文类加载器?大家下面自己思考一下。
参考:https://iowiki.com/java/lang/thread_getcontextclassloader.html
public class CompareClassLoader {
public static void main(String[] args) {
//getClassLoader方法是当前类的加载器,它是使用双亲委派模型来加载类的
ClassLoader classLoader = CompareClassLoader.class.getClassLoader();
//getContextClassLoader是当前线程的类加载器,它是为了避开双亲委派模型的加载方式
ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader);
System.out.println(threadClassLoader);
//下面比较子线程与主线程的类加载器
new Thread(()->{
// returns the context ClassLoader for this Thread
ClassLoader c = Thread.currentThread().getContextClassLoader();
// sets the context ClassLoader for this Thread
Thread.currentThread().setContextClassLoader(c);
System.out.println("Class = " + c.getClass());
System.out.println("Parent = " + c.getParent());
}).start();
}
}
类加载器命名空间
每个类加载器中同一个class都是独一无二的。类加载器进行类加载时,首先会加载记录表也就是缓存中,查看该类是否已经被加载过,如果已经加载过就不会重复加载,否则将会认为其是首次加载,下面就是同一个class被不同类加载器加载后的内存情况,可以看到同一个类在不同类加载器都是首次加载。
运行时包
编写代码的时候通常会给一个类指定一个包名,包的作用是为了组织类,防止不同包下同样名称的class引起冲突,还能起到封装的作用,包名和类名构成了类的全限定名称。在JVM运行时class会有一个运行时包,运行时的包是由类加载器的命名空间和类的全限定名称共同组成的。这样做主要是处于安全和封装的考虑。
初始类加载器
JVM规范的规定,在类的加载过程中,所有参与的类加载器,即使没有亲自加载过该类,也都会被标识为该类的初始类加载器。
类的卸载
JVM的启动过程中,JVM会加载很多的类,JVM运行期间加载了多少class可以启动JVM时指定-verbose:class参数观察得到。我们知道某个对象在堆内存中如果没有其他地方引用则会在垃圾回收器线程进行GC的时候被回收掉,那么该对象在堆内存中的Class对象以及Class在方法区中的数据结构在满足以下三个条件时才会被GC回收:
- 该类所有的实例都已经被GC。
- 加载该类的ClassLoader实例被回收。
- 该类的class实例没有在其他地方被引用。
需要注意的一点:在加载JDK核心类库时,如果想要通过自定义类加载器绕过双亲委派机制去加载核心类库时,会抛出SecurityException异常,这是由于JVM源码ClassLoader在defineClass时做了安全性检查,源码如下,
/* Determine protection domain, and check that:
- not define java.* class,
- signer of this class matches signers for the rest of the classes in
package.
*/
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
![img](https://img-blog.csdnimg.cn/img_convert/9502f229afef8dd107714530964f2a73.png)
![img](https://img-blog.csdnimg.cn/img_convert/16734be7b545a76c336a32ab55a2d1e0.png)
![img](https://img-blog.csdnimg.cn/img_convert/a0b7d5d19860ad3c1b40534ad2b30ab7.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**