java类加载机制?双亲委派模型有可能被破坏吗

什么是类加载机制?

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行,校验,转换解析,初始化,最终形成可以被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制
与一般的编译语言不同,在java语言里面,类的加载,解析,链接,初始化等都是在运行时动态进行的,这样虽然有一些性能开销,但是为java语言提供了很大的灵活性

类加载时机

类文件被加载到内存中开始,卸载出内存为止,它的整个生命周期包括,
加载 > 验证 > 准备 > 解析 > 初始化 > 使用 > 卸载,其中 加载,验证,准备,初始化,卸载这几个顺序是确定的,但是解析可能不是(解析是指把常量池中的符号引用,替换成直接引用的过程),它在某些情况下可能,初始化之后才开始执行,这是为了支持java的运行时绑定,比如多态情况下。
什么时候开始类加载机制的第一个阶段:加载? 虚拟机中并没有明确规定,但是对于初始化阶段,虚拟机严格规定了5种情况必须对类进行初始化

  • 1 遇到new, getstatic, putstatic, 或者 invokestatic 这4条字节码指令时,分别对应java 的 new 创建对象,使用用static变量,设置static变量,或者调用static方法时
  • 2 使用java.lang.reflect包的方法对类进行反射调用的时候
  • 3 当初始化一个类的时候,如果发现其父类没有被初始化,则先初始化其父类
  • 4 当虚拟机启动时,包含main方法的那个类,虚拟机会先初始化这个类
  • 5 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHander的实例最后解析结果为REF_getStatic, REF_putStaic, REF_invokeStaitc方法的句柄,但是这个句柄对应的实体类没有被初始化,那么要先触发其初始化
    这5种情况被称为类的主动引用,除此之外的任何其他引用类的方式都不会触发初始化 例如
public class SupperObj {
    static {
        System.out.println("supper obj 被初始化");
    }
    public static String f = "hello,world";
}
public class TestObj extends SupperObj {
    static {
        System.out.println("TestObj 被初始化");
    }
}
调用该方法只会输出 supper Obj 被初始化
对于子类引用父类的静态字段,只有直接定义这个字段的类会被初始化
String t =  TestObj.f;

例2 
TestObj [] testObjs = new TestObj[0];
执行该代码并没有输出  TestObj 被初始化 
说明并没有触发 com.haojiangbo.test.TestObj 的初始化阶段
查看字节码发现 对应的是 anewarray   指令,
这个字节码,不属于主动引用的五种情况的范畴
附 字节码
 stack=1, locals=2, args_size=1
         0: iconst_0
关键     1: anewarray     #2                  // class com/haojiangbo/test/TestObj
         4: astore_1
         5: return

