复习整理篇-类加载

一.类加载机制

  JVM把class文件加载到内存,并对数据进行校验、准备、解析、初始化,最终形成JVM可以直接使用的Java类型的过程。

图片引用:https://baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc

在这里插入图片描述

1.加载
  将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码库、常量池等),在堆中生成一个class类对象代表这个类(反射原理),作为方法区类数据的访问入口。

图片引用:https://www.jianshu.com/p/dd39654231e0

在这里插入图片描述
在加载阶段,虚拟机需要完成以下3件事情:
  ①通过一个类的全限定名来获取定义此类的二进制字节流;
  ②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  ③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

2.验证
  验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
①文件格式验证
  第一阶段要验证字节流是否符合class文件格式的规范,并且能够被当前版本的虚拟机处理。
②元数据验证
  第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
③字节码验证
  第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
④符号引用验证
  最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身除外(常量池中的各种符号引用)的信息进行匹配性校验。

3.准备
  正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内部都将在方法区中进行分配。注意此时的设置初始值为默认值,具体赋值在初始化阶段完成。

4.解析
  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
  符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的class文件格式中。
  直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

5.初始化
  初始化阶段是执行类构造器()方法的过程。
  类构造器clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
  对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
  ①遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  ②使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  ③当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  ④当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  ⑤当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

二.Java程序初始化顺序

1.父类的静态变量    2.父类的静态代码块    3.子类的静态变量
4.子类的静态代码块   5.父类的非静态变量    6.父类的非静态代码块
7.父类的构造方法    8.子类的非静态变量    9.子类的非静态代码块
10.子类的构造方法

三.类的引用

1.主动引用(一定会初始化)

  • new一个类的对象
  • 调用类的静态成员(除了final常量)和静态方法
  • 使用java.lang.reflect包的方法对类进行反射调用
  • 当虚拟机启动,java.Hello,则一定会初始化Hello类,说白了就是先启动main方法所在的类
  • 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类

2.被动引用

  • 当访问一个静态域后,只有真正声明这个域的类才会被初始化
      例:通过子类引用父类的静态变量,不会导致子类初始化
  • 通过数组定义类引用,不会触发此类的初始化
  • 引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池了)

四.类加载器的原理

1.类缓存
  标准的Java S类加载器可以按照要求查找类,一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过,jvm垃圾收集器可以回收这些Class对象。

2.类加载器的分类

图片引用:https://www.cnblogs.com/fengbs/p/7595849.html

在这里插入图片描述
(1)启动类加载器
  ①它来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar,sun.boot.class.path路径下的内容),是用原生代码(C语言)来实现的,并不继承自java.lang.ClassLoader
  ②加载扩展类和应用程序类加载器,并指定它们的父类加载器
(2)扩展类加载器
  ①用来加载Java的扩展库(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路径下的内容)。Java虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载Java类。
  ②由sun.misc.Launcher $ ExtClassLoader实现
(3)应用程序类加载器
  ①它根据Java应用的类路径(Classpath.java.class.path路径下的内容)来加载Java类,一般来说,java应用的类都是由它来完成加载的。
  ②由sun.misc.Launcher $ AppClassLoader实现
(4)自定义类加载器
  ①开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

3.java.lang.ClassLoader类
  (1)作用:java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成对应的字节代码,然后从这些字节代码中定义出一个Java类,即Java.lang.Class类的一个实例。
  ·ClassLoader还负责加载java应用所需的资源,如图像文件和配置文件等
  (2)常用方法:
  ·getParent():返回该类加载器的父类加载器
  ·loadClass(String name):加载名称为name的类,返回的结果是java.lang.class类的实例
  此方法负责加载指定名字的类,首先会从已加载的类中去寻找,如果没有找到,从parentClassLoader[ExtClassLoader]中去加载;如果没有加载成功,则从Bootstrap ClassLoader中尝试加载(findBootstrapClassOrNull方法),如果还是加载失败,则自己加载。如果还不能加载,则抛出ClassNotFoundException。

  ·findClass(String name):查找名称为name的类,返回的结果是java.lang.class类的实例。
  ·findLoadedClass(String name):查找名称为name的已经被加载过的类,返回的结果是java.lang.class类的实例。
  ·defineClass(String name,byte[] b,int off,int len):把字节数组b中的内容转换成Java类,返回的结果是java.lang.class类的实例。这个方法被声明为final的
  ·resolveClass(Class<?> c):链接指定的java类

五.类加载器的代理模式

  代理模式即是将指定类的加载交给其它的类加载器。常用双亲委托机制
  双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
  双亲委托机制是为了保证Java核心库的类型安全,这种机制保证不会出现用户自己能定义java.lang.Object类等的情况。例如:用户定义了java.lang.String,那么加载这个类时最高级父类会首先加载,发现核心类中也有这个类,那么就加载了核心类库,而自定义的永远都不会加载
注意:双亲委托机制是代理模式的一种,但并不是所有的类加载器都采用双亲委托机制。在tomcat服务器类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。

六.自定义类加载器

1.首先检查请求的类型是否已经被这个类加载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;
2.委派类加载请求给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;
3.调用本类加载器的findClass()方法,试图获取对应的字节码,如果获取的到,则调用defineClass()导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(),loadClass()转抛异常,终止加载过程。【这里的异常种类不止一种】
  被两个类加载器加载的同一个类,jvm认为是不相同的类。

七.线程上下文类加载器

  通常你需要动态加载资源的时候,你至少有三个ClassLoader可以选择:
  1.系统类加载器或叫做应用类加载器(system classLoader or application classLoader)
  2.当前类加载器
  3.当前线程类加载器
  当前线程类加载器是为了抛弃双亲委派加载链模式。

  每个线程都有一个关联的上下文类加载器。如果你使用new Thread()方式生成新的线程,新线程将继承其父进程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都是用系统类加载器作为上下文类加载器。
  Thread.currentThread().getContextClasLoader()

八.Tomcat服务器的类加载器

  每个web应用都有一个对应的类加载器实例。该类加载器也使用代理模式(不同于前面说的双亲委托机制),所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。但也是为了保证安全,这样核心库就不在查询范围之内。

原文链接:【Java基础】类加载过程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值