面试官:谈谈类加载器吧,你有没有看过类加载器的源码?

简介: ​Java类加载器是Java运行时环境的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。学习类加载器时,掌握Java的委派概念很重要。

一、类加载

1.1、在java代码中,类型的加载,连接,初始化过程都是在程序运行期间完成的。

图示:

1.2、类型的加载——这里的类型是指的什么?

答:类型就是指的我们Java源代码通过编译后的class文件。

1.3、类型的来源有哪些?

(1)本地磁盘

(2)网络下载,class文件

(3)war,jar下加载,class文件

(4)从专门的数据库中读取,class文件(少见)

(5)将java源文件动态编译成class文件

1)典型的就是动态代理,通过运行期生成class文件

2)我们的jsp会被转换成servlet,而我们的serlvet是一个java文件,会被编译成class文件

1.4、通过什么来进行加载?(类加载器)

1.5、类加载的分类以及各种加载职责以及层级结构

(1)系统级别

1)启动类加载器

2)扩展类加载器

3)系统类加载器(App类加载器)

(2)用户级别的

自定义类加载器(继承我们的ClassLoader)

(3)层级结构

二、类加载器加载我们的Class的时候遵循我们的双亲委派模型

在双亲委派机制中,各个加载器按照父子关系形成树型结构,除了根加载器以外,每一个加载器有且只有一个父加载器

1、源码分析:

1 protected Class<?> loadClass(String name, boolean resolve)
2  throws ClassNotFoundException
3 {
4  synchronized (getClassLoadingLock(name)) {
5  //检查当前的class对象是否被加载过,被加载过就返回
6  Class<?> c = findLoadedClass(name);
7  if (c == null) {
8  long t0 = System.nanoTime();
9  try {
10  //判断当前的classLoader是否有父类
11  //若存在父类
12  if (parent != null) {
13  //调用父类的loadClass
14  c = parent.loadClass(name, false);
15  } else {//不存在父类,表示当前的classLoader是extClassLoader
16  //那么就会调用启动类判断是否加载过
17  c = findBootstrapClassOrNull(name);
18  }
19  } catch (ClassNotFoundException e) {
20  // ClassNotFoundException thrown if class not found
21  // from the non‐null parent class loader
22  }
23  //到目标位置,app ext boot都没有去加载过
24  if (c == null) {
25  // If still not found, then invoke findClass in order
26  // to find the class.
27  long t1 = System.nanoTime();
28  //委托我们的子类的classLoader去找
29  c = findClass(name);
30
31  // this is the defining class loader; record the stats
32  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 ‐ t0);
33  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
34  sun.misc.PerfCounter.getFindClasses().increment();
35  }
36  }
37  if (resolve) {
38  resolveClass(c);
39  }
40  return c;
41  }
42 }

2、双亲委派模型加载的流程图

i

3、类加载器的双亲委派模型的好处:

总所周知:java.lang.object类是所有类的父类,所以我们程序在运行期间会把java.lang.object类加载到内存中,假如java.lang.object类能够被我们自定义类加载器去加载的话,那么jvm中就会存在多份Object的Class对象,而且这些Class对象是不兼容的。关注公种浩:程序员追风。回复 003 领取2020最新Java面试题手册(200多页PDF文档)

所以双亲委派模型可以保证java核心类库下的类型的安全。

借助双亲委派模型,我们java核心类库的类必须是由我们的启动类加载器加载的,这样可以确保我们核心类库只会在jvm中存在一份这就不会给自定义类加载器去加载我们核心类库的类。

根据我们的演示案例,一个class可以由多个类加载器去加载,同事可以在jvm内存中存在多个不同版本的Class对象,这些对象是不兼容的。

4、如何手写一个自定义类加载器(根据ClassLoader的doc文档)

(1)我们自定义类加载器必须要继承ClassLoader

(2)我们必须要findClass(String name)方法

1 /**
2  * Finds the class with the specified <a href="#name">binary name</a>.
3  * This method should be overridden by class loader implementations that
4  * follow the delegation model for loading classes, and will be invoked b
y
5  * the {@link #loadClass <tt>loadClass</tt>} method after checking the
6  * parent class loader for the requested class. The default implementatio
n
7  * throws a <tt>ClassNotFoundException</tt>.
8  *
9  * @param name
10  * The <a href="#name">binary name</a> of the class
11  *
12  * @return The resulting <tt>Class</tt> object
13  *
14  * @throws ClassNotFoundException
15  * If the class could not be found
16  *
17  * @since 1.2
18  */
19 protected Class<?> findClass(String name) throws ClassNotFoundException
{
20  throw new ClassNotFoundException(name);
21 }

(3)写方法loadClassData(String name)去读取我们的class数据(非必须)

