JVM类加载


类加载

注:本文章基于JDK1.8

1.类的生命周期

在Java中一个类的生命周期为:加载、连接、初始化、使用、卸载5个步骤

加载阶段

在加载阶段会创建两个对象一个Java.lang.class和instanceKlass对象

  1. 第一步

    • 加载阶段第一步是类加载器根据类的全限定名(包名+类名)通过不同的渠道以二进制的方式获取字节码的信息
    • 程序员可以使用Java代码拓展不同的渠道,如磁盘上的字节码文件,动态代理生成(程序运行时使用动态代理生成)等
  2. 第二步

    • 类加载器加载完类之后,Java虚拟机会将字节码文件中的信息保存到方法区中
    • 生成一个InstanceKlass对象,保存类的所有信息,里面还包含特定功能比如多态的信息

    在这里插入图片描述

    • 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)

    在这里插入图片描述

    • 对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中的所有信息。这样Java虚拟机就能很好的控制开发者访问数据的范围,instanceKlass对象是使用C/C++编写的Java程序是无法直接访问的,而堆区的Java.lang.class,是把instanceKlass对象进行了封装,可以让Java程序直接访问,且只包含一些开发者会使用的到的信息,去除了一些不需要的信息。

      在这里插入图片描述

连接阶段

连接阶段又分为验证、准备、解析三个步骤

  1. 验证阶段

    • 这个环节主要的目的是验证Java字节码文件是否遵循了《Java虚拟机规范》中的约束,这个阶段一般不需要程序员参与。

    • 比如说验证字节码内容中的魔术也就是文件头是否Java字节码的文件头,以及主次版本号是否满足当前Java虚拟机的要求,主版本号不能高于运行环境主版本号,如果主版本号相等,副版本号也不能超过

    • 还有就是原信息验证,比如一个类必须有父类,也就是super不能为空

      在这里插入图片描述

    • 验证程序执行指令的语义,方法内的指令执行中是否跳转到不正确的位置,比如方法内的指令执行到一半强行跳转到其他方法中

    • 符号引用验证,例如是否访问了其它类中private的方法等

  2. 准备阶段

    public class Demo {
        public static int num = 10;
        public static final VALUE = 100;
    }
    
    • 准备阶段为静态变量(static)分配内存并设置赋初始值,静态变量是存在于堆区的class对象中,并为其赋默认值0,b比如说上诉中的num就会将其先赋值为0,注意这里并不是赋值为10,而是0。
    • 而被final修改的静态变量则会直接在准备阶段将其赋值为其设置的值,因为这个值是不会改变的,例如上述代码中的VALUE会被赋值为100。因为这个值被final修饰,编译就会认为这个值以后是不会被改变的
    • 为静态变量赋初值,是防止静态变量指向的内存地址中有残留的数据,为了避免出现随机值。

    而静态变量的初始值,处理boolean的默认值为false,char的默认值为'\u0000',引用类型为null,其它基本类都为0

  3. 解析阶段

    • 将常量池中的符号引用替换成指向内存的直接引用,也就是将编号索引直接替换成内存地址的引用
    • 符号引用就是字节码文件中使用编号来访问常量池中的内容
    • 直接引用不再使用编号,而是使用内存中地址进行访问具体的数据

初始化阶段

  • 初始化阶段会执行静态代码块中的代码,并为静态变量赋值
  • 初始化阶段执行字节码中clinit部分的字节码执行

一下几种方式会导致类初始化:

  1. 访问一个类的静态变量或者静态方法,需要注意的是变量是final修饰的并且等号右边是常量不会触发初始化
  2. 调用Class.forName(String className)
  3. new一个该类的对象时
  4. 执行Main方法的当前类(也就是执行main方法的类)

如下代码value的值为1,因为静态代码的执行顺序是和编写顺序一致的

public class Demo1 {
    static {
        value = 2;
    }
    public static int value = 1;

    public static void main(String[] args) {

    }
}

上面代码的字节码

在这里插入图片描述

如下代码:

public class Test {
    public static void main(String[] args) {
        System.out.println("A");
        new Test();
        new Test();
    }
    
    public Test() {
        System.out.println("B");
    }
    {
        System.out.println("C");
    }
    static {
        System.out.println("D");
    }
}

最后输出:DACBCB,在类初始化的时候会执行静态代码块static,而普通代码块代码最后编译后指令会放在构造方法中,也就是比构造方法先执行,最后再执行构造方法。

clinit指令在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的

  1. 无静态代码块并且无静态变量赋值语句
  2. 有静态变量的声明,但是没有赋值语句
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化

需要注意的是访问父类的静态变量,只初始化父类

下面代码输出1,因为访问父类的静态变量只会初始化父类

public class Demo {
    public static void main(String[] args) {
        System.out.println(B.a);
    }
}
class A {
    static int a = 0;
    static {
        a = 1;
    }
}
class B extends A {
    static {
        a = 2;
    }
} 

数组的创建不会导致数组中元素的类进行初始化,代码如下:创建这个Test数组并不会导致类初始化。也就是不会执行Test类中的静态代码块

