Android技术知识Android开发Android开发经验谈

看完这篇 JVM 原理解析你还不懂?来打我

2022-06-27  本文已影响0人  程序老秃子

JVM 的主要作用是什么?

JVM 就是 Java Virtual Machine(Java虚拟机)的缩写; JVM 屏蔽了与具体操作系统平台相关的信息,使 Java 程序只需生成在 Java 虚拟机上运行的目标代码 (字节码),就可以在不同的平台上运行

JVM的运行机制

JVM(Java Virtual Machine)是用于运行Java字节码的虚拟机; 包括一套字节码指令集、一组程序寄存器、一个虚拟机栈、一个虚拟机堆、一个方法区和一个垃圾回收器;JVM运行在操作系统之上,不与硬件设备直接交互

Java源文件在通过编译器之后被编译成相应的.Class文件(字节码文件),.Class文件又被JVM中的解释器编译成机器码在不同的操作系统(Windows、Linux、Mac)上运行

每种操作系统的解释器都是不同的,但基于解释器实现的虚拟机是相同的,这也是Java能够跨平台的原因; 在一个Java进程开始运行后,虚拟机就开始实例化了,有多个进程启动就会实例化多个虚拟机实例;进程退出或者关闭,则虚拟机实例消亡,在多个虚拟机实例之间不能共享数据

Java程序的具体运行过程如下

  • (1)Java源文件被编译器编译成字节码文件
  • (2)JVM将字节码文件编译成相应操作系统的机器码
  • (3)机器码调用相应操作系统的本地方法库执行相应的方法。Java虚拟机包括一个类加载器子系统(Class Loader SubSystem)、运行时数据区(Runtime Data Area)、执行引擎和本地接口库(Native InterfaceLibrary)

本地接口库通过调用本地方法库(Native Method Library)与操作系统交互,如图所示

其中:

类加载器子系统用于将编译好的.Class文件加载到JVM中; 运行时数据区用于存储在JVM运行过程中产生的数据,包括程序计数器、方法区、本地方法区、虚拟机栈和虚拟机堆

执行引擎包括即时编译器和垃圾回收器; 即时编译器用于将Java字节码编译成具体的机器码,垃圾回收器用于回收在运行过程中不再使用的对象;本地接口库用于调用操作系统的本地方法库完成具体的指令操作

Java 的内存区域?

JVM 在执行 Java 程序的过程中会把它管理的内存分为若干个不同的区域; 这些组成部分有些是线程私有的,有些则是线程共享的,Java 内存区域也叫做运行时数据区,它的具体划分如下:

虚拟机栈 : Java 虚拟机栈是线程私有的数据区,Java 虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置

方法在执行过程中,会在虚拟机栈中创建一个 栈帧(stack frame);每个方法执行的过程就对应了一个入栈和出栈的过程

本地方法栈: 本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是 Java 中使用 native 关键字修饰的方法所存储的区域

程序计数器:程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都通过程序计数器来完成

方法区:方法区是各个线程共享的内存区域,它用于存储虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据

:堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,所有的对象实例都会分配在堆上。JDK 1.7后,字符串常量池从永久代中剥离出来,存放在堆中

多线程

在多核操作系统上,JVM允许在一个进程内同时并发执行多个线程; JVM中的线程与操作系统中的线程是相互对应的,在JVM线程的本地存储、缓冲区分配、同步对象、栈、程序计数器等准备工作都完成时,JVM会调用操作系统的接口创建一个与之对应的原生线程

在JVM线程运行结束时,原生线程随之被回收; 操作系统负责调度所有线程,并为其分配CPU时间片,在原生线程初始化完毕时,就会调用Java线程的run()执行该线程;在线程结束时,会释放原生线程和Java线程所对应的资源

在JVM后台运行的线程主要有以下几个:

  • 虚拟机线程(JVM Thread):虚拟机线程在JVM到达安全点(SafePoint)时出现
  • ◎ 周期性任务线程:通过定时器调度线程来实现周期性操作的执行
  • GC线程:GC线程支持JVM中不同的垃圾回收活动
  • 编译器线程:编译器线程在运行时将字节码动态编译成本地平台机器码,是JVM跨平台的具体实现
  • 信号分发线程:接收发送到JVM的信号并调用JVM方法

