java类加载机制

源代码 2024-9-29 07:08:19 65 0 来自 中国
java代码执行流程


根据上图所示,Java 代码执行步调如下:
步调 1: 获取 Java 源代码;
步调 2: 编译器把 java 文件变化成 class 文件。编译过程大抵可以分为 1 个准备过程和 3 个处理处罚过程:
准备过程:初始化插入式注解处理处罚器
剖析与添补符号表过程,包罗:词法、语法分析等
注解处理处罚过程
语义分析与字节码生成过程
步调 3: 若要运行此 Java 步伐,JVM 中会有一个叫类加载器(class loader)的内置步伐把字节码从硬盘载入 JVM;
步调 4: JVM 中尚有一个叫字节码校验器(bytecode verifier)的内置步伐检测是否存在运行期错误(比方栈溢出)。若通过检测,字节码校验器就会将字节码通报给表明器(interpreter);
步调 5: 表明器会对字节码举行逐行翻译,将其翻译成当前所在体系可以理解的呆板码(machine code);
步调 6:将呆板码交给操纵体系,操纵体系会以 main 方法作为入口开始执行步伐。至此,一个Java步伐就如许运行起来了。
类的生命周期

一个类在 JVM 里的生命周期有 7 个阶段,分别是加载(Loading)、验证(Verification)、准备(Preparation)、剖析(Resolution)、初始化(Initialization)、利用(Using)、卸载(Unloading)。

2.png
此中前五个部门(加载,验证,准备,剖析,初始化)统称为类加载,下面我们就具体先容一下这五个过程。
加载
加载(Loading)阶段是整个「类加载」(Class Loading)过程中的一个阶段,各位不要肴杂。
加载的告急作用是将外部的 .class 文件,加载到 Java 的方法区内。
这个阶段 JVM 须要完成以下三个操纵:

  • 通过一个类的全限定名(包名 + 类名)来获取界说此类的二进制字节省;
  • 将这个字节省所代表的静态存储布局转化为方法区的运行时数据布局;
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
    加载 class 文件有以下几种方式:
  • 从 ZIP 压缩包中读取,这很常见,成为日后 jar、war格式的底子;
  • 通过网络获取,范例场景:Web Applet;
  • 运行时盘算生成,利用最多的是:动态署理技术;
  • 由其他文件生成,范例场景是 JSP 应用;
  • 从加密文件中获取,范例的防 Class 文件被反编译的掩护措施。
验证

验证是毗连阶段的第一步,这一阶段的目的是确保 class 文件里的字节省信息符合当前捏造机的要求,不会危害捏造机的安全。
从代码量和泯灭的执行性能的角度上讲,验证阶段的工作量在捏造机的类加载过程中占了相称大的比重。
假如输入的字节省如不符合 Class 文件格式的束缚,将抛出一个 java.lang.VerifyError 非常或其子类非常。
从团体上看,验证阶段大抵会完成如下四个阶段的查验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

  • 文件格式验证:告急验证字节省是否符合 Class 文件格式的规范,并且能被当前版本的捏造机处理处罚。比如:


  • 是否以魔数 0xCAFEBABE 开头;
  • 主、次版本号是否在当前 Java 捏造机担当范围之内;
  • 常量池的常量中是否有不被支持的常量范例(查抄常量 tag 标志)

  • 元数据验证:是对字节码形貌的信息举行语义分析,以包管其形貌的信息符合《Java语言规范》的要求。比如:


  • 这个类的父类是否继续了不允许被继续的类(被 final 修饰的类)
  • 假如这个类不是抽象类,是否实现了其父类或接口之中要求实现的全部方法
  • 类中的字段、方法是否与父类产生抵牾(比方覆盖了父类的 final 字段,大概出现不符合规则的方法重载,比方方法参数都划一,但返回值范例却差异等)

  • 字节码验证:这一阶段是整个验证过程中最复杂的一个阶段,告急目的是通过数据流分析和控制流分析,确定步伐语义是合法的、符合逻辑的;
  • 符号引用验证:末了一个阶段的校验行为发生在捏造机将符号引用转化为直接引用的时间,这个转化动作将在 毗连的第三阶段——剖析阶段中发生。
