2020-12-19 JVM-编译和类加载机制

屏幕快照 20201215 下午11.26.59.png

引言:今天谈谈源码文件如何编译Class字节码文件以及字节码文件如何加载到JVM中。

源码转换为字节码文件

1.源码通过什么命令转换为字节码文件,具体的流程是什么?

编译命令: javac -g:vars Person.java ---> Person.class

反编译命令: javap -v -p Person.class

编译流程:Person.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽 象语法树 -> 字节码生成器 -> Person.class文件

2.源码和字节码文件内容是否有区别,如果理解两者?

通俗来讲,源码文件是给程序员来看的,字节码文件是给JVM来看的。文件内容没有区别,仅仅是展示形式的区别。

通常字节码文件包括:magic、版本号、常量池、字段表集合、方法表集合。其中常量池中存储了字面量符号引用两方面内容。

字面量:文本字符串、final修饰等。

符号引用:类和接口的全限定名、字段名称和描述符、方法名称和描述符。

//TODO 待补充

Class文件如何加载到JVM中--类加载机制

屏幕快照 20201216 上午8.48.27.png

定义

类加载机制是指我们将类的字节码文件所包含的数据读入内存,并生成数据访问入口的的一种特殊机制。

过程

过程包括:装载、链接、初始化、使用、卸载。

装载

查找和导入class文件。

  • 如何查找class文件?

    1. 从本地系统中加载。--场景:本地编译生成的 .class文件。
    2. 通过网络下载。场景:--Web Applet,也就是我们的小程序应用。
    3. 从zip、jar包等归档文件中加载。--场景:maven本地库加载jar war包。
    4. 从专门的数据库中提取。--场景:JSP应用从专有数据库中提取.class文件,较为少见
    5. 将Java源文件编译为.class文件,运行时加载--场景:动态代理技术。
  • 通过什么可以将class文件导入内存中,如何导致到内存,导入到JVM的那些区域,有什么注意事项?

    1. 类加载器

    2. 通过一个类的全限定名获取定义此类的二进制字节流,以二进制流的形式将.class文件导入到JVM运行时数据区中。

      具体是:(1)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 (方法区:类信息,静态变量,常量)

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

      注意:1.反射形成的代码不在这个阶段加载进入方法区。2. 程序员如果对类加载机制进行操作用以实现代码增强获取其他目的--仅能在 装载时期进行完成,常用方法有自定义ClassLoader等。

链接:包括:验证、准备、解析。

验证
  1. 验证的目的是什么?

    • .class文件中字节流包含的信息满足当前JVM的要求
    • 信息不会危害JVM。
  2. 验证分哪几个阶段,作用是什么?

    包含:文件格式验证 --> 元数据验证 --> 字节码验证 --> 符号引用验证。

    验证类型 校验内容 目的 例子 说明
    文件格式验证 字节流 输入的字节流能正确的解析并存储器方法区 1.是否以16进制cafebaby开头
    2.版本号是否正确。
    字节流才会进入内存的方法区中进行存储,后面验证都是基于方法区的存储结构进行的
    元数据验证 对Java语法校验 不存在不符合Java语法 规范的元数据信息 1.是否继承了final类
    2.一个非抽象类是否实现了所有的抽象方法
    case 1 :final类是不能被继承的,继承了就会出现问题。
    case 2:如果没有实现全部的抽象方法,那么这个类也是无效的。
    字节码验证 1.数据流、
    2.控制流
    3.类的方法体
    1.确保程序语义合法
    2.确保类中方法运行不会损害JVM
    栈数据类型和操作码操作参数吻合(比如栈空间只有4个字节,但是我们实际需要的远远大于 4个字节,那么这个时候这个字节码就是有问题的)
    符号引用验证 常量池中的符号引用 确保解析动作能正常执行 1.常量池中描述类是否存在
    2.访问的方法或者字段是否存在且具有足够的权限
    1.符号引用验证最后一个阶段的验证
    2.发生时期:解析阶段
    3.常量池中符号引用的匹配性校验
  3. 是否可以取消验证?

    -Xverify:none 取消验证

准备

​ 操作的是方法区中的类变量信息,为类变量(静态变量)分配内存并且设置该类变量的默认初始值,包括8种基本类型和一个引用类型(reference = null)附初始值。

​ 注意:1.不会为静态常量赋值(即final修饰的static),因为在编译的时候就会已经分配了,在准备阶段进行初始化。

              2. 不会为实例变量(没有static修饰的变量)进行赋值,实例变量会随实例分配Heap 中。     

class文件什么类变量会被ContstantValue 关键字修饰,该关键字的作用是什么?

  1. 只用被final 和static修饰的基本类型变量和String类型变量才能被ContstantValue修饰。

  2. 通知JVM在准备阶段为自动为静态常量赋值。

