JVM之类加载子系统

类加载子系统

Java程序的执行过程:

  1. 编写源程序 .java
  2. 使用javac 命令将.java文件编译为 .class字节码文件
  3. 使用Java命令使用.class 文件

简略过程如下图所示:

在这里插入图片描述

类加载子系统:

类加载子系统的作用:

  1. 类加载子系统负责从文件或者网络中加载class 文件,class文件在文件开头有特定的文件标识(魔数, 在jvm虚拟机中的字节码文件使用十六进制0xCAFEBABE(咖啡宝贝)来标识)
  2. ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
  3. 加载的类信息存放于一块成为方法区的内存空间(不同的jvm实现有不同的叫法,在这里统称为方法区)。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)

类加载器ClassLoader的角色

  1. 我们在编写玩Java源程序并编译成class文件后,class文件会存放于本地硬盘上,当真正运行程序时,这个class文件会被加载到jvm当中,我们就可以使用这个.class实例化出n个一摸一样的实例。

  2. class file 加载到JVM中,被成为NDA元数据模板,存放在方法区。

  3. 在.class文件 -> JVM -> 最终成为元数据模板,此过程就需要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。

    简明的说ClassLoader就是将.class文件从硬盘搬运到jvm中的搬运工

类的加载过程:

在这里插入图片描述

加载(loading):
  1. 通过一个类的全限定类名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表整个类的java.lang.Class对象,作为方法区整个类的各种数据的访问入口
类的加载方式:
  1. 从本地系统中直接加载
  2. 通过网络获取,典型场景: Web Applet
  3. 从Zip压缩包中读取, 成为日后jar、war格式的基础
  4. 运行时计算生成,使用最多的:动态代理
  5. 由其他文件生成,典型场景: JSP
  6. 从装有数据库中提取.class文件,比较少见
  7. 从加密文件中获取,典型的防Class文件被反编译的保护措施
链接(Linking):
验证:

目的在于确保Class文件的字节流中包含的信息符合当前虚拟机要求,保证被加载的类的正确性

主要包括四种验证: 文件格式验证,元数据验证,字节码验证,符号引用验证

准备:

为类变量分贝内存并且设置该类变量的默认初始值,即零值

这里不包含final修饰的static,因为final在编译的时候就会分配,准备阶段会显示初始化。

这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

解析:

将常量池内的符号引用转换为直接引用的过程

事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。

符号引用就是一组和好来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个简介定位到目标的句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

初始化阶段:

初始化阶段就是执行类构造器方法“()”的过程

此方法不需要定义,是Javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

构造方法中指令按语句在源文件中出现的顺序执行。

<clinit>不同于类的构造器。(关联: 构造器是虚拟机视角下的<init>

若该类具有父类,JVM会保证子类的<clinit>方法执行前,父类的<clinit>已经执行完毕。

虚拟机必须保证一个类的<clinit>方法在多线程下被同步枷锁

类加载器分类:

狭义的分类:

Bootstrap ClassLoader(类引导加载器)

Extension ClassLoader(扩展类加载器)

System ClassLoader(系统类加载器, 也叫应用加载器(AppClassLoader))

在这里插入图片描述

1.启动类加载器(虚拟机自带的类加载器)
  • 这个类加载使用c/c++语言实现,嵌套在JVM内部
  • 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或者sun.boot.class.path路径下的内容),用于提供JVM本身需要的类
  • 并不继承自Java.lang.ClassLoader,没有父加载器。
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包为Java、javax、sun等开头的类
2.扩展类加载器(Extension ClassLoader类)
  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于java.lang.ClassLoader类
  • 父类加载器为启动类加载器
  • 从Java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的按爪个目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
3.应用程序类加载器(系统类加载器,AppClassLoader)
  • Java语言编写, 由sun.misc.Launcher$AppClassLoader实现
  • 派生于java.lang.ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性 java.class.path指定路径下 类库。
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类的加载器。
4.自定义类加载器
  • 在Java的日常应用程序开发中,类的加载几乎是由上述三种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
  • 为什么要自定义类的加载器。
    1. 隔离加载类
    2. 修改类加载的方式
    3. 扩展加载资源
    4. 防止源码泄露
