深入理解JVM(三)-类加载与执行引擎

目录

一、Class文件介绍

二、类加载阶段

三、类加载器

 四、执行引擎


        本文章是根据《深入理解Java虚拟机》一书,并参考网上其他文档进行的系统性的和简单容易理解的方式进行的整理。

        Java是解释型语言,Java编译之后的字节码文件仍需通过虚拟机解释执行或编译执行,究其原因则是因为计算机仍然只能识别0和1。(PS:当然如果未来的科技不在只使用0和1时,计算机科学可能会经历很长时间的阵痛)

一、Class文件介绍

        Class文件的结构基本上比较稳定,经常变化的是访问标识和属性的扩展,以便于支持Java新的功能和功能的优化。

1、魔数与版本

        每个class文件的前四个字节表示魔数,他的唯一作用是确定这个文件是否是一个能被虚拟机接受的class文件, 很多文件存储标准中都使用魔数来进行身份识别,使用魔数而不是扩展名来进行识别主要基于安全方面的考虑。因为扩展名可以随便改动。文件格式的制定者可以自有的选择魔数值。紧接着魔数的4个字节后(class文件的魔数为CAFEBASE),第5个和第6个表示次版本号。第7、8表示主版本号。(PS:之前在一个开发的JDK与服务器JDK不一致的项目上,测试同事打包的代码在服务器一直无法运行,就是通过编译的Class文件获取到编译的JDK版本与服务器中的版本不一致)

2、常量池

        class文件中在魔数和版本号之后是常量池。

3、访问标识

        在常量池之后是访问标识。

4、类索引、父类索引与接口索引集合

        保存的都是类或接口的全限定名字符串。

5、字段表集合

        类的成员变量的集合。

6、方法表集合

        类的方法集合。

7、属性表集合

        类的属性的集合。

二、类加载阶段

        Class文件需要被加载到虚拟机中才能被运行和使用。

1、类的生命周期

        类的生命周期包括加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中加载、验证、准备、初始化和卸载5个阶段是按照顺序的,而解析和使用则顺序不定。类的初始化操作只会执行一次,类的初始化操作是的类引用称为主动引用,其他则称为被动引用。

2、类加载阶段

        主要完成三件事:

        ①通过类的全限定名来获取定义此类的二进制字节流(可能来源于Class文件、反射机制生成的二进制字节流、网络上的文件流等);

        ②将二进制字节流中所代表的静态存储结构转换为方法区的运行时数据结构(1.8之后为元数据区);

        ③在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区(元数据区)这些数据的访问入口。

        何时触发类加载:
        ①使用类加载器直接加载;
        ②创建本类或这个类的子类实例时;
        ③访问类中静态成员时(包含属性和方法),但使用static final修饰的8种基本类型以及String类型时不会触发类被加载(类的编译优化)。

3、验证阶段

        验证二进制字节流中的内容符合当前虚拟机的要求,并且不会危害虚拟机自身的安全(因为二进制字节流的来源比较广泛,所以其内容的产生不一定严格经过编译的)。验证内容可以包括字节码文件格式的验证、元数据验证、字节码验证和符号引用验证等。

4、准备阶段

        为类变量准备内存和设置初值的阶段。其中整型会被设置为0,浮点型会被设置为0.0d,引用类型会被设置为null。其中有个特殊的情况:

        ①public static int value = 123;这个语句在准备阶段会为value设置初值为0,在初始化阶段才会设置值为123;

        ②public static final int value = 123;这个语句在准备阶段会直接设置处置为123;主要是编译器优化的结果。

5、解析阶段

        将常量池中的符号引用变为直接引用的过程,因为JVM虚拟机采用的是直接引用。

6、初始化阶段

        类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态代码块和静态初始化成员变量(如前面准备阶段非final修饰的static变量将会在这个阶段赋值,成员变量也将被初始化)。当多个线程去初始化同一个类时,那么只会有一个类去执行初始化操作,而其他类处于阻塞状态。

三、类加载器

        最初是为了满足Java Applet的需求而被开发出来的,但如今类加载器却在类层次划分、OSGI、热部署、代码加密等领域大放光彩。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟里里面的唯一性。比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有真正的意义,否则,即使这两个类来源于同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

1、类加载器分类

        站在Java虚拟机角度讲,只存在两种不同类型的类加载器:一种是用C++实现的,是虚拟机自身一部分的启动类加载器;一种是用Java语言实现的,独立于虚拟机外部的其他类加载器,继承于java.lang.ClassLoader抽象类。

        站在Java开发人员角度的加载器分类:

        ①启动类加载器(Bootstrap):启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中。注意由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的。启动类加载器无法被Java程序直接使用。

        ②扩展类加载器(Extension):扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

        ③系统类加载器(System,App类加载器):负责加载用户类路径上所指定的类库(系统类路径java -classpath或-D java.class.path),如果应用程序中没有自定义加载器,那么此加载器就为默认加载器。

        ④线程上下文类加载器:通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader)。此类加载器打破了双亲委派模型

        ⑤自定义类加载器:自定义的ClassLoader来加载特定路径下的class文件生成class对象。

        我们的应用程序一般由启动类加载器、扩展类加载器和系统类加载器相互配合进行加载的,如果有必要可以增加线程上下文类加载器和自定义类加载器进行类的加载。

