Java虚拟机
Java虚拟机的组成
Java虚拟机的组成由类加载器ClassLoader、运行时数据区域(JVM管理的内存)和执行引擎(即时遍历器、解释器垃圾回收器)
- 类加载器加载class字节码文件中的内容到内存
- 运行时数据区域负责管理jvm使用到的内存,比如创建对象和销毁对象
- 执行引擎将字节码文件中的指令解释称机器码,同时使用即时编译器优化性能
字节码文件的组成
字节码文件的组成由基础信息、常量池、字段、方法和属性
基础信息
魔数、字节码文件对应的Java版本号、访问标识(public final等等)、父类和接口
Magic魔数
- 文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容
- 软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该种类型就会出错
- Java字节码文件中国,将文件头成为magic魔数
文件类型 | 字节数 | 文件头 |
---|---|---|
jpg | 3 | FFD8FF |
png | 4 | 89504E47 |
bmp | 2 | 424D |
xml | 5 | 3C3F786D6C |
avi | 4 | 41564920 |
java字节码文件 | 4 | CAFEBABE |
主副版本号
- 主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不通过版本的标识,一般只需要关心主版本号
- 版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容
名称 | 作用 |
---|---|
Magic魔数 | 固定位0xCAFEBABE |
副版本号 | |
主版本号 | 编译字节码文件的JDK版本 |
访问标识 | 标识是类还是接口、注解、枚举、模块标识public final abstract |
类、父类、接口索引 | 通过这些索引可以找到类、父类、接口的信息 |
常量池
保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用
- 字节码文件中常量池的作用:避免相同的内容重复定义,节省空间
- 常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据
- 字节码指令中通过编号引用到常量池的过程称之为符号引用
方法
当前类或接口声明的方法信息字节码指令
- 操作数栈是临时存放数据的地方,局部变量表是存放方法中的局部变量的位置
类的生命周期
加载阶段
-
加载阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
-
类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中
生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息
-
同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象
作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)
- 推荐使用JDK自带的hsdb工具查看Java虚拟机内存信息。工具位于JDK安装目录下lib文件夹中的sa-jdi.jar中
- 启动命令:java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
连接阶段
连接阶段分为验证、准备和解析
验证的主要目的是验证内容是否满足《Java虚拟机规范》
准备的主要目的是给静态变量赋初值
解析的主要目的是将常量池中的符号引用替换成指向内存的直接引用
初始化阶段
- 初始化阶段会执行静态代码块中的代码,并为静态变量赋值
- 初始化阶段会执行字节码文件中clinit部分的字节码指令
- clinit方法中的执行顺序与Java中编写的顺序是一致的
- 添加-XX:+TraceClassLoading参数可以打印出加载并初始化的类
以下几种方式会导致类的初始化:
- 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且右边是常量不会触发初始化
- 调用Class.forName(String className)
- new一个该类的对象时
- 执行Main方法的当前类
clinit指令在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的
- 无静态代码块且无静态变量赋值语句
- 有静态变量的声明,但是没有赋值语句
- 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化
- 直接访问父类的静态变量,不会触发子类的初始化
- 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法
- 数组的创建不会导致数组中元素的类进行初始化
- final修饰的变量如果过赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化
类加载器
类加载器是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术
类加载器的分类
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的
-
虚拟机底层实现
源代码位于Java虚拟机的源码中,实现语言与虚拟机底层语言一致,比如Hotspot使用过C++
-
加载程序运行时的基础类
保证Java程序运行中基础类被正确地加载,比如java.lang.String,确保其可靠性
-
JDK中默认提供或者自定义
JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以自己根据需求定制
-
继承自抽象类ClassLoader
-
所有Java中实现的类加载器都需要继承ClassLoader这个抽象类
类加载器的设计JDK8和8之后的版本差别较大,JDK8及之前的版本中默认的类加载器有如下几种:
- 启动类加载器(Bootstrap),虚拟机底层实现(C++),加载Java中最核心的类
- 扩展类加载器(Extension),Java实现,允许扩展Java中比较通用的类
- 应用程序类加载器(Application),Java实现,加载应用使用的类
启动类加载器
- 启动类加载器(Bootstrap)是由Hotspot虚拟机提供的、使用C++编写的类加载器
- 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等
通过启动类加载器去加载用户jar包:
-
放入jre/lib下进行扩展
不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载
-
使用参数进行扩展
推荐,使用-Xbootclasspath/a:jar包目录/jar包名进行扩展
扩展类加载器
- 扩展类加载器是JDK中提供的、使用Java编写的类加载器
- 默认加载Java安装目录/jre/lib/ext下的类文件
- 扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器
- 它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备通过目录或者指定jar包将字节码文件加载到内存中
通过扩展类加载器去加载用户jar包:
-
放入jre/lib/ext下进行扩展
不推荐,尽可能不要去更改JDK安装目录中的内容
-
使用参数进行扩展
推荐,使用-Djava.ext.dirs=jar包目录进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加上原始目录
双亲委派机制
双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载
向上查找如果已经加载过,就直接返回Class对象,加载过程结束。这样就能避免一个类重复加载
向下委派加载起到了一个加载优先级的作用
双亲委派机制的作用:
-
保证类加载的安全性
通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性
-
避免重复加载
双亲委派机制可以避免同一个类被多次加载
每个类加载器都有一个父类加载器,在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器
- 如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载。所以看上去是自顶向下尝试加载
- 第二次再去加载相同的类,仍然会向上进行委派,如果某个类加载器加载过就会直接返回
在Java中主动加载一个类的方式:
- 使用Class.forName方法,使用当前类的类加载器去加载指定的类
- 获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载
-
每个Java实现的类加载器中保存了一个个成员变量叫“父”类加载器,可以理解为它的上级,并不是继承关系
-
应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是空,但是在代码逻辑上,扩展类加载器依然会吧启动类加载器当成父类加载器处理
-
启动类加载器使用C++编写,没有父类加载器