解析

目的是什么?对那些符号引用进行解析?

  1. 符号引用转变成直接引用。 -- 解析阶段是常量池将符号引用转变成直接引用的过程。

  2. 对解析结果进行缓存

  3. 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。

初始化

初始化执行阶段?如何理解?步骤?类变量进行初始值设定有两种方式?

  1. 执行类构造器()方法的过程

  2. 在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的
    主观计划去初始化类变量和其他资源,比如赋值.

    题目:private static int i = 1;准备阶段int = ? 初始化阶段 int = ?

    准备阶段 int = 0; 初始化阶段 int 1 = 1;

    • 假如这个类还没有被加载和连接,则程序先加载并连接该类

    • 假如该类的直接父类还没有被初始化,则先初始化其直接父类

    • 假如类中有初始化语句,则系统依次执行这些初始化语句

    • 使用静态代码块为类变量指定初始值
    • 声明类变量是指定初始值

使用

类什么时候初始化?

类主动引用的时候才会被初始化。

主动引用常见的六种场景

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

被动引用的场景?

  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
  • 定义类数组,不会引起类的初始化。
  • 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化的)

卸载

类被卸载的条件?

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。

  • 加载该类的ClassLoader已经被回收。 该类对应的java.lang.Class对象没有任何地方被引用,

  • 无法在任何地方通过反射访问该类的方法。

    注意:上面三个条件都满足的情况下才会被回收,然而Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class 对象,因此这些Class对象始终是可触及的。

类加载器

什么是类加载器?

  1. 负责读取Java文件并转换成.class类的实例代码块。
  2. 类加载器的作用于是加载文件进入JVM,并确认文件的唯一性。--解释:类在同一个类加载器中具有唯一性,而不同类加载器中是允许同名类存在的, 这里的同名是指全限定名相同。但是在整个JVM里,纵然全限定名相同,若类加载器不同,则仍 然不算作是同一个类,无法通过 instanceOf 、equals 等方式的校验。

类加载器为什么要分层?

为了解决同名类存在,无法加载的问题。case : 假如用户调用他编写的java.lang.String类。理论上该类可以访问和改变java.lang包下其他类的默认访问 修饰符的属性和方法的能力。也就是说,我们其他的类使用String时也会调用这个类,因为只有一个类 加载器,我无法判定到底加载哪个。因为Java语言本身并没有阻止这种行为,所以会出现问题。

引申-- 可不可以使用不同级别的类加载器来对我们的信任级别做一个区分呢? 类加载器包括4种,各自的作用是什么?

比如用三种基础的类加载器做为我们的三种不同的信任级别。最可信的级别是java核心API类(Bootstrap ClassLoader)。然后是 安装的拓展类(Extension ClassLoader),最后才是在类路径中的类(属于你本机的类--App ClassLoader)。

延伸--Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

JVM类加载机制的三种方式?

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

 例如,系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法 所依赖的类及引用的类也载入,依此类推。“全盘负责”机制也可称为当前类加载器负责机 制。显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的类加载器。
以上步骤只是调用了ClassLoader.loadClass(name)方法,并没有真正定义类。真正加载 class字节码文件生成Class对象由“双亲委派”机制完成。
  1. 父类委托:父类委托别名就叫双亲委派机制--“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标 类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

    注意如果自己加载到则导入JVM,如果没有则抛出异常。

  2. 缓存机制:缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class 时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进 制数据,并将其转换成Class对象,存入缓存区。

  • 为什么修改了Class后必须重启JVM程序的修改才会生效?

对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用

自定义类加载器?

我们可以继承java.lang.ClassLoader类,实现自己的类加载器。

如果想保持双亲委派模型,仅仅重写findClass(name)方法;

如果想破坏双亲委派模型,可以重写loadClass(name)方法。

自定义类加载器的建议:

  1. 这里传递的文件名需要是类的全限定性名称,因为 defineClass 方法是按这 种格式进行处理。如果没有全限定名,那么我们需要做的事情就是将类的全路径加载进去,例如:我们的setRoot就是前缀地址 setRoot + loadClass的路径就是文件的绝对路径。
  2. 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
  3. 待字定义类加载器加载的文件不要放到AppClassLoader可以加载的类路径下。

如何实现热部署?

OSGI:比如我们的JAVA程序员更加追求程序的动态性,比如代码热部署,代码热替换。也就是就是机器 不用重启,只要部署上就能用。OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每 一个程序模块都有一个自己的类加载器,当需要更换一个程序模块时,就把程序模块连同类加载器一起 换掉以实现代码的热替换

你可能感兴趣的:(2020-12-19 JVM-编译和类加载机制)