public class Demo1 {

    public static void main(String[] args) {
        Test[] test = new Test[10];
    }
}

class Test{
    static {
        System.out.println("1_1");
    }
}

final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化,下面代码则会对Test类进行初始化,并执行静态代码块

public class Demo1 {

    public static void main(String[] args) {
        System.out.println(Test.a);
    }
}

class Test{
    public static final int a = Integer.valueOf(1);
    static {
        System.out.println("Test静态代码块");
    }
}     

2.类加载器

类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术,类加载器只参与加载过程中的字节码获取并加载到内存这一部分。

类加载器的分类

类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。

  • 虚拟机底层实现:源代码位于Java虚拟机的源码中,实现语言宇虚拟机底层语言一致,比如Hotspot使用C++实现
  • JDK中默认提供或者自定义:JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以自己根据需求定制

JDK8及以前的版本中默认的类加载器有如下几种

  • 虚拟机底层使用C++实现

    • 启动类加载器(Bootstrap):加载Java中最核心的类
  • Java代码实现

    • 扩展类加载器(Extension):允许拓展Java中比较通用的类
    • 应用程序类加载器(Application):加载应用使用的类
启动类加载器(Bootstarp)

启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供、使用C++编写的类加载器。

默认加载Java安装目录/jre/lib下的类文件,通过一下代码尝试获取到启动类加载器。下面的代码打印为null,因为启动类加载器底层是C++实现的,而Java是上层的代码,为了安全考虑Java代码中是或不到底层的类加载器的。

public static void main(String[] args) {
    ClassLoader classLoader = String.class.getClassLoader();
    System.out.println(classLoader);
}

所以当获取到的类加载器为空的时候,其实应该就是启动类加载器。

如果我们想扩展jar包使用启动类加载器进行加载,是不建议直接将jar包放入/jre/lib下的,这可能会导致放进去由于文件名不匹配的问题也不会正常地被加载,建议使用一下方法:

  • 使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展
扩展类加载器&应用程序加载器

扩展类加载器和应用程序类加载器都是JDK提供的,使用Java编写的类加载器。它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备通过目录或者指定jar包将字节码文件加载到内存中。

  • 扩展类加载器默认加载Java安装目录/jre/lib/ext下的类文件
  • 应用程序类加载器加载classpath下的类文件

在这里插入图片描述

  • URLClassLoader: 利用URL获取目录下或者指定的jar包进行加载,获取其字节码数据
  • SecureClassLoader:使用证书机制提升类加载的安全性
  • ClassLoader:抽象类,定义了类加载器的具体行为模式,通过JNI调用底层的Java虚拟机方法

3.类的双亲委派机制

什么是双亲委派机制?

由于Java虚拟机中有多个类加载器,双亲委派机制的核心就是解决一个类到底由谁加载的问题。

双亲委派机制有什么作用?

  1. 保证类加载的安全性:通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性安全性
  2. 避免重复加载:双亲委派机制避免同一个类被多次加载

在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器 。如果一个类加载器 的 parent为null,则会提交给启动类加载器加载

每个Java实现的类加载器中保存了一个成员变量叫parent的双亲加载器,,可以理解为它的上级,并不是继承关系。

在这里插入图片描述

  • 应用程序类加载器的parent父类加载器是扩展类的加载器,而扩展类加载器的parent是空的
  • 启动类加载器是使用C++编写,没有上级类加载器

在这里插入图片描述

双亲委派机制指的是:自底向上查找是否加载过,再由顶向下进行加载

  • 当程序需要进行加载时,首先是从应用程序加载器检查这个类是否被加载,如果没有就会让拓展类加载器进行检查,依此类推,如果这个类被加载过就会直接返回
  • 启动类加载器检查玩这个类也没有被加载时,就会判断这个类是否在它的加载列表中,如果是就直击加载,如果不存在就让下一级检测是否能被加载
  • 如果在加载过程中这3个类加载器都无法去加载这个类,最后就会出现一个类无法找到的错误

小结:

  1. 当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载
  2. 应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加
    载器。
  3. 双亲委派机制的好处有两点: 第一是避免恶意代码替换JDK中的核心类库,比java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载

打破双亲委派机制

自定义类加载器

在这里插入图片描述

  • 一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,
    Tomcat要保证这两个类都能加载并且它们应该是不同的类。
  • 如果不打破双亲委派机制, 当应用类加载器加载Web应用1中的MyServlet之后, Web应用2中相同限定名的MyServlet类就无法被加载了。
  • Tomcat使用了自定义类加载器来实现应用之间类的隔离,每一个应用会有一个独立的类加载器加载对应的类。

在这里插入图片描述

  • 先来分析ClassLoader的原理,ClassLoader中包含了4个核心方法。
  • 双亲委派机制的核心代码就位于loadClass方法中