1 /**
2  * 自定义的加载器
3  * Created by smlz on 2019/10/22.
4  */
5 public class TulingClassLoader extends ClassLoader {
6
7  private final static String fileSuffixExt = ".class";
8
9  private String classLoaderName;
10
11  private String loadPath;
12
13  public void setLoadPath(String loadPath) {
14  this.loadPath = loadPath;
15  }
16
17  public TulingClassLoader(ClassLoader parent, String classLoaderName) {
18  /**
19  * 指定当前类加载器的父类加载器
20  */
21  super(parent);
22  this.classLoaderName = classLoaderName;
23  }
24
25  public TulingClassLoader(String classLoaderName) {
26  /**
27  * 使用appClassLoader加载器 作为本类的加载器
28  */
29  super();
30  this.classLoaderName = classLoaderName;
31  }
32
33  public TulingClassLoader(ClassLoader classLoader) {
34  super(classLoader);
35  }
36
37  /**
38  * 方法实现说明:创建我们的class 的二进制名称
39  * @author:smlz
40  * @param name: 类的二进制名称
41  * @return:
42  * @exception:
43  * @date:2019/10/22 14:42
44  */
45  private byte[] loadClassData(String name) {
46  byte[] data = null;
47  ByteArrayOutputStream baos = null;
48  InputStream is = null;
49
50  try {
51  name = name.replace(".","\\");
52  String fileName = loadPath+name+fileSuffixExt;
53  File file = new File(fileName);
54  is = new FileInputStream(file);
55
56  baos = new ByteArrayOutputStream();
57  int ch;
58  while (‐1 != (ch = is.read())){
59  baos.write(ch);
60  }
61  data = baos.toByteArray();
62  }catch (Exception e) {
63  e.printStackTrace();
64  }finally {
65  try{
66  if(null != baos) {
67  baos.close();
68  }
69  if(null !=is) {
70  is.close();
71  }
72  }catch (Exception e) {
73  e.printStackTrace();
74  }
75  }
76
77  return data;
78  }
79
80  protected Class<?> findClass(String name) throws ClassNotFoundException
{
81  byte[] data = loadClassData(name);
82  System.out.println("TulingClassLoader 加载我们的类:===>"+name);
83  return defineClass(name,data,0,data.length);
84  }
85 }

(4)特别需要注意:我们自定义的类加载器默认情况下的父类加载器是我们的系统AppClassLoader

代码证据:

1 public TulingClassLoader(String classLoaderName) {
2  /**
3  * 使用appClassLoader加载器 作为本类的加载器
4  */
5  super();
6  this.classLoaderName = classLoaderName;
7 }
8
9 //调用super()的时候
10 protected ClassLoader() {
11  //在这里,把getSystemClassLoader()作为我们自定义类加载器的
12  //父亲
13  this(checkCreateClassLoader(), getSystemClassLoader());
14 }
15
16 private ClassLoader(Void unused, ClassLoader parent) {
17  this.parent = parent;
18  if (ParallelLoaders.isRegistered(this.getClass())) {
19  parallelLockMap = new ConcurrentHashMap<>();
20  package2certs = new ConcurrentHashMap<>();
21  domains =
22  Collections.synchronizedSet(new HashSet<ProtectionDomain>());
23  assertionLock = new Object();
24  } else {
25  // no finer‐grained lock; lock on the classloader instance
26  parallelLockMap = null;
27  package2certs = new Hashtable<>();
28  domains = new HashSet<>();
29  assertionLock = this;
30  }
31 }

5、怎么用实验证明我们的自定义类加载器的父加载器就是系统类加载器

(1)把我们的Person.class文件copy的指定的磁盘目录下。同时classpath下 存在我们的Person.class文件

