JVM浅出深入系列-第一章 JVM执行流程

JVM浅出深入系列-第一章 JVM执行流程

简介

世界上没有最好的编程语言,如果有,我相信一定是JAVA。

  • 官网链接: https://docs.oracle.com/javase/specs/index.html.

为何要学习JVM?

  • 如果你在线上遇到了OOM,你是否会束手无策。
  • 线上卡顿是否可能是因为频繁Full GC造成的。
  • 新项目上线,服务器数量以及配置不足,对于性能的扩展只能靠服务器的增加,而不能通过JVM的调优达到实现服务器性能的突破。
  • 面试经常会问到JVM的一些问题,但是当面试官问到你实际的落地点时,你就会茫然不知所措,没有条理性,或者答非所问。

JVM是什么?

Java Virtual Machine(Java虚拟机),JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

  • 官网简介
    Jvm简介

    • Java虚拟机是Java平台的基石,其负责其硬件和操作系统的独立性,其编译的代码很小以及保护用户免受恶意程序攻击的能力。
    • Java虚拟机是一种抽象计算机,像真正的计算机一样,它有一个指令集并在运行时操作各种内存区域。
    • Java虚拟机不承担任何特定的实现技术、主机硬件或主机操作系统,它本身并没有被解释。
    • Java虚拟机不知道Java编程语言,只知道特定的二进制格式,即 class 文件格式, class 文件包含Java虚拟机指令(或字节码)和符号表,以及其他辅助信息。
    • 出于安全考虑,Java虚拟机对 class 文件中的代码施加了强大的语法和结构约束,但是,任何具有可以用有效 class 文件表示的功能的语言都可以由Java虚拟机托管,由通用的、与机器无关的平台吸引,其他语言的实现者可以将Java虚拟机作为其语言的交付工具。
  • 为什么要在程序和操作系统中间添加一个JVM?

Java是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要JVM进行一番转换。

  1. Write Once Run Anywhere
    在这里插入图片描述
  • 图中可以看到,有了JVM这个抽象层之后,Java就可以实现跨平台了。JVM只需要保证能够正确执行.class文件,就可以运行在诸如Linux、Windows、MacOS等平台上了。
  • 而Java跨平台的意义在于一次编译,处处运行,能够做到这一点JVM功不可没。比如我们在Maven仓库下载同一版本的jar包就可以到处运行,不需要在每个平台上再编译一次。
  • 现在的一些JVM的扩展语言,比如Clojure、JRuby、Groovy等,编译到最后都是.class文件,Java语言的维护者,只需要控制好JVM这个解析器,就可以将这些扩展语言无缝的运行在JVM之上了。
  • JVM上承开发语言,下接操作系统,它的中间接口就是字节码。
  • JVM JRE JDK是什么关系?
    官网链接 https://docs.oracle.com/javase/8/docs/index.html
    JVM是Java程序能够运行的核心。但需要注意,JVM自己什么也干不了,你需要给它提供生产原料(.class文件)。 仅仅是JVM,是无法完成一次编译,处处运行的。它需要一个基本的类库,比如怎么操作文件、怎么连接网络等。而Java体系很慷慨,会一次性将JVM运行所需的类库都传递给它。JVM标准加上实现的一大堆基础类库,就组成了Java的运行时环境,也就是我们常说的JRE(JavaRuntimeEnvironment) 对于JDK来说,就更庞大了一些。除了JRE,JDK还提供了一些非常好用的小工具,比如javac、java、jar等。它是Java开发的核心。 我们也可以看下JDK的全拼,JavaDevelopmentKit。JVM、JRE、JDK它们三者之间的关系,可以用一个包含关系表示。
    在这里插入图片描述

JVM到底干了些什么?

在这里插入图片描述

1 Java源代码如何变成Class文件的 ?

编译: javac -g:vars XXXX.java —> XXXX.class

  • 分析编译器干了什么事?

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

    由上可知,其实我们的编译器其实做的事情其实就是“对等信息转换”。JAVA文件中的信息其实跟
    我们Class文件中的信息,其实是一样的。

2 JVM折腾些啥 ?

  • 通过编译器的一顿操作,将Java源码变成了字节码文件,这时就需要类加载机制了!
  • 类加载机制是指我我们将类的字节码文件所包含的数据读入内存,同时我们会生成数据的访问入口的一种特殊机制。那么我们可以得知,类加载的最终产品是数据访问入口。
    在这里插入图片描述
2.1 加载class文件的方式 :
  1. 从本地系统中直接加载
    典型场景:这个我就不废话了

  2. 通过网络下载.class文件
    典型场景:Web Applet,也就是我们的小程序应用

  3. 从zip,jar等归档文件中加载.class文件
    典型场景:后续演变为jar、war格式

  4. 从专有数据库中提取.class文件
    典型场景:JSP应用专有数据库中提取.class文件,较为少见

  5. 将Java源文件动态编译为.class文件,也就是运行时计算而成
    典型场景:动态代理技术

  6. 从加密文件中获取,
    典型场景:典型的防Class文件被反编译的保护措施

2.2 *JVM折腾些啥?

类加载的方式已经了解,那么如何加载使用呢?
在这里插入图片描述

2.2.1 装载(Load)

