双亲委派机制

双亲委派机制

类的生命周期

类的生命周期可以划分为7个阶段

加载-->验证-->准备--> 解析-->初始化-->使用-->卸载

加载:

在类加载的第一个阶段,JVM 的主要目的是将字节码从磁盘转化为二进制的字节流加载到内存中,接着会在JVM 的方法区创建一个对应的class对象这个class对象就是这个类的各个数据的访问的入口;

JVM 加载 Class 字节码文件到内存中,并在方法区创建对应的 Class 对象

加载阶段主要完成以下三件事

  1. 通过一个类的全限定名来获取其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

验证:

验证是链接阶段的第一步,它是确保被加载的类的正确性.

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段大致会完成4个阶段的检验动作:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备:

准备阶段所做的工作是为类的静态变量分配内存(注意是静态变量),这些内存都将在方法区中分配, 并将其初始化为默认值.

对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  2. 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

举个栗子:

//假设现在一个类加载完成处于准备阶段---且存在一个变量
public static int age = 18;
//注意在准备阶段变量age的值会被赋值为0.而在初始化阶段之后才会被赋值成为18
  1. 对基本数据类型,类变量(static)和全局变量来说,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  2. 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过
  3. 而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值(即构造函数),总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  4. 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  5. 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
  6. 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

举个栗子

//假设现在一个类加载完成处于准备阶段---且存在一个变量
public static  final int age = 18;
//编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将age赋值为18。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中

解析

解析阶段做的工作是把类中的符号引用转换为直接引用

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。

在Java中对类变量进行初始值设定有两种方式:

  1. 声明类变量时指定初始值
  2. 使用静态代码块为类变量指定初始值
JVM初始化步骤
  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句
类的初始化时机

只有当对类的主动使用的时候才会导致类的初始化,注意,这和类加载不同. 类的主动使用包括以下六种:

  1. 创建类的实例,也就是new的方式
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(如Class.forName(“com.shengsiyuan.Test”))
  5. 初始化某个类的子类,则其父类也会被初始化
  6. Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类。

结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

  1. 执行了System.exit()方法
  2. 程序正常执行结束
  3. 程序在执行过程中遇到了异常或错误而异常终止
  4. 由于操作系统出现错误而导致Java虚拟机进程终止

什么是类加载器

通过一个类全限定名称来获取其二进制文件(.class)流的工具,被称为类加载器(classloader)。


如上图所示,Java 支持 4 种 classloader

  1. 启动类加载器(

    Bootstrap ClassLoader
    

    • 用于加载 Java 的核心类
    • 它不是一个 Java 类,是由底层的 C++ 实现。因此,启动类加载器不属于 Java 类库,无法被 Java 程序直接引用。Bootstrap ClassLoaderparent 属性为 null
  2. 标准扩展类加载器(

    Extention ClassLoader
    

    )

    • sun.misc.Launcher$ExtClassLoader 实现
    • 负责加载 JAVA_HOMElibext 目录下的或者被 java.ext.dirs 系统变量所指定的路径中的所有类库
  3. 应用类加载器(

    Application ClassLoader
    

    )

    • sun.misc.Launcher$AppClassLoader 实现
    • 负责在 JVM 启动时加载用户类路径上的指定类库
  4. 用户自定义类加载器(

    User ClassLoader
    

    )

    • 当上述 3 种类加载器不能满足开发需求时,用户可以自定义加载器
    • 自定义类加载器时,需要继承 java.lang.ClassLoader 类。如果不想打破双亲委派模型,那么只需要重写 findClass 方法即可;如果想打破双亲委派模型,则需要重写 loadClass 方法

前 3 种 classloader 均继承了抽象类 ClassLoader,其源码如下,该抽象类拥有一个 parent 属性,用于指定其父类的加载器。

public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
    
    // ...

    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

可以通过下面这种方式,打印加载路径及相关 jar。

System.out.println("boot:" + System.getProperty("sun.boot.class.path"));
System.out.println("ext:" + System.getProperty("java.ext.dirs"));
System.out.println("app:" + System.getProperty("java.class.path"));

自定义类加载器

此处给出一个自定义类加载器示例。

package com.lbs0912.java.demo;

import java.io.IOException;
import java.io.InputStream;

public class ConsumerClassLoaderDemo extends ClassLoader {

    public static void main(String[] args) throws Exception {

        ClassLoader myClassLoader = new ConsumerClassLoader();
        Object obj = myClassLoader.loadClass("com.lbs0912.java.demo.ConsumerClassLoaderDemo").newInstance();
        ClassLoader classLoader = obj.getClass().getClassLoader();
        // BootStrapClassLoader在Java中不存在的,因此会是null
        while (null != classLoader) {
            System.out.println(classLoader);
            classLoader = classLoader.getParent();
        }
    }
}

class ConsumerClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String classFile = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream in = getClass().getResourceAsStream(classFile);
            if (null == in) {
                return super.loadClass(name);
            }
            byte[] bytes = new byte[in.available()];
            in.read(bytes);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
}

