深入理解java虚拟机3——类加载

4.1 Java运行流程

  • 编译期
  • 运行期

4.2 Class类文件结构

4.3 字节码指令

  • 构造器的字节码指令(<clinit>()V,<init>()V)
  • 方法调用的字节码指令(静态绑定(解析,非虚方法)/动态绑定(分派,虚方法))
    • invokevirtual
    • invokespecial
    • invokespecial
  • invokevirtual执行流程(多态原理)
  • vtable和itable

4.4 类加载机制

  • 加载(用类加载器将字节码载入方法区中)
  • 连接
    • 校验(检验类是否符合jvm规范)
    • 准备(为静态变量分配内存并设置初始值)
    • 解析(将常量池内的符号引用替换为直接引用)
  • 初始化(执行static的内容 <cinit>(),懒加载)

4.5 类加载器

  • 三个加载器(BootStrap / Extension / Application)
  • 双亲委派机制
  • 破坏双亲委派机制(线程上下文类加载器TCL)

4.1 Java运行流程

编译期(compile time):*.java-->*.class(字节码文件)

运行期(runtime):由JVM运行.class文件,将.class文件加载到内存(其中包括选择类加载器)


4.2 Class类文件结构

注:以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个 字节的无符号数

名称字节
mgaicu4魔术:CAFE BABE
minor_versionu2次版本号
major_versionu2主版本号
constant_poolu2 + x(有多少个常量+常量池内容)

常量有17种不同类型

tag+内容:tag大小u1,代表属于哪种常量类型

eg.

07 03:07表示是一个CONSTANT_Class_info类型,名称为3号常量

access_flagu2

访问标志和继承信息

是否为public/final/interface/abstract等等

this_classu2

本类名

eg. 00 05表示常量池#5为该类的全限类名

super_classu2

父类名

除了Object没有父类,剩下都有父类

interfacesu2 + x(个数 + 内容)

接口索引集合

描述这些类实现了哪些接口

field_infou2 + x

字段表:类变量(不包括局部变量),是否为public/static/final/volatile等等

eg.

00 02 (第一个field信息)(第二个field信息)

field信息格式:access_flags(2,修饰符) +name_index(2,成员名,对应常量池)+descriptor_index(2,数据类型,对应常量池)+(是否final,一般没有)+(attribute) 

method_info2 + x

方法表:记录方法

类似字段表

属性表xCode(方法表:描述代码,需要栈深度,slot变量槽);Exception(异常表)等等


4.3 字节码指令

(1)构造器的字节码指令:<cinit>()V 和 <init>()V

  • <clinit>()V:类构造器(也称为静态构造器)
    • 对象:用于初始化类变量(也称为静态变量)和执行静态初始化块
    • 执行时机:在类被首次主动使用(类加载)时执行一次,编译器会按从上到下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为<clinit>()
      • public calss Demo{
            static int i = 10;
            static{ i = 20;};
            static{ i = 30;};
        } //最后i的值为30
    • 线程安全: 类构造器的调用是线程安全的,JVM会保证一个类的<clinit>()V在多线程环境中被正确地加锁、同步。
  • <init>()V:实例构造器(也称为非静态构造器或对象构造器)
    • 对象:用于初始化实例变量和执行实例初始化块,以及调用父类的构造器
    • 执行时机:在每次创建对象时执行。从上到下顺序执行,但构造器放在最后
      • public class Demo{
            private String a = "s1";       
            {b = 20;}
            private int b = 10;
            {a = "s2";}    
            public Demo(String a, String b){
                this.a = a;
                this.b = b;
            }
        }
        
        Demo("s3", 30);
        
        //执行顺序
        //1. a="s1"    
        //2. b=20    
        //3. b=10    
        //4. a="s2"    
        //5. a="s3", b=30
    • 线程安全:线程安全需要开发者自己保证

(2)方法调用的字节码指令

