JVM类加载机制

加载流程

  • 加载:从jar包或war包中找到、并加载类的二进制文件加载外部.class文件到jvm的方法区内。

  • 验证:验证可以被.class文件是否被加载,放置受到恶意代码攻击。该步骤在类加载过程占了很大一部分,不符合规范的将抛出java.lang.VerifyError错误。一些低版本JVM无法加载高版本类库,验证过程就是该阶段完成

  • 准备: 为一些类变量分配内存,并将其初始化为默认值。此时,实例对象还没有分配内存,所以这些动作实在方法区进行的。

    局部变量不像类变量存在准备阶段,类变量有两次初始值过程:
    1.一次在准备阶段,赋予初始值(不指定一般为0,也可以指定)
    2.另一次在初始化阶段,赋予程序员定义的值。
    
  • 解析
    类加载中非常重要的一环,是将符号引用替换为直接引用的过程。符号引用时一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针,相对偏移量。

    	负责把整个类激活,串成一个可以找到彼此的网,过程不可谓不重要。解析过程保证了相互引用的完整性,把继承和组合推进到运行时。大体分为如下几个步骤
    	类或接口的解析
    	类方法解析
    	接口方法解析
    	字段解析
    	解析异常捕获:
    	1.java.lang.NoSuchFieldError:根据继承关系从上往下,找不到相关字段时的错误
    	2.java.lang.IllegalAccessError:字段或者方法,访问权限不具备时的错误
    	3.java.lang.NoSuchMethodError找不到相关方法时的错误
    	````
    
    
  • 初始化

    	初始化成员变量
    
    • static语句块,只能访问到定义在static语句块之前的变量
      public class A {
      static int a = 0;
      static {
      a = 1;
      b = 1;
      }
      static int b = 0;
      public static void main(String[] args){
      System.out.println(a); //输出1
      System.out.println(b);// 输出0
      }
      }
      下述代码无法编译通过
      static {
      b = b+1;
      }
      static int b = 0;
      
    • jvm会保证在子类的初始化方法执行之前,父类的初始化方法执行完毕,jvm第一个被执行的类初始化方法一定是java.lang.Object,意味着父类中定义的static语句块优于子类的
      例子:类初始化<cinit>(仅执行一次静态代码块)与对象<init>(调用构造方法)
      public class A {
           static {
               System.out.println("1");
           }
           public A(){
               System.out.println("2");
               }
           }
       
           public class B extends A {
               static{
               System.out.println("a");
           }
           public B(){
               System.out.println("b");
           }
       
           public static void main(String[] args){
               A ab = new B();
               ab = new B();
      	      //输出结果
      	      /1 a 2 b 2 b
      	     // 1 a :java保证子类初始化方法执行之前,先执行父类初始化方法(静态代码块,指执行一次)
      	    // 2 b :java在进行子类实例化时,会先隐式调用父类构造器实例化,在调用子类构造器实例化
           }
       }
      
    • 对象初始化时,通常new一个新对象,都会调用它的构造方法,流程如下
      因此可以看出上面代码类初始化方法(static代码块)只执行一次,对象的构造方法执行两次。再加上继承关系的先后原则,分析出,先调用父类构造器,再调用子类构造器
      

类加载器

类加载器等级制度

  • Boostarp ClassLoader
     主加载器,任何类的加载都要经过他。它的作用是加载核心类库,即rt.jar,resource.jar.charset.jar。可以指定jar的路径,-Xboostclasspath参数可以完成完成指定操作。
    该加载器是C++编写,随着jvm启动
    

Extenion ClassLoader

  • 扩展加载器,主要加载lib/ext目录下的jar包和.class文件。同样的,通过系统变量java.ext.dirs指定该目录。
    该加载器继承自URLClassLoader

App ClassLoader

  • Java类的默认加载器,有时候也叫System ClassLoader。一般用来加载classpath下的其他jar包和.class文件,我们写的代码,会首先使用该加载器加载

Custom ClassLoader

  • 自定义加载器,支持一些个性化功能