准备

准备阶段是为界说的类变量(即静态变量,被 static 修饰的变量)分配内存并初始化为尺度默认值(比如 null 大概0 值)。
从概念上讲,这些变量所利用的内存都应当在方法区中举行分配,但必须注意到方法区 本身是一个逻辑上的地域,在JDK 7及之前,HotSpot利用永世代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时间“类变量在方法区”就完满是一种对逻辑概念的表述了。
准备阶段,有两个关键点须要注意:

  • 起首是这时间举行内存分配的仅包罗类变量,而不包罗实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中;
  • 然后就是初始化为尺度默认值。
假设一个类变量的界说如下:
public static int value = 123;在准备阶段的值会被初始化为 0,背面在类初始化阶段才会执行赋值为 123;但是下面假如利用 final 修饰静态常量,某些 JVM 的行为就不一样了。
假设上面类变量 value 的界说修改为:
public static final int value = 123;编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段捏造机就会根据 ConstantValue 的设置将 value 赋值为 123。
假如类字段的字段属性表中存在 ConstantValue 属性,那在准备阶段变量值就会被初始化为 ConstantValue 属性所指定的初始值。<<深入理解Java捏造机>>
剖析

剖析阶段是 Java 捏造机将常量池内的符号引用更换为直接引用的过程。
先容剖析之前,我们简朴相识一下符号引用直接引用

  • 符号引用是一种界说,可以是任何字面上的寄义,而直接引用就是直接指向目的的指针、相对偏移量。
  • 直接引用的对象都存在于内存中,你可以把通讯录里的女友手机号码,类比为符号引用,把面临面和你用饭的人,类比为直接引用。
简朴的来说就是我们编写的代码中,当一个变量引用某个对象的时间,这个引用在 .class 文件中是以符号引用来存储的(相称于做了一个索引记载)。
在剖析阶段就须要将其剖析并链接为直接引用(相称于指向实际对象)。假如有了直接引用,那引用的目的肯定在堆中存在。
加载一个 class 时, 须要加载全部的 super 类和 super 接口。
个人理解,在编译的时间一个每个 java 类都会被编译成一个 class 文件,但在编译的时间,被引用的类、方法大概变量还没有被加载到内存中,捏造机并不知道所引用类的地点,以是就用符号(比如com.example.Test)引用来取代,而在这个剖析阶段就是为了把这个符号引用转化成为真正的地点的阶段。
剖析阶段负责把整个类激活,串成一个可以找到相互的网,过程不可谓不告急。那这个阶段都做了哪些工作呢?
告急针对类或接口、字段、类方法、接口方法、方法范例、方法句柄和调用点限定符 7 类符号引用举行。
我们来看几个常常发生的非常,就与这个阶段有关。

  • java.lang.NoSuchFieldError 根据继续关系从下往上,找不到相关字段时的报错。
  • java.lang.IllegalAccessError 字段大概方法,访问权限不具备时的错误。
  • java.lang.NoSuchMethodError 找不到相关方法时的错误。
初始化

类的初始化阶段是类加载过程的末了一个步调。初始化阶段就是执行类构造器 <clinit>() 方法的过程。
<clinit>()方法是由编译器自动收集类中的全部类变量赋值动作和静态语句块(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);         System.out.println(b);     } }运行结果
10a 和 b 唯一的区别就是它们的 static 代码块的位置。
这就引出一个规则:static 语句块,只能访问到界说在 static 语句块之前的变量。
以是下面的代码是无法通过编译的。
static {   b = b + 1;}static int b = 0;规则二:Java 捏造机遇包管在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法已经执行完毕。
因此在 Java 捏造机中第一个被执行的 <clinit>()方法的范例肯定是 java.lang.Object。正因云云,下面的代码字段 B 的值将会是 2 而不是 1。
public class Parent {    public static int A = 1;    static {        A = 2;    }    static class Sub extends Parent{        public static int B = A;    }    public static void main(String[] args) {        System.out.println(Sub.B);    }}clinit 和 init 方法
<clinit> 是类(Class)初始化执行的方法,<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();     } }打印结果
1a2b2b此中 static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区,在同一个类加载器下,这些信息有一份就够了,以是上面的 static 代码块只会执行一次,它对应的是 <clinit> 方法。
而对象初始化就不一样了。
通常,我们在 new 一个新对象的时间,都会调用它的构造方法,就是 <init>,用来初始化对象的属性。每次新建对象的时间,都会执行。