1 /**
2  * 证明系统类加载器就是我们的自定义类加载器的父类
3  * Created by smlz on 2019/11/12.
4  */
5 public class AppClassLoaderIsCustomerClassLoaderParent {
6
7  public static void main(String[] args) throws ClassNotFoundException {
8  /**
9  * 执行test1()方法的时候打印结果式我们的系统类加载器去加载我们的
10  Person的,虽然我们是通过TulingClassLoader 去加载我们的Person.class
11  但是由于双亲委派模型会委托我们的AppClassLoader去我们的classes路面下去
12  加载Person.class由于我们的classes目录下存在我们的Person.class
13  所以我们的Person.class被我们的AppClassLoader去加载.
14
15
16
17  ===================================================
18  若我们把classpath下的Person.class给删除掉,那么我们的
19  TulingClassLoader尝试去加载我们的Person.class,由于双亲委派模型下会委托父类A
ppClassLoader
20  加载,但是我们人工把类路径下的Person.class给删除掉了后,那么我们的AppClassLo
ader加载不了
21  我们的Person.class,从而是由我们的TulingClassLoader去加载.
22  **/
23  test1();
24
25  }
26
27  //正常情况下,把我们的AppIsCustParentDemo放到D:\\smlz的目录下
28  public static void test1() throws ClassNotFoundException {
29
30  TulingClassLoader tulingClassLoader = new TulingClassLoader("tulingClas
sLoader");
31  //设置加载路径
32  tulingClassLoader.setLoadPath("D:\\smlz\\");
33  //通过自定义类加载器加载我们的AppIsCustParentDemo
34  Class<?> targetClass = tulingClassLoader.loadClass("com.tuling.smlz.jv
m.open.AppIsCustParentDemo");
35
36  System.out.println("targetClass 被class加载器加载..."+targetClass.getCla
ssLoader());
37
38  }
39 }

6、同一个Person.class文件 被我们的不同的类加载器去加载,那么我们的jvm内存种会生成二个对应的Person的Class对象,而且这二个对应的Class对象是相互不可见的(通过Class对象反射创建的实例对象相互是不能够兼容的不能相互转型) 这一点也很好的解释了

1 public class Person {
2
3  private Person person;
4
5  public void setPerson(Object person) {
6  this.person = (Person) person;
7  }
8 }
1 public class Demo {
2  //需要把我们的ClassPath下的Person.class给删除
3  public static void main(String[] args) throws ClassNotFoundException, Il
legalAccessException, InstantiationException, NoSuchMethodException, Invoca
tionTargetException {
4
5  TulingClassLoader classLoader1 = new TulingClassLoader("tulingClassLoade
r1");
6  classLoader1.setLoadPath("D:\\smlz\\");
7
8  TulingClassLoader classLoader2 = new TulingClassLoader("tulingClassLoade
r2");
9  classLoader2.setLoadPath("D:\\smlz\\");
10
11  //通过classLoader1加载我们的Person
12  Class<?> class1 = classLoader1.loadClass
13
("com.tuling.smlz.jvm.open.TheSameClassLoadedByDiffClassLoader.Person");
14  System.out.println("class1的类加载器:‐>"+class1.getClassLoader());
15
16  Class<?> class2 = classLoader2.loadClass
17
("com.tuling.smlz.jvm.open.TheSameClassLoadedByDiffClassLoader.Person");
18  System.out.println("class2的类加载器:‐>"+class2.getClassLoader());
19
20  System.out.println("class1==class2:"+(class1==class2));
21
22  //模拟问题
23  Object person = class1.newInstance();
24
25  Object person2 = class2.newInstance();
26
27  Method method = class1.getMethod("setPerson",Object.class);
28  //会抛出类型转换错误
29  method.invoke(person,person2);
30  }
31 }

7、类加载器的全盘委托机制以及类加载器的命名空间

(1)类加载器的全盘委托机制:比如我们的Person类是由我们的AClassLoader进行加载的,那么我们Person引用的Dog类就会委托给我们的A ClassLoader进行加载

1 public class Person {
2
3  public Person() {
4  System.out.println("Dog类是由我们的类加载器:‐
>"+Dog.class.getClassLoader());
5  }
6 }
7
8 public class Dog {
9 }
10
11 public class MainTest {
12
13  public static void main(String[] args) {
14
15  Person person = new Person();
16  System.out.println("Person的classLoader:‐>"+person.getClass().getClassLo
ader());
17
18  }
19 }

(2)类加载器的命名空间

类加载器的命名空间 是有类加载器本身以及所有父加载器所加载出来的binary name(full class name)组成。

1)在同一个命名空间里,不允许出现二个完全一样的binary name。

2)在不同的命名空间种,可以出现二个相同的binary name。当时二 者对应的Class对象是相互不能感知到的,也就是说Class对象的类型是不一样的。关注公种浩:程序员追风。回复 003 领取2020最新Java面试题手册(200多页PDF文档)

3)子加载器的命名空间中的binary name对应的类中可以访问 父加 载器命名空间中binary name对应的类,反之不行

8、验证子加载器加载出来的类可以访问父加载器加载的类

测试环境:我们的Person是由我们的自定义类加载器(把classpath下的Person.class删除,并且把Person.class copy到磁盘文件上)TulingClassLoader进行加载的,Dog 是由我们的AppClassLoader进行加载的. 我们在Person中访问Dog。


1 public class Dog {
2 }
3
4 public class Person {
5
6  public Person() {
7  new Dog();
8  System.out.println("Dog的classLoader:‐‐>"+Dog.class.getClassLoader());
9  }
10
11 }
12
13 public class TestDemo {
14  public static void main(String[] args) throws ClassNotFoundException, I
llegalAccessException, InstantiationException {
15  TulingClassLoader classLoader = new TulingClassLoader("tulingClassLoade
r");
16  classLoader.setLoadPath("D:\\smlz\\");
17  Class<?> clazz = classLoader.loadClass("com.tuling.smlz.jvm.open.classl
oadernamespace.Person");
18  clazz.newInstance();
19
20  System.out.println("Person的类加载器:"+clazz.getClassLoader());
21  }
22 }

9、如何证明父加载加载的类不能访问子加载器加载的类

测试环境:把我们的Person.class放置在C:ProgramFilesJavajdk1.8.0_131jreclasses这个目录下,那么我们的Person.class就会被我们的启动类加载器加载,而我们的Dog类是被AppClassLoader进行加载,我们的Person类 中引用我们的Dog类会抛出异常。

1 public class Dog {
2 }
3
4 public class Person {
5
6  public Person() {
7  new Dog();
8  System.out.println("Dog的classLoader:‐‐>"+ Dog.class.getClassLoader());
9  }
10 }
11
12
13 public class TestDemo {
14
15  public static void main(String[] args) throws IllegalAccessException, I
nstantiationException {
16
17
18  System.out.println("Person的类加载器:"+Person.class.getClassLoader());
19
20  System.out.println("Dog的类加载器:"+Dog.class.getClassLoader());
21
22  Class<?> clazz = Person.class;
23  clazz.newInstance();
24
25  }
26 }
27
28 运行结果:
29  Person的类加载器:null
30 Dog的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
31 Exception in thread "main" java.lang.NoClassDefFoundError: com/tuling/sm
lz/jvm/open/ParentClassLoaderNotAccessSonClassLoader/Dog
32  at com.tuling.smlz.jvm.open.ParentClassLoaderNotAccessSonClassLoader.Pe
rson.<init>(Person.java:11)
33  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native
Method)
34  at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstruc
torAccessorImpl.java:62)
35  at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Delegating
ConstructorAccessorImpl.java:45)
36  at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
37  at java.lang.Class.newInstance(Class.java:442)
38  at com.tuling.smlz.jvm.open.ParentClassLoaderNotAccessSonClassLoader.Te
stDemo.main(TestDemo.java:16)

10、打破双亲委派模型之 线程上下文类加载器

场景:JDBC接口技术之SPI之应用。

类的首次主动使用会触发类的初始化。

1)调用静态方法

