死磕JVM(六) 类加载机制

目录

1 前言

2 类加载

2.1 加载

2.2 验证

2.3 准备

2.4 解析

2.5 初始化

3 类加载器基本概念

java.lang.ClassLoader类介绍

3.1 类加载器的树状组织结构

3.2 类加载器的双亲委派模型


死磕JVM(一)内存区域  https://blog.csdn.net/u012133048/article/details/85344025

死磕JVM(二)内存模型  https://blog.csdn.net/u012133048/article/details/87886352

死磕JVM(三)内存溢出 https://blog.csdn.net/u012133048/article/details/87891398

死磕JVM(四) 垃圾回收机制 https://blog.csdn.net/u012133048/article/details/85413539

死磕JVM(五)对象的创建 https://blog.csdn.net/u012133048/article/details/87938452

死磕JVM(六) 类加载机制 https://blog.csdn.net/u012133048/article/details/85378148

死磕JVM (七) 锁优化 https://blog.csdn.net/u012133048/article/details/85490843

死磕JVM (八) 总结 https://blog.csdn.net/u012133048/article/details/88069289

1 前言

一个Java文件从编码完成到最终执行,一般主要包括两个过程:

编译

运行

  1. 编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
  2. 运行,则是把编译声称的.class文件交给Java虚拟机(JVM)执行。

而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。

由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次

2 类加载

特性:

1.全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

2.父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

3.缓存机制,缓存机制将会保证所有加载过的Class都会被存放在内存中,当程序中需要使用某个Class时,类加载器先会在方法区寻找该Class信息,若不存在,系统才会执行类加载过程,并将其转换成Class对象,存入内存中。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。

过程:

类加载的过程主要分为三个部分:加载、链接、初始化

链接又可以细分为三个小部分:验证、准备、解析

其中加载、验证、准备、初始化和卸载着5个阶段的顺序是确定的,解析阶段并不一定,在某些情况下,在初始化阶段之后再开始,这时为了支持java语言的运行时绑定(动态绑定)

2.1 加载

简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。这里有两个重点:

字节码来源。一般的加载来源包括:

  • 从本地路径下编译生成的.class文件(jar包中的class文件);
  • 从远程网络(Applet);
  • 运行时动态代理计数,通过java.lang.reflect.Proxy中,就是用ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
  • 。。。。。。

类加载器。一般包括引用类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。后续会详细介绍。

类加载完成后,虚拟机外部的二进制字节流就会按照虚拟机所需的格式存储在方法区,然后内存中会实例化一个java.lang.Class类的对象(该类的对象并没有存在java堆中,而是存在方法区中,这和其他类的实例不同),该对象作为程序访问方法区中这些类型数据的外部接口。

 

2.2 验证

  1. 主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
  2. 包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
  3. 对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
  4. 对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
  5. 对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(privatepublic等)是否可被当前类访问?

注意:验证是非常重要的阶段,但并不是必须的阶段,如果所运行的全部代码,已经经过反复使用或者验证过,可以在实施阶段考虑使用-Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

 

2.3 准备

主要是为类变量(注意,不是实例变量,实例变量会在对象实例化一起分配在java堆中)分配内存,并且赋予初值。

特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。

比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456 那么该阶段tmp的初值就是456,这时因为有final,有final关键字,表示显示的赋予初值

举个例子:类变量

public static int a = 1;

在准备阶段会将a赋值为0,在初始化节点赋值为1。

 

2.4 解析

将方法区常量池内(常量池包含类信息常量池)的符号引用替换为直接引用的过程。

两个重点:

  1. 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
  2. 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

2.5 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<client>方法执行之前,父类的<client>方法已经执行完毕,由于父类的<clinit>方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,这里比较抽象用代码看一波就明白了(书中的例子):

static class Parent{

    public static int A = 1;
    static {
        A = 2;

    }
}

static class Sub extends Parent{

    public static in B = A;
}

public static void main(String[] args){

    System.out.println(Sub.B)
}

上述代码的结果将输出2,以为这父类的静态语句块,优先于子类B=A的赋值操作。

可以总结出两条初始化的特性:

1、假如该类的直接父类还没有被初始化,则先初始化其直接父类(直接父类也依次执行1,2,3 保证类依赖的所有父类都会被初始化);

2、假如类中有初始化语句,则系统依次执行这些初始化语句(static 语句)。

这部分内容在https://blog.csdn.net/u012133048/article/details/94048373这篇博文会比较详细

特殊情况:

  1.  如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法。
  2. 接口中不能使用静态语句块,但是仍然有初始化的赋值操作,因此也会生成<client>()方法,但是和类不同的是,接口执行<client>()方法不需要先执行父类借口而的<client>()方法。
  3. 在多线程环境中,对同一个类进行初始化,将会被正确的加锁和同步,只会有一个线程去执行,而且执行过一次后,其他线程都不会去执行。

有且只有下面5种情况才会立即初始化类,称为主动引用

1、遇到new、getstatic、putstatic或者invokestatic这4条字节码指令时:使用new关键字实例化对象的时候,读取或者设置一个类的静态字段的时候(除了被final,已在编译期把结果放入常量池的静态字段)

2、用java.lang.reflect包的方法对类进行反射调用没初始化过的类时Class.forname()会进行初始化;

       而.classJVM将使用类装载器, 将类装入内存(前提是:类还没有装入内存),不做类的初始化工作.返回Class的对象

3、初始化一个类时发现其父类没初始化,则要先初始化其父类;

4、含main方法的那个类,jvm启动时,需要指定一个执行主类,jvm先初始化这个类

5、使用动态语句支持是。
 

注意以下几种情况不会执行类初始化:

通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

  1. 定义对象数组,不会触发该类的初始化。
  2. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  3. 通过类名获取Class对象,不会触发类的初始化。
  4. 通过Class.forName加载指定类时,如果指定参数initializefalse时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  5. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

 

3 类加载器基本概念

顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。

java.lang.ClassLoader类介绍

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。

 

3.1 类加载器的树状组织结构

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

  • 引导类加载器bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader
  • 扩展类加载器extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

 

3.2 类加载器的双亲委派模型

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

这里可以举个例子:

        如果没有双亲委派机制,我们在rt.jar中创建一个相同类名的java.lang.Object类,并放在程序的ClassPath中,系统中将会出现多少个不同的Object类,java类型体系中最基础的行为也就无法保证,应用程序也会变得一片混乱,但是如果有了双亲委派记住,就可以发现,我们编写的同名类可以被编译,但是永远无法被加载运行。

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值