程序员的成长之路

互联网/程序员/技术/资料共享

关注

阅读本文大概需要 12 分钟。

来自:juejin.im/post/5cffa528e51d4556da53d091

一、引言

当程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、链接、初始化三个步骤对该类进行类加载。

二、类的加载、链接、初始化

1、加载

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象。类的加载过程是由类加载器来完成,类加载器由JVM提供。我们开发人员也可以通过继承ClassLoader来实现自己的类加载器。

1.1、加载的class来源
  • 从本地文件系统内加载class文件

  • 从JAR包加载class文件

  • 通过网络加载class文件

  • 把一个java源文件动态编译,并执行加载。

2、类的链接

通过类的加载,内存中已经创建了一个Class对象。链接负责将二进制数据合并到 JRE中。链接需要通过验证、准备、解析三个阶段。

2.1、验证

验证阶段用于检查被加载的类是否有正确的内部结构,并和其他类协调一致。即是否满足java虚拟机的约束。

2.2、准备

类准备阶段负责为类的类变量分配内存,并设置默认初始值。

2.3、解析

我们知道,引用其实对应于内存地址。思考这样一个问题,在编写代码时,使用引用,方法时,类知道这些引用方法的内存地址吗?显然是不知道的,因为类还未被加载到虚拟机中,你无法获得这些地址。

举例来说,对于一个方法的调用,编译器会生成一个包含目标方法所在的类、目标方法名、接收参数类型以及返回值类型的符号引用,来指代要调用的方法。

解析阶段的目的,就是将这些符号引用解析为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必会触发解析与初始化)。

3、类的初始化

类的初始化阶段,虚拟机主要对类变量进行初始化。虚拟机调用< clinit>方法,进行类变量的初始化。

java类中对类变量进行初始化的两种方式:

  1. 在定义时初始化

  2. 在静态初始化块内初始化

3.1、< clinit>方法相关

虚拟机会收集类及父类中的类变量及类方法组合为< clinit>方法,根据定义的顺序进行初始化。虚拟机会保证子类的< clinit>执行之前,父类的< clinit>方法先执行完毕。

因此,虚拟机中第一个被执行完毕的< clinit>方法肯定是java.lang.Object方法。

public class Test {static int A = 10;static {A = 20;}
}class Test1 extends Test {private static int B = A;public static void main(String[] args) {System.out.println(Test1.B);}
}
//输出结果
//20

从输出中看出,父类的静态初始化块在子类静态变量初始化之前初始化完毕,所以输出结果是20,不是10。

如果类或者父类中都没有静态变量及方法,虚拟机不会为其生成< clinit>方法。

接口与类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法。

public interface InterfaceInitTest {long A = CurrentTime.getTime();}interface InterfaceInitTest1 extends InterfaceInitTest {int B = 100;
}class InterfaceInitTestImpl implements InterfaceInitTest1 {public static void main(String[] args) {System.out.println(InterfaceInitTestImpl.B);System.out.println("---------------------------");System.out.println("当前时间:"+InterfaceInitTestImpl.A);}
}class CurrentTime {static long getTime() {System.out.println("加载了InterfaceInitTest接口");return System.currentTimeMillis();}
}
//输出结果
//100
//---------------------------
//加载了InterfaceInitTest接口
//当前时间:1560158880660

从输出验证了:对于接口,只有真正使用父接口的类变量才会真正的加载父接口。这跟普通类加载不一样。

虚拟机会保证一个类的< clinit>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的< clinit>方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>方法完毕。

public class MultiThreadInitTest {static int A = 10;static {System.out.println(Thread.currentThread()+"init MultiThreadInitTest");try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {Runnable runnable = () -> {System.out.println(Thread.currentThread() + "start");System.out.println(MultiThreadInitTest.A);System.out.println(Thread.currentThread() + "run over");};Thread thread1 = new Thread(runnable);Thread thread2 = new Thread(runnable);thread1.start();thread2.start();}
}
//输出结果
//Thread[main,5,main]init MultiThreadInitTest
//Thread[Thread-0,5,main]start
//10
//Thread[Thread-0,5,main]run over
//Thread[Thread-1,5,main]start
//10
//Thread[Thread-1,5,main]run over

从输出中看出验证了:只有第一个线程对MultiThreadInitTest进行了一次初始化,第二个线程一直阻塞等待等第一个线程初始化完毕。

3.2、类初始化时机
  1. 当虚拟机启动时,初始化用户指定的主类;

  2. 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;

  3. 当遇到调用静态方法或者使用静态变量,初始化静态变量或方法所在的类;

  4. 子类初始化过程会触发父类初始化;

  5. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口初始化;

  6. 使用反射API对某个类进行反射调用时,初始化这个类;