2)给静态变量赋值获取读取一个静态变量

3)反射 Class.forName

4)new 出一个对象

5)执行main方法的时候

6)初始化子类会初始化他的父类

相关文章
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
73 2
|
4月前
|
JavaScript 前端开发
【Vue面试题二十五】、你了解axios的原理吗?有看过它的源码吗?
这篇文章主要讨论了axios的使用、原理以及源码分析。 文章中首先回顾了axios的基本用法,包括发送请求、请求拦截器和响应拦截器的使用,以及如何取消请求。接着,作者实现了一个简易版的axios,包括构造函数、请求方法、拦截器的实现等。最后,文章对axios的源码进行了分析,包括目录结构、核心文件axios.js的内容,以及axios实例化过程中的配置合并、拦截器的使用等。
【Vue面试题二十五】、你了解axios的原理吗?有看过它的源码吗?
|
19天前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
39 2
|
4月前
|
JavaScript 前端开发
【Vue面试题二十七】、你了解axios的原理吗?有看过它的源码吗?
文章讨论了Vue项目目录结构的设计原则和实践,强调了项目结构清晰的重要性,提出了包括语义一致性、单一入口/出口、就近原则、公共文件的绝对路径引用等原则,并展示了单页面和多页面Vue项目的目录结构示例。
|
3月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
106 35
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
3月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
491 37
面试官: 请你手写一份 Call()源码,看完此篇不用担心!
面试官: 请你手写一份 Call()源码,看完此篇不用担心!
|
5月前
|
存储 安全 Java
Android面试题之ArrayList源码详解
ArrayList是Java中基于数组实现的列表,提供O(1)的索引访问,但插入和删除操作平均时间复杂度为O(n)。默认容量为10,当需要时会通过System.arraycopy扩容。允许存储null,非线程安全。面试常问:List是接口,ArrayList是其实现之一,推荐使用List接口编程以实现更好的灵活性。更多详情见[ArrayList源码](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/ArrayList.java#ArrayList.Node)。
36 2
|
4月前
|
存储 JavaScript 前端开发
JS浅拷贝及面试时手写源码
JS浅拷贝及面试时手写源码
|
6月前
|
存储 安全 Java
《ArrayList & HashMap 源码类基础面试题》面试官们最喜欢问的ArrayList & HashMap源码类初级问,你都会了?
《ArrayList & HashMap 源码类基础面试题》面试官们最喜欢问的ArrayList & HashMap源码类初级问,你都会了?
44 0