以是,上面代码的 static 代码块只会执行一次,对象的构造方法执行两次。再加上继续关系的先后原则,不难分析出正确结果。
类加载的机遇

关于在什么环境下须要开始类加载过程的第一个阶段「加载」,JVM 规范中并没有举行逼迫束缚,但是对于初始化阶段,JVM 规范规定了只有六种环境必须立即对类举行「初始化」(加载、验证、准备天然须要在此之前开始):

  • 碰到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,假如范例没有举行过初始化,则须要先触发其初始化阶段。可以大概生成这四条指令的范例 Java 代码场景有:


  • 利用 new 关键字实例化对象的时间;
  • 读取或设置一个范例的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时间;
  • 调用一个范例的静态方法的时间;

  • 利用 java.lang.reflect 包的方法对范例举行反射调用的时间,假如范例没有举行过初始化,则须要先触发其初始化;
  • 当初始化类的时间,假如发现其父类还没有举行过初始化,则须要先触发其父类的初始化;
  • 当捏造机启动时,用户须要指定一个要执行的主类(包罗 main() 方法的谁人类),捏造机遇先初始化这个主类;
  • 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类;
  • 当一个接口中界说了 JDK 8 新参加的默认方法(被 default 关键字修饰的接口方法)时,假如有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
这六种会触发范例举行初始化的场景,JVM 规范中利用了一个非常猛烈的限定语:有且只有,这六种场景中的行为称为对一个范例举行自动引用。
除此之外,全部引用范例的方式都不会触发初始化,称为被动引用。
被动引用

我们举三种被动引用的例子:
实例1:通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
/** * 通过子类引用父类的静态字段,不会导致子类初始化 **/public class SuperClass {    static {        System.out.println("SuperClass init!");    }    public static int value = 123;}public class SubClass extends SuperClass {    static {        System.out.println("SubClass init!");    }}public class NotInitialization {    public static void main(String[] args) {        System.out.println(SubClass.value);    }}运行结果
SuperClass init!123上述代码运行之后,只会输出SuperClass init!,而不会输出SubClass init!。
对于静态字段,只有直接界说这个字段的类才会被初始化,因此通过其子类来引用父类中界说的静态字段,只会触发父类的初始化而不会触发子类的初始化。
实例2:通过数组界说来引用类,不会触发此类的初始化
public class NotInitialization {    public static void main(String[] args) {        SuperClass[] sca = new SuperClass[10];    }}运行之后发现没有输出 SuperClass init!,分析并没有触发类 SuperClass 的初始化阶段。
实例3:常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到界说常量的类,因此不会触发界说常量的类的初始化
public class ConstClass {    static {        System.out.println("ConstClass init!");    }    public static final String HELLOWORLD = "hello world";}public class NotInitialization {    public static void main(String[] args) {        System.out.println(ConstClass.HELLOWORLD);    }}运行结果:
hello world上述代码运行之后,也没有输出ConstClass init!,这是由于固然在Java源码中确实引用了 ConstClass 类的常量 HELLOWORLD,但着实在编译阶段通过常量传播优化,已经将此常量的值 hello world 直接存储在 NotInitialization 类的常量池中,以后 NotInitialization 对常量 ConstClass.HELLOWORLD 的引用,实际都被转化为NotInitialization 类对自身常量池的引用了。
也就是说,实际上 NotInitialization 的 Class文件之中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 文件后就已不存在任何接洽了。
类加载器

类加载过程可以形貌为:通过一个类的全限定名来获取形貌该类的二进制字节省。实现这个动作的代码被称为类加载器(Class Loader)。
体系自带的类加载器分为三种:

  • 启动类加载器(BootstrapClassLoader)
  • 扩展类加载器(ExtClassLoader)
  • 应用类加载器(AppClassLoader)