在Java字节码中,invokespecial、invokevirtual和invokestatic是用于方法调用的指令,它们在调用方法时的用途和方式有所不同

  • invokevirtual(调用实例方法)
    • 用途:用于调用对象的实例方法(public方法)
    • 绑定类型:动态绑定,编译期不确定,运行期确定,因此支持多态。在运行期,JVM会根据对象的实际类型的方法表来查找具体要执行的方法
  • invokestatic(调用静态方法)
    • 用途:用于调用一个类的静态方法
    • 绑定类型:静态绑定,也就是编译时绑定。因为静态方法是属于类的而不是实例的
  • invokespecial(调用特殊方法)
    • 用途:用于调用包括初始化方法<init>,private方法,final方法,super父类方法
    • 绑定类型:静态绑定,编译期绑定。这四种方法调用会在类加载的时候就把符号引用变成直接引用
  • invokeinterface:调用接口方法
invokespecial非虚方法——>静态绑定:编译期就确定构造方法(<init>);private方法;父类方法解析静态方法、私有方法、构造方法、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用)会在类加载的时候就把符号引用变成直接引用
invokestatic非虚方法——>静态绑定:编译期就确定静态方法
invokevirtual虚方法——>动态绑定:编译期不确定,运行期确定普通方法(包括final方法)分派

静态分派:重载,编译期就可知;在编译阶段,就能根据参数的静态类型决定 了会使用哪个重载版本,并把这个方法的符号引用写到 main()方法里的两条invokevirtual指令的参数中

动态分派:重写,运行期再选择;invokevirtual指令的工作过程是,优先寻找当前类中是否有该方法,如有直接选择该方法,若没有找到,则在父类中寻找,直到找到为止。

(3)invokevirtual执行流程(多态原理)

  • 多态原理:invokevirtual指令
    • 1. 通过栈帧中的引用找到堆中的对象
    • 2. 分析对象头中的Klass Word,找到实际的class
    • 3. class结构中有vtable(在类加载的链接阶段就已经生成),可以得到该方法的地址
    • 4. 执行方法中的字节码

(4)vtable 和 itable

  • 在Java实现中我们常使用多态性,在java里主要是通过itable, vtable来实现准确的跳转
  • 如何在编译期初始化vtable:当类初始化的时候,复制父类的虚拟表, 然后根据自己的顺序替换或者增加虚拟表的内容,
    • 如果重写函数,(方法名字,参数签名完全一样),也就是替换虚拟表相同顺序的内容
    • 如果重载函数(方法名字,参数签名完全不一样)/或者自己写的函数,顺序添加到虚拟表尾部
  • 根据前面的分析,我们可以知道 
    • invokeinterface 使用的是itable
    • invokevitual 使用的是vtable
    • invokesepical 直接调用不需要转换
    • invokestatic  直接调用不需要转换

4.4 类加载机制

Java中的类加载机制是指Java虚拟机(JVM)如何加载.class文件,并将其转换为java.lang.Class对象的过程。这个过程主要分为加载(Loading)、链接(Linking)和初始化(Initialization)三个阶段,每个阶段又包含了一些具体的步骤

(1)加载:用类加载器将字节码载入方法区中

  • 具体步骤
    • 1. 通过一个类的全限定名来获取定义此类的二进制字节流
    • 2. 类加载器负责读取Class文件的二进制字节流,并将其转换成java.lang.Class类的实例
  • 该阶段可以由用户自定义类加载器
  • 内部采用C++的instanceKlass描述java类(成员有:java类镜像与c++对象相互转换,父类,成员变量,方法,常量池,类加载器,vtable, itable)
  • 如果这个类的父类还没有加载,先加载父类
  • 加载和连接可能是交替进行的