5.用户自定义类加载器实现步骤:
  1. ​ 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。
  2. 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类的加载,但是在JDK1.2之后已不再推荐用户覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
  3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLCLassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码的方式,使自定义类加载器编写更简洁。
关于ClassLoader

在这里插入图片描述

sun.misc.Launcher它是一个Java虚拟机的入口应用

获取ClassLoader的方式

一下代码都是伪代码:

public ClassLoaderTest{
    public static void main(String[] args){
        // 方式1
        classzz.getClassLoader()
        // 方式2
        Thread.currentThread().getContextClassLoader()
        // 方式3
        ClassLoader.getSystemClassLoader()
        // 方式4
        DriverManager.getCallerClassLoader()
    }
}

双亲委派机制

简述:

Java虚拟机对class文件采取的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采取的就是双亲委派模式,即把请求由父类处理,它是一种任务委派模式。

工作原理:

  1. 如果一个类加载器收到了类加载请求,它并不会自己去加载,而是把这个请求委托给父亲的加载器去执行;
  2. 如果父亲加载器还存在其父类加载器,则进一步线上委托,一次递归,请求最终将达到顶层的启动类加载器;
  3. 如果类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此任务,子类加载器才会尝试自己去加载,这就是双亲委派机制。
源代码证明上述(写了必要的注释):

java.lang.ClassLoader#loadClass(String name, boolean resolve)方法

 /**
     * Loads the class with the specified <a href="#name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * <ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     *
     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
     *   on the parent class loader.  If the parent is <tt>null</tt> the class
     *   loader built-in to the virtual machine is used, instead.  </p></li>
     *
     *   <li><p> Invoke the {@link #findClass(String)} method to find the
     *   class.  </p></li>
     *
     * </ol>
     *
     * <p> If the class was found using the above steps, and the
     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
     *
     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     *
     * <p> Unless overridden, this method synchronizes on the result of
     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
     * during the entire class loading process.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @param  resolve
     *         If <tt>true</tt> then resolve the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     */
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{
        // 获取当前的classLoader对象为锁对象
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 检查此name(全限定类名)的类是否已经被加载
            Class<?> c = findLoadedClass(name);
            // 如果为空
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 查看是否存在父加载器
                    // 如果存在父加载器, 那么使用父加载器去加载
                    // 如果不存在父加载器,那么使用引导类加载器加载(bootstrapClassLoader)
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				// 如果使用无法加载,那么抛出ClassNotFoundException()
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 此方法没有具体逻辑,只有异常处理,抛出ClassNotFoundException
                    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;
        }
    }
举个栗子:

在这里插入图片描述

  1. 线程上下文类加载器(ContextClassLoader)需要加载jdbc.jar, 它委派给父加载器(AppClassLoader)----> ExtClassLoader ----> Bootstrap ClassLoader加载rt.jar中的SPI核心类,其中包含了jdbc的接口
  2. 此时没有jdbc的实现类,那么再反向委托到ContextClassLoader中, -----> AppClassLoader加载JDBC实现类。
优势:
  1. 避免类的重复加载
  2. 保护程序的安全,防止核心API被随意篡改
  • 自定义的java.lang.String
  • 自定义的java.lang.xxxx

解释一下第二条,因为双亲委派机制的存在任何类的加载都会被向上委派,核心类库中的String已经被加载了,所以自定义的类就无法被加载。

其他
  • 再JVM中表示两个Class对象是否为同一个类存在的两个必要条件
    • 类的完整类名必须一致,包括类名
    • 加载这个类的ClassLoader必须相同
  • 换句话说,再JVM中,即使这两个类对象来自同一个class文件,被同一个虚拟机所加载,但只要加载他们的ClassLoader实例对象不同,那么这两个类对象也是不相等的
对类加载器的引用

JVM必须知道一个类型是由启动类加载器(bootstrapClassLoader)加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

以上内容均来自宋红康老师的JVM教程,在此感谢康老师。
我只是想写个笔记,发不出来,我也是醉了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值