例3 
public class SupperObj {
    static {
        System.out.println("supper obj 被初始化");
    }
    public static final String f = "hello,world";
}
String t =  TestObj.f;
该代码也不会触发类的初始化,因为编译的时候,常量已经预编译到
类文件的常量池里了,所以不存在对 SupperObj  任何引用关系
附 字节码
Constant pool:
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
   #2 = Class              #22            // com/haojiangbo/test/TestObj
   常量池已经包含 这个字符串
   #3 = String             #23            // hello,world
   
 Code:
      stack=1, locals=2, args_size=1
      	加载常量池中的字符串
         0: ldc           #3                  // String hello,world
        赋值到本地变量表 slot 1 的位置  java代码 String t =  TestObj.f;
         2: astore_1
         3: return
      LineNumberTable:
        line 5: 0
        line 6: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0     a   [Ljava/lang/String;
            3       1     1     t   Ljava/lang/String;

接口的加载过程,和类加载过程有些区别,接口的初始化,并不会引起其父接口的初始化

public interface Inteface1 {
    String a = Test.print("1");
}
public interface Inteface2  extends  Inteface1{
    String b = Test.print("2");
}
public class TestObj  implements Inteface2{
    static {
        System.out.println("TestObj 被初始化");
    }
}
String z =  TestObj.b;
执行这段代码只会输出 2 ,因为 并不会触发 inteface 的父接口初始化

类与类加载器

类加载器虽然只用于类的加载动作,但是它在java程序中起到的作用远远不限制于类加载阶段,对于任意一个类,都需要由加载这个类的类加载器和类本身一同确认这个类在jvm中的唯一性,也就是说,同一个类文件,如果被不同的类加载器加载,那么这两个类肯定不相等,比如调用 instandof 关键字判断

双亲委派模型

从 jvm 的角度来看,类加载器只存在2种类型,一种是 (bootstarp ClassLoader) 这个是启动类加载器,使用c++语言实现,是虚拟机自身的一部分,另一种就是其他的类加载器,这里借用断水流大师兄的话就是说,只有空手道部门,或者其他部门,bootstarp就是空手道部门,剩下的类加载器就是其他部门,这些其他类加载器由java语言实现,集成抽象类,java.lang.classLoader
常用的3种类加载器

  • 1启动类加载器(bootstarap ClassLoader)
    前面已经说过,这个加载器是空手道部分,属于jvm的一部分,由c++实现,作用是负责将存放在<JAVA_HOME>/lib中的,或者被 -Xbootclasspath 指定的路径中,并且被虚拟机识别的(按照文件名识别,比如rt.jar,名字不符合的类库,即使放在lib目录下也不会被加载)

  • 2扩展类加载器(extension classloader)
    这个类加载器,负责加载 <JAVA_HOME>/lib/ext目录中的,或者是被 java.ext.dir系统变量所指定的路径中的类库,开发者可以直接使用该类加载器

  • 3应用程序类加载器(application classLoader)
    一般情况下我们称他为系统类加载器,它负责加载用户类路径 classpath上指定的类库,开发者可以直接使用这个类加载器,如果没有自定义类加载器,这个类加载器就是默认类加载器

  • 4自定义类加载器
    指我们开发者 extends ClassLoader 自己实现的类加载器
    在这里插入图片描述* 双亲委派的工作流程
    如上图,双亲委派的工作流程是,如果一个类加载器收到了类加载的请求,那么它不会自己尝试加载这个类,而是先委托它的父类加载器去加载这个类,每一层的类加载器都是如此,只有父类加载器反馈无法完成这个加载请求的时候(它的搜索范围,没有找到对应的类),子类加载器才会自己去加载
    使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是java的类也跟着类加载器有了层次关系,比如java.lang.Object,它存放在rt.jar之中,不管是哪个类需要加载这个类,都会最终委派到最顶级的类加载器中,这样就保证了不管在哪个类加载器下面,这个类都是同一个类。

  • 破坏双亲委派模型
    双亲委派模型并不是一个强制性的约束模型,而是java推荐给开发者的一种模型,java中的大部分类加载器都是遵循着这个模型,但是也有例外, 到现在为止,双亲委派模型出现过3次较大规模的破坏

第一次被破坏
发生在双亲委派模型出现之前,双亲委派模型是jdk1,2之后引入的,为了兼容之前的版本,java.lang.ClassLoader 增加了一个新的protected方法findClass() 在此之前,继承classloader的目的无非就是为了重写loadclass方法,1.2之后已经不提倡覆盖loadClass方法,应当写到findClass方法之中,如果父类加载器加载失败,则自动会调用自己的findClass方法,这样就保证写出来的类加载器是符合双亲委派模型的
第二次破坏
第二次破坏是由这个模型自身的缺陷引起的,双亲委派模型很好的解决了基础类在各个类加载器中的统一问题,理想状态下都是由用户调用基础类的代码,但如果基础类需要调用用户编写的代码呢,由于父类加载器中并不存在子类加载器加载的的类文件,为了解决这个问题,java开发团队只好引入了一个不太优雅的设计, 线程上线文类加载器, 这个类加载器可以通过java.lang.Thread的setContextClassLoaser()方法进行设置,如果创建线程时未设置,则从父线程中继承一个,如果应用程序全局范围内都没有设置过得话,那么就是默认的 应用程序类加载器,有了线程上下文类加载器,在执行基类的代码时,就可以得到当前应用程序的类加载器,因为用户的代码都是由应用程序类加载器加载,所以就可以通过反射或者其他的办法进行调用
第三层次破坏
第三次破坏是当前一些热门的词 “热替换”,“热部署”,动态替换,已经加载的类文件,这种不太熟悉,不做介绍,详情可参考其他文章。
发布了11 篇原创文章 · 获赞 1 · 访问量 360

猜你喜欢

转载自blog.csdn.net/qq_37421368/article/details/105668992