(2)验证:检验类是否符合jvm规范

  • 检验类是否符合jvm规范,安全性检查
  • 在类加载整个流程中占最大比重,较为耗时
  • 具体步骤
    • 文件格式验证(版本号,tag是否正确等等)
    • 元数据验证(父类是谁,是否实现父类所有方法)
    • 字节码验证(时间最长,主要分析属性表中Code是否符合规范)
    • 符号引用验证(这个阶段发生在解析阶段

(3)准备:为静态变量分配内存并设置初始值

  • 只分配static变量,不包括实例变量,后者在对象实例化时候分配在堆中
  • static变量分配空间和赋值是两个步骤
    • 分配空间(准备阶段),这个时候static变量的值是0或者null。
    • 赋值(初始化阶段)
  • 也有例外:
    • final static的基本类型:由于final不可修改,准备阶段就已经分配内存并赋值
    • final static的new出来的对象类型:初始化阶段再赋值
分配空间赋值
static int a = 2;准备阶段初始化阶段 <cinit>()
static final int b = 2;准备阶段准备阶段
static final String c = "2";准备阶段准备阶段
static final Object e = new Object();准备阶段初始化阶段 <cinit>()
static final String f = new String("2");准备阶段初始化阶段 <cinit>()

(4)解析:将常量池内的符号引用替换为直接引用

  • 符号引用:字符串,能根据这个字符串定位到指定的数据,比如java/lang/StringBuilder
  • 直接引用:内存地址,有直接引用表示内存中已存在

(5)初始化:执行static的内容 <cinit>(),懒加载

  • 初始化阶段就是执行类构造器<cinit>()方法的过程 
  • 必须执行初始化的发生时机:
    • 遇到new关键字,如果类型没有进行过初始化,则必须触发
    • 首次访问这个类的静态变量或静态方法
    • 使用java.lang.reflect包的方法对类型进行反射调用的时候
    • 如果父类没触发,优先触发父类
    • 优先初始化main()方法

4.5 类加载器

任意两个类,即使是同一个class文件,被同一个JVM加载,类加载器不同,则它们也属于两个类

(1)三个类加载器

继承抽象类java.lang.ClassLoader

类加载器名称加载哪里的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问,C++是实现,是虚拟机自身一部分
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为Bootstrap
Application ClassLoaderclasspath上级为Extension
自定义类加载器自定义上级为Application

可以调用Class类下的getClassLoader(全限类名)方法获取该类的加载器:如果是Bootstrap则显示为null

(2)双亲委派机制

  • 概念:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
  • 例子:加载String时,Application询问Extension是否已经加载过该类,Extension再向上级Bootstrap询问——>如果Bootstrap已经加载过,则Application不再加载
  • 源码:java.lang.ClassLoader的loadClass()方法。
  • 为什么需要双亲委派机制:保证Java平台的安全性(如java.lang.Object这样的核心类只由启动类加载器来加载)、保持类的一致性(避免类重复加载)以及提高类加载的效率
  • 逻辑:先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的 loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败, 抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查请求的类是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父类加载器抛出ClassNotFoundException
                    // 说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    // 在父类加载器无法加载时
                    // 再调用本身的findClass方法来进行类加载
                    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;
        }
    }

(3)破坏双亲委派机制

  • 双亲委派机制的弊端:因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件
  • 需要打破该机制的场景
    • 热部署: 在开发大型应用时,经常需要重新加载修改过的类以实现热部署,避免重启整个应用。双亲委派机制下,一旦类被加载,就会一直由同一个类加载器保持引用,无法替换或卸载,因此需要通过打破这一机制来实现类的热替换
    • 多版本共存: 在某些应用场景中,可能需要同时使用一个类的多个版本,例如在OSGi环境中。双亲委派机制会导致总是只能加载到一个版本的类,因此需要特定的类加载策略来支持多版本的共存
    • 特定场景下的类隔离: 在某些企业应用服务器或模块化平台中,不同的应用或模块需要使用独立的类库版本,而不是共享同一个版本。这时候需要打破双亲委派机制,以实现类库的隔离
    • 避免标准Java类加载器的限制: 有时候,开发者可能需要通过自定义类加载器来绕过标准Java类加载器的一些限制。例如,加载网络上的资源或者解密加载的类
  • 打破方法:可以通过自定义类加载器并重写其loadClass方法来实现。自定义的类加载器可以先尝试加载类,如果失败再按照双亲委派机制委托给父类加载器
    • 如线程上下文类加载器TCL(Thread Context ClassLoader)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值