Java 中的类加载机制?

Java 虚拟机负责把描述类的数据从 Class 文件加载到系统内存中,并对类的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称之为 Java 的类加载机制

一个类从被加载到虚拟机内存开始,到卸载出内存为止,一共会经历下面这些过程

类加载机制一共有五个步骤,分别是: 加载、链接、初始化、使用和卸载阶段,这五个阶段的顺序是确定的

其中链接阶段会细分成三个阶段,分别是验证、准备、解析阶段,这三个阶段的顺序是不确定的,这三个阶段通常交互进行; 解析阶段通常会在初始化之后再开始,这是为了支持 Java 语言的运行时绑定特性(也被称为动态绑定

下面我们就来聊一下这几个过程

加载

关于什么时候开始加载这个过程,《Java 虚拟机规范》并没有强制约束,所以这一点我们可以自由实现; 加载是整个类加载过程的第一个阶段,在这个阶段,Java 虚拟机需要完成三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流表示的一种存储结构转换为运行时数据区中方法区的数据结构
  • 在内存中生成一个 Class 对象,这个对象就代表了这个数据结构的访问入口

《Java 虚拟机规范》并未规定全限定名是如何获取的,所以现在业界有很多获取全限定名的方式:

  • 从 ZIP 包中读取,最终会改变为 JAR、EAR、WAR 格式
  • 从网络中获取,最常见的应用就是 Web Applet
  • 运行时动态生成,使用最多的就是动态代理技术
  • 由其他文件生成,比如 JSP 应用场景,由 JSP 文件生成对应的 Class 文件
  • 从数据库中读取,这种场景就比较小了
  • 可以从加密文件中获取,这是典型的防止 Class 文件被反编译的保护措施

加载阶段既可以使用虚拟机内置的引导类加载器来完成,也可以使用用户自定义的类加载器来完成; 程序员可以通过自己定义类加载器来控制字节流的访问方式

数组的加载不需要通过类加载器来创建,它是直接在内存中分配,但是数组的元素类型(数组去掉所有维度的类型)最终还是要靠类加载器来完成加载

验证

加载过后的下一个阶段就是验证

因为我们上一步讲到在内存中生成了一个 Class 对象,这个对象是访问其代表数据结构的入口,所以这一步验证的工作就是确保 Class 文件的字节流中的内容符合《Java 虚拟机规范》中的要求,保证这些信息被当作代码运行后,它不会威胁到虚拟机的安全

验证阶段主要分为四个阶段的检验:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

文件格式验证

这一阶段可能会包含下面这些验证点:

  • 魔数是否以 0xCAFEBABE 开头
  • 主、次版本号是否在当前 Java 虚拟机接受范围之内
  • 常量池的常量中是否有不支持的常量类型
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
  • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
  • Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息

实际上验证点远远不止有这些,上面这些只是从 HotSpot 源码中摘抄的一小段内容

元数据验证

这一阶段主要是对字节码描述的信息进行语义分析,以确保描述的信息符合《Java 语言规范》,验证点包括:

  • 验证的类是否有父类(除了 Object 类之外,所有的类都应该有父类)
  • 要验证类的父类是否继承了不允许继承的类
  • 如果这个类不是抽象类,那么这个类是否实现了父类或者接口中要求的所有方法
  • 是否覆盖了 final 字段,是否出现了不符合规定的重载等

需要记住这一阶段只是对《Java 语言规范》的验证

字节码验证

字节码验证阶段是最复杂的一个阶段,这个阶段主要是确定程序语意是否合法、是否是符合逻辑的

这个阶段主要是对类的方法体(Class 文件中的 Code 属性)进行校验分析;这部分验证包括

  • 确保操作数栈的数据类型和实际执行时的数据类型是否一致
  • 保证任何跳转指令不会跳出到方法体外的字节码指令上
  • 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是不能把父类数据类型赋值给子类等诸如此不安全的类型转换
  • 其他验证

如果没有通过字节码验证,就说明验证出问题; 但是不一定通过了字节码验证,就能保证程序是安全的

符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转换为直接引用的时候,这个转化将在连接的第三个阶段,即解析阶段中发生;

符号引用验证可以看作是对类自身以外的各类信息进行匹配性校验,这个验证主要包括

  • 符号引用中的字符串全限定名是否能找到对应的类
  • 指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用的类、字段方法的可访问性是否可被当前类所访问
  • 其他验证

这一阶段主要是确保解析行为能否正常执行,如果无法通过符号引用验证,就会出现类似 IllegalAccessError、NoSuchFieldError、NoSuchMethodError 等错误

验证阶段对于虚拟机来说非常重要,如果能通过验证,就说明你的程序在运行时不会产生任何影响

准备

准备阶段是为类中的变量分配内存并设置其初始值的阶段,这些变量所使用的内存都应当在方法区中进行分配

在 JDK 7 之前,HotSpot 使用永久代来实现方法区,是符合这种逻辑概念的;而在 JDK 8 之后,变量则会随着 Class 对象一起存放在 Java 堆中

下面通常情况下的基本类型和引用类型的初始值

解析

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程

  • 符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。
  • 直接引用:直接引用可以直接指向目标的指针、相对便宜量或者一个能间接定位到目标的句柄;直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中

这样说你可能还有点不明白,我再换一种说法:

在编译的时候一个每个 Java 类都会被编译成一个 class 文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段

《Java 虚拟机规范》并未规定解析阶段发生的时间,只要求了在 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield 和 putstatic 这 17 个用于操作符号引用的字节码指令之前,先对所使用的符号引用进行解析

解析也分为四个步骤

  • 类或接口的解析
  • 字段解析
  • 方法解析
  • 接口方法解析

初始化

初始化是类加载过程的最后一个步骤,在之前的阶段中,都是由 Java 虚拟机占主导作用,但是到了这一步,却把主动权移交给应用程序

对于初始化阶段,《Java 虚拟机规范》严格规定了只有下面这六种情况下才会触发类的初始化

  • 在遇到 new、getstatic、putstatic 或者 invokestatic 这四条字节码指令时,如果没有进行过初始化,那么首先触发初始化;通过这四个字节码的名称可以判断,这四条字节码其实就两个场景,调用 new 关键字的时候进行初始化、读取或者设置一个静态字段的时候、调用静态方法的时候
  • 在初始化类的时候,如果父类还没有初始化,那么就需要先对父类进行初始化。
  • 在使用 java.lang.reflect 包的方法进行反射调用的时候
  • 当虚拟机启动时,用户需要指定执行主类的时候,说白了就是虚拟机会先初始化 main 方法这个类
  • 在使用 JDK 7 新加入的动态语言支持时,如果一个 jafva.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,需要先对其进行初始化
  • 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

其实上面只有前四个大家需要知道就好了,后面两个比较冷门

如果说要回答类加载的话,其实聊到这里已经可以了,但是为了完整性,我们索性把后面两个过程也来聊一聊

使用

这个阶段没什么可说的,就是初始化之后的代码由 JVM 来动态调用执行

卸载

当代表一个类的 Class 对象不再被引用,那么 Class 对象的生命周期就结束了,对应的在方法区中的数据也会被卸载

但是需要注意一点:JVM 自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的

尾述

文章基本上就到这里,文章只是对Java中的JVM基础理论做了一些个人理解的东西,如有地方不对或者有不同理解的可以提出来

有需要获取更多Android相关资讯的同学 可以 点击 此处 即可 免费获取

现在点击还可以获得 更多《Android 学习笔记+源码解析+面试视频》

最后我想说:

对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们

当程序员容易,当一个优秀的程序员是需要不断学习的; 从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人

技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面

Android 架构师之路还很漫长,与君共勉

PS:有问题欢迎指正,可以在评论区留下你的建议和感受;

欢迎大家点赞评论,觉得内容可以的话,可以转发分享一下

上一篇下一篇

猜你喜欢

热点阅读