启动类加载器:它用来加载 Java 的核心类(存放<JAVA_HOME>\lib目次,大概被-Xbootclasspath 参数所指定的路径),是用原生 C++ 代码来实现的,是捏造机自身的一部门
我们在代码层面无法直接获取到启动类加载器的引用,以是不允许直接操纵它,假如获取它的对象,将会返回 null。
扩展类加载器:以 Java 代码的情势实现的。负责加载 <JAVA_HOME>\lib\ext 目次中,大概被 java.ext.dirs 体系变量所指定的路径中全部的类库。
应用步伐类加载器:它负责在 JVM 启动时加载来自 Java 下令的 -classpath 大概 -cp 选项、java.class.path 体系属性指定的 jar 包和类路径。
在应用步伐代码里可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取应用类加载器。
假如没有特殊指定,即在没有利用自界说类加载器环境下,用户自界说的类都由此加载器加载。
别的还可以自界说类加载器。
假如用户自界说了类加载器,则自界说类加载器都以应用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载器是有条理关系的,启动加载器又叫根加载器,是扩展加载器的父加载器,但是直接从 ExClassLoader 里拿不到它的引用,同样会返回 null。
上图展示的各种类加载器之间的条理关系被称为类加载器的双亲委派模型(Parents Delegation Model)。
双亲委派模型要求除了顶层的启动类加载器外,别的的类加载器都应有本身的父类加载 器。不外这里类加载器之间的父子关系一样平常不是以继续(Inheritance)的关系来实现的,而是通常利用组合(Composition)关系来复用父加载器的代码。
双亲委派机制

当一个自界说类加载器须要加载一个类,比如 java.lang.String,它很懒,不会一上来就直接试图加载它,而是先委托本身的父加载器去加载,父加载器假如发现本身尚有父加载器,会不绝往前找,只有当父加载器反馈本身无法完成这个加载哀求(它的搜刮范围中没有找到所需的类)时,子加载器才会实验本身去完成加载。
假如启动类加载器已经加载了某个类比如 java.lang.String,全部的子加载器都不须要本身加载了。
双亲委派模型的实现:
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {        // 起首,查抄哀求的类是否已经被加载过了         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;    }这个模型的利益在于 Java 类有了一种优先级的条理分别关系。比如 Object 类,这个毫无疑问应该交给最上层的加载器举行加载,纵然是你覆盖了它,最终也是由体系默认的加载器举行加载的。
假如没有双亲委派模型,就会出现很多个差异的 Object 类,应用步伐会一片紊乱。
怎样突破双亲委派(/怎样粉碎双亲委派)

固然可以粉碎了,我们知道类的加载方式默认是双亲委派,假如我们有一个类想要通过自界说的类加载器来加载这个类,而不是通过体系默认的类加载器,说白了就是不走双亲委派那一套,而是走自界说的类加载器
我们知道双亲委派的机制是ClassLoader中的loadClass方法实现的,突破双亲委派,着实就是重写这个方法,来用我们本身的方式来实现即可
固然这里要注意一下,Object.class这是对象的顶级类,改变类的类加载器的时间要注意,假如全部改了,Object.class就找不到了,加载不了了
以是呢,这里重写的时间,要注意分类办理,把你想要通过自界说类加载器加载的和想通过默认类加载器加载的分隔开
以JDBC为例
1 倒霉用java SPI时,以如下方式加载驱动实现类:
在Driver类中像DriverManager注册对应的驱动实现类。
Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://myhost/test?useUnicode=true&characterEncoding=utf-8&useSSL=false", "test", "test");public class Driver extends NonRegisteringDriver implements java.sql.Driver {    public Driver() throws SQLException {    }    static {        try {            DriverManager.registerDriver(new Driver());        } catch (SQLException var1) {            throw new RuntimeException("Can't register driver!");        }    }}————————————————
版权声明:本文为CSDN博主「徐俊生」的原创文章,依照CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/D812359/article/details/124075194
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 16:45, Processed in 0.120588 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表