  7. Class.forName()会触发类的初始化

3.3、final定义的初始化

注意:对于一个使用final定义的常量,如果在编译时就已经确定了值,在引用时不会触发初始化,因为在编译的时候就已经确定下来,就是“宏变量”。如果在编译时无法确定,在初次使用才会导致初始化。

public class StaticInnerSingleton {/*** 使用静态内部类实现单例:* 1:线程安全* 2:懒加载* 3:非反序列化安全,即反序列化得到的对象与序列化时的单例对象不是同一个,违反单例原则*/private static class LazyHolder {private static final StaticInnerSingleton INNER_SINGLETON = new StaticInnerSingleton();}private StaticInnerSingleton() {}public static StaticInnerSingleton getInstance() {return LazyHolder.INNER_SINGLETON;}
}

看这个例子,单例模式静态内部类实现方式。我们可以看到单例实例使用final定义,但在编译时无法确定下来,所以在第一次使用StaticInnerSingleton.getInstance()方法时,才会触发静态内部类的加载,也就是延迟加载。

这里想指出,如果final定义的变量在编译时无法确定,则在使用时还是会进行类的初始化。

3.4、ClassLoader只会对类进行加载,不会进行初始化
public class Tester {static {System.out.println("Tester类的静态初始化块");}
}class ClassLoaderTest {public static void main(String[] args) throws ClassNotFoundException {ClassLoader classLoader = ClassLoader.getSystemClassLoader();//下面语句仅仅是加载Tester类classLoader.loadClass("loader.Tester");System.out.println("系统加载Tester类");//下面语句才会初始化Tester类Class.forName("loader.Tester");}
}
//输出结果
//系统加载Tester类
//Tester类的静态初始化块

从输出证明:ClassLoader只会对类进行加载,不会进行初始化;使用Class.forName()会强制导致类的初始化。

三、类加载器

类加载器负责将.class文件(不管是jar,还是本地磁盘,还是网络获取等等)加载到内存中,并为之生成对应的java.lang.Class对象。一个类被加载到JVM中,就不会第二次加载了。

那怎么判断是同一个类呢?

每个类在JVM中使用全限定类名(包名+类名)与类加载器联合为唯一的ID,所以如果同一个类使用不同的类加载器,可以被加载到虚拟机,但彼此不兼容。

1、JVM类加载器分类

1.1、Bootstrap ClassLoader

Bootstrap ClassLoader为根类加载器,负责加载java的核心类库。根加载器不是ClassLoader的子类,是有C++实现的。

public class BootstrapTest {public static void main(String[] args) {//获取根类加载器所加载的全部URL数组URL[] urLs = Launcher.getBootstrapClassPath().getURLs();Arrays.stream(urLs).forEach(System.out::println);}
}
//输出结果
//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar
//file:/C:/SorftwareInstall/java/jdk/jre/classes

根类加载器负责加载%JAVA_HOME%/jre/lib下的jar包(以及由虚拟机参数 -Xbootclasspath 指定的类)。

我们将rt.jar解压,可以看到我们经常使用的类库就在这个jar包中。

1.2 、Extension ClassLoader

Extension ClassLoader为扩展类加载器,负责加载%JAVA_HOME%/jre/ext或者java.ext.dirs系统熟悉指定的目录的jar包。大家可以将自己写的工具包放到这个目录下,可以方便自己使用。

1.3、 System ClassLoader

System ClassLoader为系统(应用)类加载器,负责加载加载来自java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器默认都以系统类加载器作为父加载器。

四、类加载机制

1.1、JVM主要的类加载机制。

  1. 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也由该类加载器负责载入,除非显示使用另一个类加载器来载入。

  2. 父类委托(双亲委派):先让父加载器试图加载该Class,只有在父加载器无法加载时该类加载器才会尝试从自己的类路径中加载该类。

  3. 缓存机制:缓存机制会将已经加载的class缓存起来,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存中不存在该Class时,系统才会读取该类的二进制数据,并将其转换为Class对象,存入缓存中。这就是为什么更改了class后,需要重启JVM才生效的原因。

注意:类加载器之间的父子关系并不是类继承上的父子关系,而是实例之间的父子关系。

public class ClassloaderPropTest {public static void main(String[] args) throws IOException {//获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println("系统类加载器:" + systemClassLoader);/*获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定,如果操作系统没有指定CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径*/Enumeration<URL> eml = systemClassLoader.getResources("");while (eml.hasMoreElements()) {System.out.println(eml.nextElement());}//获取系统类加载器的父类加载器,得到扩展类加载器ClassLoader extensionLoader = systemClassLoader.getParent();System.out.println("系统类的父加载器是扩展类加载器:" + extensionLoader);System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));System.out.println("扩展类加载器的parant:" + extensionLoader.getParent());}
}
//输出结果
//系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
//file:/C:/ProjectTest/FengKuang/out/production/FengKuang/
//系统类的父加载器是扩展类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
//扩展类加载器的加载路径:C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
//扩展类加载器的parant:null

从输出中验证了:系统类加载器的父加载器是扩展类加载器。但输出中扩展类加载器的父加载器是null,这是因为父加载器不是java实现的,是C++实现的,所以获取不到。但扩展类加载器的父加载器是根加载器。

1.2、类加载流程图

图中红色部分,可以是我们自定义实现的类加载器来进行加载。

五、创建并使用自定义类加载器

1、自定义类加载分析

除了根类加载器,所有类加载器都是ClassLoader的子类。所以我们可以通过继承ClassLoader来实现自己的类加载器。

ClassLoader类有两个关键的方法:

  1. protected Class loadClass(String name, boolean resolve):name为类名,resove如果为true,在加载时解析该类。

  2. protected Class findClass(String name) :根据指定类名来查找类。

所以,如果要实现自定义类,可以重写这两个方法来实现。但推荐重写findClass方法,而不是重写loadClass方法,因为loadClass方法内部会调用findClass方法。

我们来看一下loadClass的源码

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {//第一步,先从缓存里查看是否已经加载Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {//第二步,判断父加载器是否为nullif (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) {//第三步,如果前面都没有找到,就会调用findClass方法long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

loadClass加载方法流程:

  1. 判断此类是否已经加载;

  2. 如果父加载器不为null,则使用父加载器进行加载;反之,使用根加载器进行加载;

  3. 如果前面都没加载成功,则使用findClass方法进行加载。

所以,为了不影响类的加载过程,我们重写findClass方法即可简单方便的实现自定义类加载。

2、实现自定义类加载器

基于以上分析,我们简单重写findClass方法进行自定义类加载。

public class Hello {public void test(String str){System.out.println(str);}
}public class MyClassloader extends ClassLoader {/*** 读取文件内容** @param fileName 文件名* @return*/private byte[] getBytes(String fileName) throws IOException {File file = new File(fileName);long len = file.length();byte[] raw = new byte[(int) len];try (FileInputStream fin = new FileInputStream(file)) {//一次性读取Class文件的全部二进制数据int read = fin.read(raw);if (read != len) {throw new IOException("无法读取全部文件");}return raw;}}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {Class clazz = null;//将包路径的(.)替换为斜线(/)String fileStub = name.replace(".", "/");String classFileName = fileStub + ".class";File classFile = new File(classFileName);//如果Class文件存在,系统负责将该文件转换为Class对象if (classFile.exists()) {try {//将Class文件的二进制数据读入数组byte[] raw = getBytes(classFileName);//调用ClassLoader的defineClass方法将二进制数据转换为Class对象clazz = defineClass(name, raw, 0, raw.length);} catch (IOException e) {e.printStackTrace();}}//如果clazz为null,表明加载失败,抛出异常if (null == clazz) {throw new ClassNotFoundException(name);}return clazz;}public static void main(String[] args) throws Exception {String classPath = "loader.Hello";MyClassloader myClassloader = new MyClassloader();Class<?> aClass = myClassloader.loadClass(classPath);Method main = aClass.getMethod("test", String.class);System.out.println(main);main.invoke(aClass.newInstance(), "Hello World");}
}
//输出结果
//Hello World

ClassLoader还有一个重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是将class的二进制数组转换为Calss对象。

此例子很简单,我写了一个Hello测试类,并且编译过后放在了当前路径下(大家可以在findClass中加入判断,如果没有此文件,可以尝试查找.java文件,并进行编译得到.class文件;或者判断.java文件的最后更新时间大于.class文件最后更新时间,再进行重新编译等逻辑)。

六、总结

本篇从类加载的三大阶段:加载、链接、初始化开始细说每个阶段的过程;详细讲解了JVM常用的类加载器的区别与联系,以及类加载机制流程,最后通过自定义的类加载器例子结束本篇。小弟能力有限,大家看出有问题请指出,让博主学习改正。欢迎讨论啊。

注意:本篇博客总结主要来源。如有转载,请注明出处

  1. 《疯狂java讲义(第3版)》

  2. 《深入理解java虚拟机++JVM高级特性与最佳实践》

<END>

推荐阅读:

【36期】说说 如何停止一个正在运行的线程?

【35期】谈谈你对Java线程之间通信方式的理解

【34期】谈谈为什么要拆分数据库?有哪些方法?

5T技术资源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,单片机,树莓派,等等。在公众号内回复「2048」,即可免费获取!!

微信扫描二维码,关注我的公众号

朕已阅 

【37期】请你详细说说类加载流程,类加载机制及自定义类加载器相关推荐

  1. 请你详细说说类加载流程,类加载机制及自定义类加载器

    一.引言 当程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载.链接.初始化三个步骤对该类进行类加载. 二.类的加载.链接.初始化 1.加载 类加载指的是将类的class文件读入内存,并 ...

  2. ccs加载out文件_类加载流程、类加载机制及自定义类加载器详解

    原文:juejin.im/post/5cffa528e51d4556da53d091 一.引言 当程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载.链接.初始化三个步骤对该类进行类加载 ...

  3. JVM类加载机制、双亲委派机制、自定义类加载器、打破双亲委派机制

    1.类加载器 站在Java虚拟机的角度看,只有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(HotSpot虚拟机.JDK8中), ...

  4. 自定义类加载器_jvm超详细探索自定义类加载器(值得收藏)

    原创:鲁班学院子牙老师 微信公众号搜索启明南 如果你想看懂本篇文章,需要你对类加载器有一定的了解.如JVM自带的类加载器.双亲委派.自定义类加载器.类加载每个阶段做了什么--如果你对这些知识还有夹生的 ...

  5. ​大数据和云计算技术周报(第37期)

    写在第37期周报 "大数据" 三个字其实是个marketing语言,从技术角度看,包含范围很广,计算.存储.网络都涉及,知识点广.学习难度高. 本期会给大家奉献上精彩的:Spark ...

  6. http://click.aliyun.com/m/20492/ DT科技评论第37期:Cato_Network将会重新定义下一代网络以及安全

    原文链接 DT科技评论第37期:Cato Network将会重新定义下一代网络以及安全 dt科技评论  2017-05-10 15:08:12  浏览68  评论0 安全 无线 防火墙 云安全 互联网 ...

  7. 《强化学习周刊》第37期:视觉深层框架、Transformer World模型、注意力增强强化学习...

    No.37 智源社区 强化学习组 强 化 学  习 研究 观点 资源 活动 关于周刊 强化学习作为人工智能领域研究热点之一,其研究进展与成果也引发了众多关注.为帮助研究与工程人员了解该领域的相关进展和 ...

  8. 【Android 逆向】Dalvik 函数抽取加壳 ( 类加载流程分析 | native 函数查询 | dalvik_system_DexFile.cpp#defineClassNative 函数 )

    文章目录 前言 一.查询 defineClassNative 函数 二.dalvik_system_DexFile.cpp#Dalvik_dalvik_system_DexFile_defineCla ...

  9. 【Android 逆向】Dalvik 函数抽取加壳 ( 类加载流程分析 | DexPathList#findClass 函数分析 | DexFile#loadClassBinaryName 函数 )

    文章目录 前言 一.DexPathList.java#findClass 类加载函数源码分析 二.DexFile.java#loadClassBinaryName 函数源码分析 前言 上一篇博客 [A ...

  10. ​大数据和云计算技术周报(第37期) - 云+社区 - 腾讯云

    写在第37期周报 "大数据" 三个字其实是个marketing语言,从技术角度看,包含范围很广,计算.存储.网络都涉及,知识点广.学习难度高. 本期会给大家奉献上精彩的:Spark ...

最新文章

  1. 我终于决定要放弃 okhttp、httpClient,选择了这个牛逼的神仙工具!贼爽
  2. VS.NET2005中的WEBPART初步(一)
  3. GitHub:除非真正需要,否则我们不会删除您的任何内容
  4. 在控制台显示sql语句,类似hibernate show_sql.
  5. 可能 delphi7 下稳定的最后一版本 GDIPLUS
  6. SAP Commerce Cloud 的代码仓库
  7. C/C++字符串输入方法比较(带回车不带回车输入)
  8. Python操作文件和目录
  9. input readonly 光标显示问题
  10. adb logcat 通过包名过滤日志并输出到txt文件
  11. 拓展Revit的方式
  12. keil编译器如何生成二进制bin文件
  13. 在docker容器中使用显卡
  14. 2022天勤数据结构
  15. java中什么是实例化
  16. 计算机管理器鼠标不见了怎么办,电脑的鼠标光标消失了
  17. 【线上直播ING】2016互联网金融应用发展半年报
  18. Xcode8使用出现bundleid: com.xxx.xxx, enable_level: 0, persist_level: 0, propagate_with_acti
  19. 计算机word资料,怎样快速找到电脑中的Word文档
  20. cpu 占用过高排查

热门文章

  1. 配置静态资源不重启即时生效
  2. 《疯狂java讲义》学习(19):枚举类
  3. 焊接技巧 -- 拖焊
  4. 关于临时HY学长被安排拉二分题不想翻译找到DYM学长这件事
  5. 禁用笔记本自带键盘技巧
  6. H5移动端出生日期插件
  7. gromacs 安装_gromacs cpu版安装
  8. 记录|小鸟云服务器搭建web服务
  9. 五分理解IaaS,PaaS,SaaS都是什么
  10. 该内存不能为 read/written解决办法