竟然还有人没搞懂 JVM 类加载器?一文彻底明白

云计算 虚拟化
在Java面试中,在考察完项目经验、基础技术后,我会根据候选人的特点进行知识深度的考察,如果候选人简历上有写JVM(Java虚拟机)相关的东西,那么我常常会问一些JVM的问题。

[[280350]]

写在前面

在Java面试中,在考察完项目经验、基础技术后,我会根据候选人的特点进行知识深度的考察,如果候选人简历上有写JVM(Java虚拟机)相关的东西,那么我常常会问一些JVM的问题。JVM的类加载机制是一个很经典的知识点,围绕这个知识点可以有下面这些难度不同的问题。

  1. 简单讲下JVM中的类加载过程
  2. JVM中的类加载和卸载的时机?
  3. 如何理解JVM中不同类加载器的概念和作用?
  4. 简单讲下JVM中的双亲委派模型?
  5. 什么情况下会破坏双亲委派模型?为什么?可否举个例子?
  6. Tomcat中的类加载机制有了解吗?为什么这么设计?
  7. 实际开发中有遇到哪些类加载器相关的问题?你又是如何解决的?
  8. JVM之上的弱类型语言例如Groovy是如何实现?简单讲下动态类加载机制?

在接下来的几篇文章,我将跟读者一起重新梳理一遍类加载器的相关知识,争取能够妥善解答上面列出的这些问题。

基本概念篇

类的加载和卸载

JVM是虚拟机的一种,它的指令集语言是字节码,字节码构成的文件是class文件。平常我们写的Java文件,需要编译为class文件才能交给JVM运行。可以这么说:C语言代码——>二进制文件——>计算机硬件,就相当于Java代码——>字节码文件——>JVM。JVM将指定的class文件读取到内存里,并运行该class文件里的Java程序的过程,就称之为类的加载;反之,将某个class文件的运行时数据从JVM中移除的过程,就称之为类的卸载。

class文件的运行时数据就是C++对象,也称为kclass对象,这些运行时数据在JDK7之前是放在永久代(PermGen),JDK8之后则放在元空间(Metaspace)。

类的生命周期

Java类从被虚拟机加载开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段;其中验证、准备和解析又统称为连接(Linking)阶段。

 

竟然还有人没搞懂 JVM 类加载器?一文彻底明白

 

类的加载的时机

虚拟机规范并未严格规定类加载的时机,跟具体的JVM虚拟机有关。类加载的最佳时机是解析Java字节码类文件中常量池符号的时候,Class.forName()、ClassLoader.loadClass()、反射API和JNI_FindClass都可以触发类加载,Hot JVM自身启动的时候也会触发类加载。

通过JVM参数中加-verbose:class,可以在应用启动的时候打印类加载的过程,如下图所示:

 

竟然还有人没搞懂 JVM 类加载器?一文彻底明白

 

  1. 初始化这个阶段,JVM虚拟机给出了5种必须对类进行“初始化”的情况
  2. 使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候、调用一个类的静态方法的时候;
  3. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要先触发其初始化;
  4. 当初始化一个类的时候,如果发现其父类还没有被初始化,则要先初始化其父类;
  5. 当虚拟机启动时,用户需要指定一个执行的主类(包含main方法的那个类),则虚拟机会优先初始化这个主类;
  6. 在JDK1.7以后,动态语言支持的时候,如果一个java.lang.invoke.MethodHandle实例最后的结果是要执行第1种情况的操作,则也要进行初始化。

类的卸载时机

类的卸载跟采用的垃圾收集算法有关,在CMS中有两种方法卸载不必要的类,一种是等到元空间(Metaspace)满了的时候触发FGC,另一种是使用跟CMS并发收集算法类似的方式,不过对于元空间的阈值和触发CMS并发收集的阈值是独立的。更具体的可以参考之前的文章:CMS学习笔记。在这里,我们只需要记住,JVM中一个类的卸载要满足下面这3个条件:

  1. 该类所有的实例对象都已被回收;
  2. 该类的类加载器对象已经被回收;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

类加载器的作用

类的加载是需要类加载器完成的,但是类加载器在JVM中的作用可不止这些。在JVM中,一个类的唯一性是需要这个类本身和类加载一起才能确定的,每个类加载器都有一个独立的命名空间。

不同的类加载器,即使是同一个类字节码文件,最后再JVM里的类对象也不是同一个,下面的代码展示了这个结论:

  1. package jvm; 
  2. import java.io.IOException; 
  3. import java.io.InputStream; 
  4. public class ClassLoaderTest { 
  5.  public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, 
  6.  InstantiationException { 
  7.  ClassLoader myLoader = new ClassLoader() { 
  8.  @Override 
  9.  public Class<?> loadClass(String name) throws ClassNotFoundException { 
  10.  String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"
  11.  InputStream inputStream = getClass().getResourceAsStream(fileName); 
  12.  if (inputStream == null) { 
  13.  return super.loadClass(name); 
  14.  } 
  15.  try { 
  16.  byte[] b = new byte[inputStream.available()]; 
  17.  inputStream.read(b); 
  18.  return defineClass(name, b, 0, b.length); 
  19.  } catch (IOException e) { 
  20.  throw new ClassNotFoundException(); 
  21.  } 
  22.  } 
  23.  }; 
  24.  Object obj = myLoader.loadClass("jvm.ClassLoaderTest").newInstance(); 
  25.  System.out.println(obj.getClass()); 
  26.  System.out.println(obj instanceof jvm.ClassLoaderTest); 
  27.  ClassLoaderTest classLoaderTest = new ClassLoaderTest(); 
  28.  System.out.println(classLoaderTest.getClass()); 
  29.  System.out.println(classLoaderTest instanceof jvm.ClassLoaderTest); 
  30.  } 

上述代码的运行结果是:

 

竟然还有人没搞懂 JVM 类加载器?一文彻底明白

 

可以看出,代码中使用自定义类加载器(myLoader)加载的jvm.ClassLoaderTest类和通过应用程序类加载器加载的类不是同一个类。综上,类加载器在JVM中的作用有:

  1. 将类的字节码文件从JVM外部加载到内存中
  2. 确定一个类的唯一性
  3. 提供隔离特性,为中间件开发者提供便利,例如Tomcat

 

责任编辑:武晓燕 来源: 今日头条
相关推荐

2020-12-07 06:19:50

监控前端用户

2021-07-08 10:08:03

DvaJS前端Dva

2019-11-06 17:30:57

cookiesessionWeb

2021-06-30 08:45:02

内存管理面试

2020-03-18 14:00:47

MySQL分区数据库

2022-06-07 10:13:22

前端沙箱对象

2022-04-11 10:56:43

线程安全

2023-11-23 06:50:08

括号

2020-12-18 09:36:01

JSONP跨域面试官

2019-08-27 14:46:59

ElasticSearES数据库

2022-03-24 08:51:48

Redis互联网NoSQL

2021-10-20 08:49:30

Vuexvue.js状态管理模式

2023-05-29 08:45:45

Java注解数据形式

2021-04-16 20:47:42

Go 指令函数

2021-08-05 06:54:05

观察者订阅设计

2023-04-12 08:38:44

函数参数Context

2020-05-11 14:35:11

微服务架构代码

2024-02-23 12:11:53

装饰器模式对象

2021-01-06 13:52:19

zookeeper开源分布式

2023-03-13 08:12:37

Golang编程路径问题
点赞
收藏

51CTO技术栈公众号