一.简介
JVM(Java Virtual Machine)全称:Java 虚拟机。
JVM既然是“虚拟”机,也就是说JVM不是真实的计算机,而是真实计算机中的一个进程,虚拟出来的一台计算机 。jvm是java得以在几乎所有系统能够运行的基础。
二.java规范与具体实现
1.java规范
Java是开源的编程语言,既然是开源就允许其他组织和平台对Java做一定修改和扩展。JVM作为Java语言重要的组成部分,当然也允许其他组织、公司或个人对JVM做具体实现。Java官方在20多年前就理解了“内卷”的魅力。允许其他组织、公司或个人针对自己的情况来对JVM做具体实现。
2.java具体实现
JVM名称 | 说明 |
---|---|
HotSpot | Oracle官方对JVM规范的实现 |
jRocket | 曾经JVM的扛把子,号称最快的虚拟机,后被Oracle收购,和HotSpot整合 |
J9 | IBM对JVM规范的实现 |
TaobaoVM | HotSpot深度定制版 |
azul zing | 商业收费版 |
3.虚拟机
我们使用的是Java 官方对JVM规范的具体实现:HotSpot 虚拟机。
三. 深入理解类加载机制/类的生命周期
1.类加载器
2.类的生命周期
1.加载
(1)启动类加载器(优先级最高)
负责加载jar包。包含所有核心类,String,System等
(2)扩展类加载器
负责加载扩展类。具体就是JDK目录/jre/lib/ext。
(3)应用程序加载器
负责加载路径中字节码文件
2.链接
(1)校验
校验加载的字节码文件是否正确
(2)准备
所有静态变量初始化并赋予默认值
(3)解析
把符号引用转换为直接引用(若类变量为常量即被final修饰,则直接赋值开发者定义的值)
(4)初始化
执行静态代码块和静态变量赋值
四.类加载器
1.sun.misc.Launcher 是JVM的入口类。
2.介绍
2.1 parent属性说明
ClassLoader是Java中提供的类加载器父类。所有的类加载器都是这个类的子类或子孙类。
提供了全局属性parent,这意味着类加载器之间具有逻辑父子关系(不是继承关系)。
2.2 native关键字介绍
当方法使用native修饰时表示方法的具体实现是通过其他语言进行实现的。Java语言在操作内存或硬件方面多使用C/C++进行实现,这些方法需要通过native进行修饰。
3. 加载器的父子关系
3.1 获取类加载器
可以通过:类名.class.getClassLoader()进行查看类是由哪种加载器进行加载。getClassLoader()返回值为ClassLoader类对象。
其中:类.class 表示获取字节码文件对象。类.class.getClassLoader()表示由哪个加载器对象加载这个字节码文件。
3.2 获取父加载器
ClassLoader中包含getParent()方法,表示获取当前加载器的父加载器。
3.3 为什么ExtClassLoader的父加载器是null
在Launcher中并没有BootstrapClassLoader类。因为Java中并没有提供BootstrapClassLoader类,而是通过C/C++语言编写的。既然Java中没有这个类所以我们在获取ExtClassLoader的父加载器时自然为null。
4. 双亲委派机制
1. 委派的过程就是一层一层向上找的过程。只要当前加载器加载过,就不会重新加载。如果没有加载过,会向上寻找是否加载过。
2. 当加载到Bootstrap ClassLoader后会一层一层的向下判断是否可以进行加载,如果能加载则加载。如果不能加载向下一层寻找下个加载器是否能加载。如果到最后一层都无法加载则报ClassNotFoundException。
五. JVM内存结构
1. JVM内存结构图
2. 源文件
源文件就是我们编写Java代码的文件。文件扩展名为.java。
3. 字节码文件
字节码文件是源文件编译后的文件。字节码文件是二进制文件,需要通过特定的工具才能查看。里面存放了源文件编译后的字节码指令。
4. 类加载器 Class Loader
Java 程序运行时会由类加载器负责把.class的字节码文件装在到内存中,供虚拟机执行。
4.1 加载 Loading
-
启动类加载器 BootStrap Class Loader
负责从启动类中加载类。具有最高执行优先级。即:rt.jar等。
-
扩展类加载器 Extension Class Loader
负责加载扩展相关类。即:jre/lib/ext 目录
-
应用程序加载器 Application Class Loader
加载应用程序类路径(classpath)中相关类
4.2 链接 Linking
-
校验 Verify
校验器会校验字节码文件是否正确。
-
准备 Prepare
所有静态变量初始化并赋予默认值
-
解析 Resolve
符号引用被换成直接引用。
4.3 初始化 Initialization
所有静态变量赋予初值,静态代码块执行。
5. 执行引擎
运行时数据区的字节码会交给执行引擎执行。
5.1 解释器 Interpreter
解释器负责解释字节码文件。每次方法调用都会被重新解释。
5.2 JIT编译器
当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为 热点代码。为了提高热点代码的执行效率,在运行时虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,简称 JIT 编译器)。
5.3 探测器
负责探测多次被调用的代码。
5.4 垃圾回收器 GC
负责回收不在被使用的对象。
6. 本地库接口
在Java代码中使用native修饰的方法表示方法具体实现使用其他编程语言实现的
7. 本地方法库
所有的本地方法,通过本地库接口调用。
8. 程序计数器
程序计数器简称:PC Register。
程序计数器是一块较小的内存空间。记录了当前线程执行到的字节码行号。每个线程都有自己的程序计数器,相互不影响。如果是native方法,计数器为空。
9. 虚拟机栈
虚拟机栈跟随线程创建而创建,所以每个线程都有一个虚拟机栈。
虚拟机栈中存储的是栈帧(frames),每个栈帧对应一个方法,每个栈帧都有自己的局部变量表、操作数栈、动态链接和返回地址等。当前正在执行的方法称为当前方法,当前方法所在的帧称为当前帧。方法执行时帧就是一个入栈操作,方法执行完成之后栈帧就是一个出栈操作。
9.1 局部变量表
局部变量表存储的8大基本数据类型和返回值以及方法参数及对象的引用。 其中long和double占用2倍长度。
局部变量表就是一个数组,数组的长度在编译期确定。通过从0开始的索引调用局部变量表的内容。
9.2 操作数栈
操作数栈存在于栈帧中,其大小在编译期确定。
操作数栈中存储了class文件中虚拟机指令以及准备要传递的参数和接收对方的返回结果。
运行时常量池中数据以及局部变量表中得值都可以由操作数栈进行获取。
9.3 动态链接
符号引用转换为直接引用分为两种情况。
在JVM加载或第一次使用转换时称为静态链接或静态解析。而在运行期间把符号转换为直接引用时就称为动态链接。
9.4 方法返回地址
方法返回地址分为两种情况:
1. 正常结束执行。例如碰见return关键字。调用程序计数器的值后当前栈帧直接出栈就可以了。
2. 异常结束。可能需要恢复上层方法的局部变量表和操作数栈,然后把返回值压如到栈帧的操作数栈中,之后调用程序计数器的值后获取到下条指令。
10. 堆
1.堆是所有线程共享的,存储类的实例和数组。
2.堆是在虚拟机启动时创建的,由GC负责回收。
3.堆可以是一块不连续的内存空间。
4.在Java 8 中,String是存在于堆中的。
5.堆分为新生代和老年代。永久代是HotSpot中实现方法区规范的,与新生代和老年代连续
新生代又被分为Eden区、From Survivor区、To Survivor区。官方说明默认分配比例为8:1:1。但是使用jmap工具进行测试时发现比例为6:1:1。
字符串常量池在堆中。
11. 方法区
方法区是线程共享的。方法区可以理解为编译代码存储区。在方法区中存储每个类的结构、运行时常量池、字段、方法、构造方法。
六. GC 垃圾回收器
1.简介
垃圾回收器( garbage collection,简称GC)负责回收JVM运行时数据区的堆内存和方法区中数据。而虚拟机栈、程序计数器、本地方法栈都是根据线程创建而创建,随着线程销毁而销毁,所以不需要进行回收。
2 可达性分析:
可达性算法没有引用计数算法中循环引用无法被回收的问题。
3. GC回收算法
3.1 标记清除算法(Mark-sweep)
缺点:
效率低。碎片多。
3.2 标记压缩算法(Mark-Compact)
和标记清除算法有点类似。主要区别是标记完成后并不会直接清除,而是把所有不回收对象先向一端移动,然后在清除掉边界外面的对象。这样就不会产生内存碎片。
3.3 复制算法(copying)
内存按照容量分为大小相等的两块。每次只使用一块。当一块使用完成后,把存活的对象复制到另一个空间,然后把空间一次清除掉。
缺点:可用内存减少。
3.4 分代收集算法
把堆分为新生代和老年代。新生代采用一种算法,老年代采用采用算法。具体新生代和老年代采用的算法需要看使用哪种垃圾回收器。