查找和导入class文件

(1) 通过一个类的全限定名获取定义此类的二进制字节流(由上可知,我们不一定从字节码文件中获
得,还有上述很多种方式)

思考:那么这个时候我们是不是需要一个工具,寻找器,来寻找获取我们的二进制字节流。而我们的java中恰好有这么一段代码模块。可以实现通过类全名来获取此类的二进制字节流这个动作,并且将这个动作放到放到java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类,实现这个动作的代码模块成为“类加载器”。

(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

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

获取类的二进制字节流的阶段是我们JAVA程序员最关注的阶段,也是操控性最强的一个阶段。因为这个阶段我们可以对于我们的类加载器进行操作,比如我们想自定义类加载器进行操作用以完成加载,又或者我们想通过 JAVA Agent来完成我们的字节码增强操作。

在我们的装载阶段完成之后,这个时候在我们的内存当中,我们的运行时数据区的方法区以及堆就已经有数据了。

  • 方法区:类信息,静态变量,常量
  • :代表被加载类的java.lang.Class对象

即时编译之后的热点代码并不在这个阶段进入方法区

2.2.2 链接(Link)
  • 2.2.2.1 验证(Verify)

验证只要是为了确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,并且还要求我们的信息不会危害虚拟机自身的安全,导致虚拟机的崩溃

  1. 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。这阶段的验证是基于二进制字节流进行的,只有经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面验证都是基于方法区的存储结构进行

举例:

  • 是否以16进制cafebaby开头
  • 版本号是否正确
  1. 元数据验证(Java语法)

对类的元数据信息进行语义校验(其实就是对Java语法校验),保证不存在不符合Java语法规范的元数据信息。

举例:

  • 是否有父类
  • 是否继承了final类
  • 一个非抽象类是否实现了所有的抽象方法
  1. 字节码验证

进行数据流和控制流分析,确定程序语义是合法的、符合逻辑的。对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
字节码的 验证会相对来说较为复杂 。

举例:

  • 运行检查
  • 栈数据类型和操作码操作参数吻合(比如栈空间只有4个字节,但是我们实际需要的远远大于4个字节,那么这个时候这个字节码就是有问题的)
  • 跳转指令指向合理的位置
  1. 符号引用验证

这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。符号引用验证的目的是确保解析动作能正常执行。

举例:

  • 常量池中描述类是否存在
  • 访问的方法或者字段是否存在且具有足够的权限

但是,我们很多情况下可能认为我们的代码肯定是没问题的,验证的过程完全没必要,那么其实我们可以添加参数

  • -Xverify:none 取消验证。
  • 2.2.2.2 准备(Prepare)

    • 为类变量(静态变量)分配内存并且设置该类变量的默认初始值。

    • 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化

    • 这里不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

在这里插入图片描述

进行分配内存的只是包括类变量(静态变量),而不包括实例变量,实例变量是在对象实例化时随着对象一起分配在java堆中的。通常情况下,初始值为零值,假设public static int a=1;那么a在准备阶段过后的初始值为0,不为1,这时候只是开辟了内存空间,并没有运行java代码,a赋值为1的指令是程序被编译后,存放于类构造器()方法之中,所以a被赋值为1是在初始化阶段才会执行。

扩展

在这里插入图片描述

在这里插入图片描述

  • 2.2.2.3 解析(Resolve)

把类中的符号引用转换为直接引用

直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定存在内存中。

2.2.3 初始化(Initialize)

初始化阶段是执行类构造器()方法的过程。

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

在Java中对类变量进行初始值设定有两种方式:

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

JVM初始化步骤:

  • 假如这个类还没有被加载和连接,则程序先加载并连接该类
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句
2.2.4 使用

那么这个时候我们去思考一个问题,我们的初始化过程什么时候会被触发执行呢?或者换句话说类初始
化时机是什么呢?

  • 主动引用

    只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种:

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

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

在类使用完之后,如果满足下面的情况,类就会被卸载

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。但是一般情况下启动类加载器加载的类不会被卸载,而我们的其他两种基础类型的类加载器只有在极少数情况下才会被卸载。

3 什么是类加载器?

  • 负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例的代码模块。
  • 类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。

一个类在同一个类加载器中具有唯一性(Uniqueness),而不同类加载器中是允许同名类存在的,这里的同名是指全限定名相同。但是在整个JVM里,纵然全限定名相同,若类加载器不同,则仍然不算作是同一个类,无法通过 instanceOf 、equals 等方式的校验。

在这里插入图片描述

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

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

  • 父类委托
    “双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

    • “双亲委派”机制加载Class的具体过程是:
    1. ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器。
    2. 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给祖父类加载器。
    3. 依此类推,直到始祖类加载器(引用类加载器)。
    4. 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。
    5. 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。
    6. 依此类推,直到源ClassLoader。
    7. 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。

    “双亲委派”机制只是Java推荐的机制,并不是强制的机制。

    我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。

  • 缓存机制
    缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效.对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

5 如何打破双亲委派

双亲委派这个模型并不是强制模型,而且会带来一些些的问题。就比如java.sql.Driver这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧

如何打破双亲委派?

  • SPI :
  • OSGI:
  • 自定义类加载器
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值