美文网首页JVM
ClassLoader加载器

ClassLoader加载器

作者: 哓晓的故事 | 来源:发表于2018-05-03 15:55 被阅读0次

    Java类的加载机制

    Java是面向对象的程式,通过Class来提供服务,Class是二进制字节码(byte code)需要通过类加载器加载后方可以使用。
    类的加载是将.class文件的二进制数据读入到内存,放入Method Area内,将实例化的Class信息存储在Heap中。并向开发人员访问Method Area数据结构的接口
    JVM规范允许类加载器做pre load,在pre load的时候发.class缺失并不会报错,只有在首次使用类的时候才报告错误。
    判断两个类是否相当,首先要类相等,其次是类加载器要相等

    任何一个Java类,都是通过某个ClassLoader加载的。通过这个ClassLoader加载的类的实例,会保存这个类对象的引用,也就是MyClass.class这种。而类对象,会保留这个ClassLoader的引用。反过来,在ClassLoader中,也会保持对这个类对象的引用。
    ClassLoader和类对象之间是双向引用

    JAVA开发人员眼中的JDK类加载器分类

    1. BootStrap ClassLoader 负责加载jre/lib 下的基础类库,类似rt.jar,使用C/C++编写,不可被Java程式引用
    2. Extension ClassLoader 负责加载jre/lib/ext目录下的所有类库,所有的Application ClassLoader都是继承此加载器
    3. Application ClassLoader 负责加载$ClassPath目录下所指定的类

    类加载方式

    类加载方式.png
    1. new Class() 隐式加载
      • 自动加载静态方法块,带参构造器
      • 并且实例化(将装载初始化合并成一个)
    2. Class.forName() 显示加载
      • 解析类,并执行类中的静态方法块【初始化】(也可以选择不加载静态方法块),无参构造器
      • 返回一个给定类或者接口的一个 Class 对象,如果没有给定 classloader, 那么会使用根类加载器。如果 initalize 这个参数传了 true,那么给定的类如果之前没有被初始化过,那么会被初始化
      • http://www.runoob.com/w3cnote/java-class-forname.html
    3. ClassLoader.loadClass()
      • 不会自动加载静态方法块,采用parent delegate装载
      • Spring框架中的IOC的实现就是使用的ClassLoader,减少初始化造成的性能损耗(只是先加载类到虚拟机),绝大多数对象,在未使用前,都可以不先初始化,原语spring的lazy
      • spring 为什么使用 classloader

    分析加载源码原理

    JDK类加载机制

    • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
    • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
    • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区(Method Area)。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

    类的生命周期

    加载(load)-->验证(validate)-->准备(prepare)-->解析(parse)-->初始化(init)-->使用(using)-->卸载(unloading)

    类的生命周期.png
    • 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
      1. 获取类的二进制流
      2. 将二进制流中的静态存储结构放置到Method Area的Runtime数据结构
      3. 生成代表此类的java.lang.Class对象,作为Method Area的数据访问接口
    • 验证保证信息符合虚拟机要求(因为class文件不一定使用java语言编译得来),文件格式、元数据、字节码、符号引用验证;
      1. 文件格式,是否以magic 0xCAFEBABE开头等等...
      2. 元数据,对字节码描述进行语义分析,是否符合Java语言规范,覆盖父类的final方法
      3. 字节码,保证类方法不会对虚拟机安全造成危害,long放置到int操作栈
      4. 符号引用,类中是否存在描述的方法
    • 准备,为类的静态变量(不是实例变量)分配内存,并将其初始化为默认值(零值,并非指定的常量值)
      private static int a = 123; // 在准备阶段a=0,在初始化阶段a=123
      private static final int a = 123; // 在准备阶段a=123
      
    • 解析,把类中的符号引用转换为直接引用(这个步骤可以在初始化后执行),类或接口字段类方法接口方法
      1. 符号引用,用一组符号来描述引用的目标,不一定在内存中,String x="abc";
      2. 直接引用,直接指向目标的指针、相对偏移量、句柄,一定在内存中,System.out.println("name:" + "abc");
    • 初始化,为类的静态变量赋予正确的初始值执行类构造器<clinit>()方法[静态方法语句块](此方法不同于实例构造器<init>()由虚拟机保证)
      1. 只有new, getstatic, putstatic, invokestatic4条字节指令,会触发对象的初始化
      2. reflect反射调用,会触发初始化
      3. 举例: 直接调用父类static变量, 初始化的只有父类static method,没有子类static method
      4. 举例: Object obj和Object[] objs,前一个会初始化映射的是java.lang.Object,后一个映射的是[java.lang.Object
      5. 举例: 对类中常量的引用,不会引发初始化,因为指向的是常量池
      6. <clinit>()父类静态语句块优先于子类的变量赋值操作
      7. 静态语句块静态变量顺序
        1. <clinit>()中,静态语句块能访问和赋值其之前定义的静态变量
        2. <clinit>()中,静态语句块只能赋值其之后定义的静态变量
      8. 接口不能使用静态语句块,但是仍然有变量初始化动作
      9. <clinit>()由虚拟机保证在多线程环境下正确加锁和同步,但是这个很可能导致线程阻塞
    • 使用,new出对象程序中使用
    • 卸载,执行垃圾回收

    JDK双亲委派模式

    双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

    按照双亲委派机制加载类,每当需要加载一个新的类时,当前的类加载器会先委托其父加载器,查询有没有加载该类。如果父类加载器已近加载该类,那么直接返回加载的class对象,如果没有那么继续向上寻找父类加载器,如果在祖宗类加载器Bootstrap都没有加载该类,那么需要当前的类加载器自己加载,如果当前的类加载器也不能加载则会跑出ClassNotFoundException异常 (PS:类加载器没有向下寻找,没有getChild只有getParent)。

    Thread.currentThread().getContextClassLoader()指出当前的类加载器是AppClassLoader,需要加载MyClassLoader.class先在父类加载器(ExtClassLoader)中寻找,没有再向祖宗类加载中寻找(Bootstrap ClassLoader),还没有找到那么AppClassLoader自己去加载。

    意义:

    • 避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次
    • 安全方面考虑的,试想如果一个人写了一个恶意的基础类(如java.lang.String)并加载到JVM将会引起严重的后果,但有了全盘负责制,java.lang.String永远是由根装载器来装载
    • 即便是自定义classLoader也不能加载String,针对java.*开头jvm的实现中已经保证必须由bootstrp来加载

    如何破坏双亲委任模型?

    第一次:在双亲委派模型出现之前—–即JDK1.2发布之前。
    第二次:是这个模型自身的缺陷导致的。线程上下文类加载器(Thread Context ClassLoader)如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即使应用程序类加载器。
    第三次:为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。

    tomcat自定义类加载器

    tomcat是一个web容器,一个进程可以同时启动多个app线程
    多个app之间的Class隔离,同时部分Class需要共享
    tomcat 自定义了5个classLoader:

    1. CommonClassLoaderextends ApplicationClassLoader
      公用,catalina和shared共享
    2. CatalinaClassLoder extends CommonClassLoader
      web容器私有
    3. SharedClassLoader extends CommonClassLoader
      webapp之间共享
    4. WebAppClassLoader extends SharedClassLoader(通过配置是否绕过双亲委派)
      webapp私有
      内部重写了loadClass和findClass方法,实现了绕过“双亲委派”直接加载web应用内部的资源
      Context.xml文件中加上<Loader delegate = "true">开启正统的“双亲委派”加载机制
    5. JasperClassLoader extends WebAppClassLoader
      webapp私有

    自定义类加载器

    自定义类加载器,可以对字节码加密,防止反编译

    GroovyClassLoader

                null                      // 即Bootstrap ClassLoader  
                 ↑  
    sun.misc.Launcher.ExtClassLoader      // 即Extension ClassLoader  
                 ↑  
    sun.misc.Launcher.AppClassLoader      // 即System ClassLoader  
                 ↑  
    org.codehaus.groovy.tools.RootLoader  // 以下为User Custom ClassLoader  
                 ↑  
    groovy.lang.GroovyClassLoader  
                 ↑  
    groovy.lang.GroovyClassLoader.InnerLoader  
    

    groovy.lang.GroovyClassLoader 当一个脚本里定义了C这个类之后,另外一个脚本再定义一个C类的话,GroovyClassLoader就无法加载了,会将加载的类放置在方法区里
    groovy.lang.GroovyClassLoader.InnerLoader 每次编译groovy源代码的时候,都会新建一个InnerLoader的实例,由于编译完源代码之后,已经没有对它的外部引用,除了它加载的类,所以只要它加载的类没有被引用之后,它以及它加载的类就都可以被GC了

    结束生命周期

    1. System.exit()
    2. main()正常执行完成
    3. 程序执行过程中出现Exception异常或者Error错误
    4. 操作系统错误导致JVM终止

    编译器

    解释器

    前端编译器(javac)

    (*.java -> *.class(byte code))关注程序编码

    • 解析与填充字符表(语法)
      1. 词法分析 生成Token序列
      2. 语法分析 根据Token序列生成AST
    • 注解处理
      1. 插入注解
    • 分析与字节码生成(语义)
      1. 语义分析 attribute()和flow()
      2. 解语法糖
        2.1 Java使用类型擦除泛型实现,List<String> 和 List<Long>在Java编译后都是List,同一个类,取值会替换成强转符号
        2.2 擦除的是Code属性中的字节码,元数据池中还是保留了泛型信息
      3. 字节码生成
        3.1 实例构造器<init>()和类构造器<clinit>()代码收敛在这2个构造器中
        3.2 字符串用StringBuffer或者StringBuilder替换

    JIT编译器

    (HotSpot自带的JIT(server[开启C2]和client[开启C1])将*.class(byte code)转化为 机器码)关注程序运行

    • JIT(解释执行加编译执行)
      • client -- 减少启动时间和内存占用
      • server -- 提供更优秀的程序运行速度
    • 分层编译(共存模式下HotSpot使用分层编译的概念)
      • 0:解释执行,不开启性能监控
      • 1:C1编译,字节码便以为机器码,简单优化,加入必要的性能监控逻辑
      • 2:C2编译, 加入更多的优化

    AOT编译器(GCJ)

    (*.java -> 机器码

    Java代码在执行时一旦被编译器编译为机器码,下一次执行的时候就会直接执行编译后的代码. 编译后的代码被缓存在 method area 中。除了jit编译的代码之外,java所使用的本地方法代码(JNI)也会存在codeCache(method area)中。

    相关文章

      网友评论

        本文标题:ClassLoader加载器

        本文链接:https://www.haomeiwen.com/subject/eyjkrftx.html