双亲委派机制

  • 定义
    除了订餐的启动类加载器意外,其余的类加载器,在加载之前,都会委派给它的副加载器。这样一层层向上传递,知道祖先们无法胜任,它才真正加载。
    如孙子想买一个面包,最终都以经过爷爷过问,如果爷爷力所能及,就直接帮孙子卖了
    
  • 继承关系
    除了Boostrap 类,每个加载器都有一个parent,并无所谓双亲。
    
  • 类加载流程
    • 源码解析-ClassLoader 对应 loadClass方法
      它首先是要parent尝试类加载,parent失败后才轮到自己。并且该方法可以被覆盖,即双亲委派机制不一定生效。
      该模型好处在于Java类有了一种优先级的层次划分关系。比如Object类,这个毫无疑问应该交给最上层类进行加载,即使你覆盖了它,最终还是有系统默认的加载器进行加载。
      如果无双亲委派模型,就会出现很多个不停的Object类,应用会一片混乱。
      
    • 加载流程
      ClassLoader 对应 loadClass方法

自定义类加载器

tomcat

  • 差异
Tomcat通过war包进行应用发布,它其实是违反了双亲委派机制原则的
对一些需要加载的非基础类,其内部WebAppClassLoader会优先加载。等它加载不到的时候,再交由上层ClassLoader进行加载。其用来隔离不同应用.class文件(如有两个应用,可能会依赖不同第三方版本,他们相互无影响)
如果一个jvm运行两个不兼容版本,是需要自定义加载器来完成。
如果定义一个ArrayList,放在应用目录里,tomcat依然不会加载。他只是自定义的加载器顺序不同,但对顶层来说,是一样的,还是有BootstrapClassLoader加载。
  • Tomcat加载流程

SPI

  • 差异
java有一个SPI(Service Provider Interface)机制,是java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件
如数据库驱动加载,jdbc加载驱动类:Class.fromName(“com.mysql.jdbc.Driver”);
是一种初始化模式,通过static代码块显式声明了驱动对象,然后把这些信息,保存到底层list中。是一个接口编程的思路。
MySQL驱动代码中有一个services/java.sql.Driver里面是“com.mysql.cj.jdbc.Driver”通过在META-INF/services目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是SPI
  • java.util.ServiceLoader
SPI实际上是”基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用java.util.ServiceLoader类进行动态装载。
同样打破双亲委派机制,DriverManager类和ServiceLoader类都是属于rt.jar。其类加载器是BootstrapClassLoader,即最上层哪个。而具体数据库驱动,却属于业务代码,因此无法被Boostrap ClassLoader加载。如果凡事过问祖先,但祖先没有能力做,就需要进行特别处理
  • 源码分析
跟踪代码发现,其当前类加载器被设置成ContextClassLoader(上下文类加载器),对刚刚启动的应用程序来说,通过跟踪Launcher类,启动main方法的类加载器是AppClassLoader(应用类加载器)

源码如下:
1.加载驱动类
//part1:DriverManager::loadInitialDrivers
  //jdk1.8 之后,变成了lazy的ensureDriversInitialized
  ...
  ServiceLoader <Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  Iterator<Driver> driversIterator = loadedDrivers.iterator();
  ...
  
  //part2:ServiceLoader::load
  public static <T> ServiceLoader<T> load(Class<T> service) {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
  }
2.appClassLoader加载驱动类
public Launcher() {
 Launcher.ExtClassLoader var1;
 try {
     var1 = Launcher.ExtClassLoader.getExtClassLoader();
 } catch (IOException var10) {
     throw new InternalError("Could not create extension class loader", var10);
 }
 
 try {
     this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
 } catch (IOException var9) {
     throw new InternalError("Could not create application class loader", var9);
 }
 Thread.currentThread().setContextClassLoader(this.loader);
 ...
 }

(4)替换jdk类

  • 替换要扩展类
Java 原生API不能满足需求时,如要修改hashMap类,需要使用java endorsed技术。我们需要将自己的hashMap类,打包成一个jar包,然后放到 -Djava.endorsed.dir指定目录,类名和包名同jdk自带已有。
Java.lang包下的类除外,其都是特殊保护,无法被扩展
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值