以下所有内容来自于 《深入理解 Java 虚拟机》这本书的理解和回顾,非照抄原文,肯定有错误,也有故意致错,一切皆为方便理解
Java 内存区域 奠定了Java 的基础模型,GC 在这其中周而复始,
那么在这之前,在 Java 程序运行之前,jvm 又做了那些事?
# 课前准备 - 类文件结构
类加载的是 class 文件,这里先大概了解以下 class 文件结构
概念
class 文件是专门给 jvm 使用的,对比机器码和操作系统,class 之于 jvm
各个平台上的 虚拟机都统一支持 class 文件
结构 :
将class 文件编译成二进制,前四个字节是“魔数”,用来做身份校验,判断是否是合法的 class 文件,类比猪肉盖章
5.6字节是次版本号,7.8是主版本号,jdk1.0主版本 45 ,新版本 + 1,判断是那个 JDK 编译的,以保证高低版本的兼容性.检测
之后就是常量池入口,访问标志等等,由字段表,属性表,方法表等组成
class 文件包含了一个程序代码的所有内容以及添加的一些额外东西,就像厨师炒菜前把菜品分门别类的准备好,放在一起
字节码指令 :
类似于 x86 指令集,Java 也有自己的指令集 ,指令参数都存放在操作数栈中
Java 虚拟机采用面向操作数栈的架构(桶状),windows 是面向寄存器(蜂窝状),前者效率低,不占空间,后者以空间换时间,
# 类加载
概念:Java 虚拟机把描述类的数据从 class 文件中加载到内存,并对数据进行校验,解析,初始化,最终形成可以直接被虚拟机使用的 Java 类型的过程
Java 的类加载是在程序运行期间完成,比之提前编译好,效率低,扩展性好(运行时可以动态链接)
过程 :
加载:
- classlode 根据 classpath 提供的全限定名获取该类的二进制字节流加载到内存(按照一定规则存放在方法区)
- 由于最终只需要一个二进制字节流,所以可以从任何地方获取,网络,磁盘,压缩包等等
- 加载过程也可以使用用户自定义类加载器
验证:如果有变态通过手敲 0 1 二进制代码,并放入内存,就可能存在安全性
- 文件格式验证:class 文件格式规范
- 元数据验证;对字节码描述的信息进行语义分析
- 字节码验证:验证分析出来的语义是合法的,安全的,由于该过程较为复杂且耗时,官方将尽可能多的验证提前到编译期
- 符号引用:在虚拟机将符号引用转化成直接引用时发生 – 解析式中端发生,检查引用是否存在,合法
准备:
- 静态变量分配内存,并设置初始值(零值,不是程序员代码的初始值)
解析:
- 符号引用替换为直接引用
在此之前的动作都有虚拟机主导完成,之后的步骤则有用户程序接管
初始化:
- 正式根据程序初始化类变量和其他资源
- 初始化阶段就是执行 () 方法的过程
# 双亲委派
在类加载过程中,加载阶段可以自定义类加载器,否则默认使用虚拟机的类加载器
那么虚拟机的类加载器是什么样子呢?
Java 的三层加载器
- 启动类加载器 Bootstrap Class Loader:加载 JDK 下 lib 文件中的类
- 扩展类加载器 Extension Class Loader:加载官方提供的扩展类库的类
- 应用程序类加载器:Application Class Loader:系统类加载器,加载用户类路径上的类库
双亲委派:
加载类时,首现由级别高的加载,没有时,再由下一级加载 ,遇到类加载请求时,先交给父类加载,父类找不到时,再有子类加载
如果一个人写了一个 String 类,里面存在恶意代码,Application Class Loader 直接就把这个类加载到内存中执行了,所有依赖于 String 的类就会有问题,等于是把官方的 String 替换掉了
反之,如果交到 启动类加载器手里,它在核心类库中找到了 官方的 String 类,此时就不会加载用户的恶意 String 的代码了
# 字节码执行
类加载完毕后,就开始执行,由于 Java指令集是基于 栈的,所有操作都在栈上进行
-
Java 虚拟机以 “栈帧” 作为最基本的执行单元
-
栈帧需要多大的局部变量表和操作数栈,在编译器已经确定
-
只有位于栈顶的方法才是运行的,只有位于栈顶的栈帧才是有效的
#分派
Java 中对方法的调用,确定目标方法的过程,多态性的体现,
静态分派:
在编译器确定,在类加载是解析
静态分派只涉及 重载 ,本质是根据方法的参数,确定实际的方法
动态分派:
概念:运行期根据方法接受者的实际类型选择方法版本
方法的重写就是使用动态分派,父子类同名方法是怎么识别的?
静态分派只涉及 重载 ,本质是根据方法的参数,确定实际的方法
动态分派:
概念:运行期根据方法接受者的实际类型选择方法版本
方法的重写就是使用动态分派,父子类同名方法是怎么识别的?
假设 A a = new B(子类),在执行过程中的 invokevirtual 指令,会根据实际类型,也就是 B 找到复合的方法,否则按照继承关系从上到下寻找最合适的方法,如果找不到,就抛出异常,这就是 重写 的本质