java底层系列-类加载机制

332 阅读8分钟

首先关于为什么我写博客的原因。 

每次遇到无法解决的的技术问题,我会先确定问题的范畴,然后去百度查询答案。 

诚然这样的方式低成本的解决问题方式, 在大部分的问题上还是起到了不错的效果。在这个过程中我发现很多人的技术博客还是写的相当不错的,分析问题不仅全面,而且还有深度,不禁为他人的能力所折服。 

当然看书也是一种学习,一种比较系统化和全面化的深度学习。 既然是学习,肯定会谈论吸收,有的时候为了加深对知识的理解,就会写一些笔记,或者直接用到工作中,解决实际技术问题。

 后来有人跟我说,你也可以向那些博主学习,可以把自己的笔记或技术见解整理一下,发表成博客,先不管前期是否有人会看,最起码可以提高自己知识整理的能力,还可以回顾一下以前的东西。 

有时想想,感觉自己技术还不够牛,或是害怕博客写的不好,所以有种退却的理由。 但是,很多事情先有念头,后来才有了行动,只要坚持,总会在这个过程中收获很多,博客质量也会慢慢提升! 想要达到高级的水平,需要不断的学习,在这个过程会吸收大量知识,而人的记忆是有限的。 

我想,把之前写的印象笔记整理出来吧,写博客倒是一条不错的选择。所以在将来每隔一段时间,会将学习的东西整理出来,发表成博客,可以方便自己的学习和巩固。 

说正事用正经图,那么现在正片开始:

java程序编译运行原理:

从虚拟机视角来看,执行 Java 代码首先需要将它编译而成的 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。类的加载:指的是将类的.class文件中的二进制数据读入到内存中

java中的类是怎么加载的?

我们使用java命令运行启动类时,首先需要的便是通过类加载器将类加载到jvm中去。通过Java命令执行代码的大体流程如下:

其中loadClass的类加载过程有如下几步:

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的

main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的

java.lang.Class对象,作为方法区这个类的各种数据的访问入口

  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如

main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过

程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。

  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的

引用、对应class实例的引用等信息。

类加载器的引用:这个类到类加载器实例的引用

对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的

对象实例一般存放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

注意:

主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

java中类加载的双亲委派机制:

上面介绍了java中项目启动启动后,类的加载过程--->类是由类加载器加载到jvm中的,java中的类加载器又分可分为以下四种:

  • 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等 
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包 
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
  • 自定义加载器:负责加载用户自定义路径下的类包

类加载器初始化过程

参见类运行加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher。sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个

sun.misc.Launcher实例。

在Launcher构造方法内部,其创建了两个类加载器,分别是

sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应

用类加载器)。

在初始化这俩个类加载器的过程中为拓展类加载器ExtClassLoader指定的父类加载为null,为

在初始化ExtClassLoader的时候 设置的他的父类加载器为null,为引导类加载器AppClassLoader(应用类加载器)指定的父类加载器为ExtClassLoader

(bootstrapLoader)是c++进行编写的 是无法获取到的 所以设置为空,在后续的类加载过程中,向上委托加载的过程中,每次嵌套加载查询过程中都会判断是有存在父类加载进行判断是否需要向上委托加载使用。

注意:

我们这里所指的父类加载器 只是单纯的指定加载器间的关系,ExtClassLoader、AppClassLoader之间无直接继承的关系存在。他们都是继承了urlclassloader.

JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。

在类加载器初始化完成之后,我们项目所使用到的.class会经过这三个类加载(此时暂不考虑到用户自定义的类加载器)器加载到jvm堆中去,其主要流如下:

类的双亲委派机制其实本质上是逐级的类加载器去查看父类加载器中是否已经加载过了,如果已经加载那么就直接获取父类加载的,不必在去加载。

如果父类未加载那么就需要向上委托,让父类加载器的加载器去进行加载

这里其实需要详细解释下:

第一次进来的时候是appcalssloader所以会先通过findloadedclass()查看是否已经加载,如果已经加载会立即返回。

若未加载便会调用他的父类加载器,此时也就是appcalssload的parent的loadclass方法进行类型的加载。在这里我们跟进去parent.loadClass(name, false);这个方法  就会发现还是跳转到这个方法 但是此时的类加载器 已经有appclassload变为extclassload。  那么上面的流程会重复一次。

但又有所不同!

因为extcalssload在前文的launcher构造器中初始化的时候是没有设置其父类加载器的,那么其实parent就是就是一个null。所以这个时候就会调用findBootStrapClassOrNull(),进行底层的类加载。

如果此时还是没加载类的话,那么此时就会父加载器加载失败,由子类加载器自行加载,通过URLclassloader下的findclass进行一个类向下嵌套委托的加载,知道类被加载为止,代码如下:

在findclass()内是由defineclass()根据路径名进行一个类的加载实现的。

为什么要设计双亲委派机制?

沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心

API库被随意篡改

避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一

次,保证被加载类的唯一性 。

为什么要将类加载器默认设计成为appcalssloader?

因为对于我们web项目来言,90%的class都是我们用户自己定义的,所以这些都会加载在appclassloader加载内,如果不是从appclassloader开始的话,那么如果从引导类加载器开始加载,那么就会消耗很多不必要的性能。