//类加载的入口,提供了双亲委派机制。内部会调用 findClass
public Class<?> loadClass(String name)
// 由类加载器子类实现,获取二进制数据调用defineClass ,比如URLClassLoader会根据文件路径去获取类文件中的二进制数据
protected Class<?> findClass(String name)
// 做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
protected final Class<?> defineClass(String name,byte[] b,int off, int len)
//执行类生命周期中的连接阶段
protected final void resolveClass(Class<?> c)

来看一下loadClass方法的源码,首先是执行入口方法的内部调用的其重载的loadClass方法,传递了一个false参数,该参数表示 是否执行连接这个过程,默认为false也就是不会执行连接这个过程

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

接下来看一下调用的重载方法

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    	// 类加载之前先加锁
        synchronized (getClassLoadingLock(name)) {
            // 检测当前类是否已经被加载过,如果被加载过,就直接返回Class对象
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	// 判断父类加载器是否为空
                    if (parent != null) {
                    	// 如果父类加载器不为空,就会由父类加载器去调用对应的loadClass方法
                        c = parent.loadClass(name, false);
                    } else {
                    	// 如果parent为空说明此时是扩展类加载器,那么就会调用启动类加载器来进行加载,也就是Bootstrap
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

				// 如果此时还为空,说明该类此时并没有被加载过,就会被当前类加载器进行加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

通过上述的源代码来看,其实双亲委派的核心代码就是这么一段:

// 判断父类加载器是否为空
if (parent != null) {
    // 如果父类加载器不为空,就会由父类加载器去调用对应的loadClass方法
    c = parent.loadClass(name, false);
} else {
    // 如果parent为空说明此时是扩展类加载器,那么就会调用启动类加载器来进行加载,也就是Bootstrap
    c = findBootstrapClassOrNull(name);
}

// 如果此时还为空,说明该类此时并没有被加载过,就会被当前类加载器进行加载
if (c == null) {
    c = findClass(name)
}

所以想要打破双亲委派机制,就可以实现一个自定义的类加载器,也就是 自定义一个类继承ClassLoader,并重写loadClass方法,将其的双亲委派代码给去除掉,这样就打破了双亲委派机制

如果只是实现一个自定义的类加载器,而不需要打破双亲委派机制,就可以重写findClass方法,因为一个类在加载的时候首先是由其加载器的父类加载器进行加载,当父类没有加载时,最后则会调用findClass方法来进行加载,我们就可以重写findClass方法来进行自定义加载,而且不会 打破双亲委派机制

注意:如果一个自定义类加载器不知道父类加载器,默认的父类加载器就是应用程序类加载器

在这里插入图片描述

两个自定义类加载器加载相同限定名的类,是不会冲突的。在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。

线程上下文类加载器

在JDBC中,不希望在代码中出现某一种数据库的语法,他要提高它的通用性,让其对接任何数据库都非常方便。在JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,使用不同的数据库,只需要添加对应的数据库驱动即可。DriverManager会把jar包中对应的驱动加载进来,如MySQL驱动。

DriverManager类位于rt.jar包中,由启动类加载器加载。 而用户jar包中的MySQL驱动需要由应用程序类加载器进行加载,就违反了双亲委派机制。

DriverManager类会先由启动类加载器进行加载后,启动类加载器会委派应用程序类加载器去加载Jar包中的MySQL驱动,而正常情况是由下向上委派,而且启动类加载器比应用程序类加载器高的多,这样就打破了双亲委派机制。

在这里插入图片描述

DriverManager怎么知道jar包中要加载的驱动在哪儿?

  • SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,如果你想去加载一个接口的实现类对象,就可以通过SPI快速的去找到
  • SPI的工作原理:
    • 在ClassPath路径下META-INF/services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写接口的实现,
    • 使用ServiceLoader加载实现类
    • 整个SPI机制分为两部分,第一部分需要在驱动的Jar中,去暴露出你要让加载器去加载哪个类放到一个固定的文件中,接下来在DriverManage代码中就会主动去使用ServiceLoader去加载文件中的类名,并且使用类加载器去加载对应的类并创建对象。
    • 通过迭代器去遍历找到Jar包符合条件的类加载并创建对象
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

SPI中是如何获取到应用程序类加载器的?

SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器 ,源代码如下

public static <S> ServiceLoader<S> load(Class<S> service) {
    //获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
  1. 启动类加载器加载DriverManager。
  2. 在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
  3. SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
  4. 这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制

那么JDBC真的打破了双亲委派机制吗?

在这里插入图片描述

  • 首先DriverManager这个类位于rt.jar中,是由启动类加载器来进行加载的,是满足双亲委派机制的
  • 而在Jar包中的MySQL驱动,这些类 位于ClassPath会由应用程序类加载器来进行加载,而应用程序类加载器也是满足双亲委派机制的,首先他会一层一层的向上查找,然后再向下委派,由于这个驱动没有被加载过,由于路径不满足要求,启动类加载器和拓展类加载器都不能加载最后还是有应用程序类加载器,也是满足要求
  • 并且启动也没有重写loadClass方法,重写loadClass方法才会 打破双亲 委派机制

JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制,所以并没有打破双亲委派机制


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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱敲代码的三毛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值