类加载机制的特点

「类加载机制」中,通过「类加载器(classloader)」来完成类加载的过程。Java 中的类加载机制,有如下 3 个特点

  1. 双亲委派
    • JVM 中,类加载器默认使用双亲委派原则
  2. 负责依赖
    • 如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。
  3. 缓存加载
    • 为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。

双亲委派

双亲委派机制是一种任务委派模式,是 Java 中通过加载工具(classloader)加载类文件的一种具体方式。 具体表现为

  1. 如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行。
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器 BootstrapClassLoader
  3. 如果父类加载器可以完成类加载任务,就成功返回;倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载。
  4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类;如果将加载任务分配至系统类加载器(AppClassLoader)也无法加载此类,则抛出异常。

关于官网的说明

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself. —— Oracel Document

Java 平台通过委派模型去加载类。每个类加载器都有一个父加载器。当需要加载类时,会优先委派当前所在的类的加载器的父加载器去加载这个类。如果父加载器无法加载到这个类时,再尝试在当前所在的类的加载器中加载这个类。

参考上述 Oracle 官网文档描述,Java 的类加载机制,更准确的说,应该叫做 “父委派模型”。但由于翻译问题,被称为了 “双亲委派机制”

双亲

classloader 类存在一个 parent 属性,可以配置双亲属性。默认情况下,JDK 中设置如下。

ExtClassLoader.parent=null;

AppClassLoader.parent=ExtClassLoader

//自定义
XxxClassLoader.parent=AppClassLoader

需要注意的是,启动类加载器(BootstrapClassLoader)不是一个 Java 类,它是由底层的 C++ 实现,因此启动类加载器不属于 Java 类库,无法被 Java 程序直接引用,所以 ExtClassLoader.parent=null;

委派

双亲设置之后,便可以委派了。委派过程也就是类文件加载过程。

ClassLoader 里面有 3 个重要的方法,即

  1. loadClass()
  2. findClass()
  3. defineClass()

实现双亲委派的代码都集中在 java.lang.ClassLoaderloadClass() 方法中

public abstract class ClassLoader {
    // 委派的父类加载器
    private final ClassLoader parent;

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


    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 保证该类只加载一次
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查该类是否被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                    	//父类加载器不为空,则用该父类加载器
                        c = parent.loadClass(name, false);
                    } else {
                    	//若父类加载器为空,则使用启动类加载器作为父类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //若父类加载器抛出ClassNotFoundException ,
                    //则说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    //父类加载器无法完成加载请求时
                    //调用自身的findClass()方法进行类加载
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

双亲委派的优点

一句话总结,双亲委派可以保证一个类不会被多个类加载器重复加载,并且保证核心 API 不会被篡改。

沙箱安全机制:

比如用户自己定义的String.class 不会被加载,防止核心库被随意篡改!

避免类的重复加载:当前ClassLoader加载了这个类,就不需要子类去加载了

本文作者:鸽宗

本文链接:https://www.cnblogs.com/xuzhidong/p/16860396.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   鸽宗  阅读(438)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
  1. 1 水星记 REOL
水星记 - REOL
00:00 / 00:00
An audio error has occurred.

作词 : 郭顶

作曲 : 郭顶

编曲 : 郭顶

监制 : 郭顶

着迷于你眼睛

银河有迹可循

穿过时间的缝隙

它依然真实地

吸引我轨迹

这瞬眼的光景

最亲密的距离

沿着你皮肤纹理

走过曲折手臂

做个梦给你

做个梦给你

等到看你银色满际

等到分不清季节更替

才敢说沉溺

还要多远才能进入你的心

还要多久才能和你接近

咫尺远近却

无法靠近的那个人

也等着和你相遇

环游的行星

怎么可以

拥有你

这瞬眼的光景

最亲密的距离

沿着你皮肤纹理

走过曲折手臂

做个梦给你

做个梦给你

等到看你银色满际

等到分不清季节更替

才敢说沉溺

还要多远才能进入你的心

还要多久才能和你接近

咫尺远近却

无法靠近的那个人

也等着和你相遇

环游的行星

怎么可以

拥有你

还要多远才能进入你的心

还要多久才能和你接近

咫尺远近却无法靠近的那个人

要怎么探寻

要多么幸运

才敢让你发觉你并不孤寂

当我还可以再跟你飞行

环游是无趣

至少可以

陪着你