2、双亲委派模型

        双亲委派模型的基本功能是进行了类的加载,但最重要的功能是保证Java程序的稳定运行

        双亲委派模型工作模式:在Java 1.2后引入,其工作原理的是如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去加载类,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

        逻辑父类:依照双亲委派模型的工作模式,双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。

        优点:

        ①避免重复加载类。Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,即当父亲已经加载了该类时,子类加载器不会再加载。

        ②避免核心类被覆盖的危险。假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

        ③核心API类不被破坏。java.lang是核心API包,需要访问权限,除了启动加载器外的其他类加载器强制加载将会报出异常。

         缺点:

        ①效率比直接进行类加载低。需要打破双亲委派模型。

        ②加密了的class文件需要解密后在加载,无法直接使用虚拟机提供的加载器加载。需要自定义类加载器(这一条即是缺点,又是优点,因为提升了安全性)。

        ③不在classpath路径下的class文件无法用虚拟机提供的加载器加载。需要自定义类加载器。

        ④热部署和JVM定义了接口但第三方提供实现类的包无法用双亲委派模型加载,需要打破双亲委派模型。

3、打破双亲委派模型

        双亲委派模型并不是一个强制性的约束模型,而是Java设计者们推荐的类加载方式。主要有3个场景:

        ①双亲委派模型引入时导致的编码妥协。因为部分类加载器已由用户在JDK1.2之前已经实现,为了保证兼容性,用户自定义加载器时需要使用findClass()方法,而不是重写loadClass()方法。

        ②启动类加载器需要加载用户的未知类时。此时引入了线程上下文类加载器。

        ③企业级应用对程序动态性的追求。所谓对程序动态性的追求,则是向代码热替换、模块热部署等的功能。

 四、执行引擎

        执行引擎是Java最核心的组成部分之一,具有执行代码的能力,如下图所示:

1、运行时栈帧结构

        栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区虚拟机栈的栈元素。栈帧存储了局部变量表、操作数栈、动态链接、方法返回地址等信息。执行引擎所运行的字节码文件都只对当前栈帧进行操作。栈帧中的局部变量表、操作数栈等的大小已经在编译好的字节码文件(也可以是网络中的字节码信息)中指定好。

        ①局部变量表。一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。最大容量在Class文件的属性中指定。局部变量表的变量需要手动赋初值。

        ②操作数栈。 操作数栈也常被称为操作栈,它是一个后入先出栈,最大容量在Class文件的属性中指定。

        ③动态链接。每个栈帧都包含一个指向运行时常量池的该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

        ④方法返回地址。当一个方法被执行后,有两种方式退出这个方法,一种是正常完成出口(方法遇到返回指令),一种是异常完成出口(JVM虚拟机内部产生异常)。

        ⑤附加信息。虚拟机规范中没有明确要求的其他供应商自行添加的信息。

2、方法调用

        方法调用不等同于方法的执行,Class文件中存储的都是符号引用(给Java带来更强大的动态扩展能力),需要在类加载期间甚至到运行期间才能确定目标方法的直接引用,所以使得Java的调用过程变得相对复杂。

        ①解析:在编译期将方法的符号引用转变为直接引用,符合”编译器可知,运行期不可变“的要求,符合这个要求的主要有静态方法和私有方法(因为这两类方法不可能通过继承或别的方式写出其他版本),其中静态方法时通用的,而私有方法是私有的。主要是针对非虚方法。补充概念:

                虚方法:能被重写的方法,一般指的是实例方法。

                非虚方法:不能被重写的方法,指的是构造方法、静态方法、私有方法和final修饰的方法。

        ②分派:Java虚拟机确定正确的目标方法的过程。分派与解析不是互斥的动作,而是在不同层次去筛选和确定目标方法的过程。主要是针对虚方法

                静态分派:在代码编译期间通过静态类型(方法参数的接收类型,而不是实际类型)来定位方法执行版本的分派动作。主要体现在方法的重载。需要注意的是,在重载情况下,编译器虽然能确定出方法的重载版本,很多情况下这个重载版本并不是“唯一”的,往往只能确定一个更加适合的版本。如下图,当调用sayHello(‘a’)时,编译器会在下面三个方法中选择一个合适的方法:

                动态分派:主要是体现了重写的特性。大概步骤如下:

                        第一步:找到对象的实际类型;

                        第二步:如果在对象中找到同名的方法并校验访问权限,如果通过则返回这个方法的直接引用,查找过程结束。

                        第三步:如果第二步没找到则去其父类中查找同名的方法和进行权限验证,如果通过则返回方法的直接引用,查找过程结束。

                        第四步:如果没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值