一、JVM简介
1.基本概念
JVM(Java Virtual Machine)全称:Java虚拟机
JVM并不是真实的计算机,而是在真实计算机中的一个进程,是虚拟出来的一台计算机。
所有的Java程序都是在JVM中运行的。JVM形成了对Java程序的保护。
只要平台(系统)能运行JVM就能运行Java程序,且Java程序运行环境是一样的,与外部平台没有关系。
所以Java程序可以跨平台、安全。
二、JVM规范与具体实现
1.官方文档
官方文档地址:https://docs.oracle.com/javase/specs/index.html
在Java官方文档中,每个版本官方都提供了两个版本的文档地址。
2. 什么是规范
Oracle官方通过文字说明,某个版本的JVM应该如何去实现、应该包含哪些内容,这些规定就是规范。
3.我们使用的是哪种虚拟机
HotSpot虚拟机
在HotSpot实现过程中也提供一些参数,让开发者可以调试它(所谓的JVM调优其实就是调整设置这些参数)。
HotSpot:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html
4.一些主流的JVM规范具体实现
JVM名称 | 说明 |
---|---|
HotSpot | Oracle官方对JVM规范的实现 |
jRocket | 曾经JVM的扛把子,号称最快的虚拟机,后被Oracle收购,和HotSpot整合 |
J9 | IBM对JVM规范的实现 |
TaobaoVM | HotSpot深度定制版 |
azul zing | 商业收费版 |
三、类加载机制/类的生命周期(面)
1.类加载器
1.1 介绍
类加载器就是把class文件加载到JVM中去
类加载器本身就是一个Java类,所以也需要被加载,这由启动类加载器Bootstrap ClassLoader来完成(C/C++写的本地的类加载器)
它是JVM的内核,无需再被类加载器加载。
1.2 加载
1.启动类加载器(Bootstrap ClassLoader) 是优先级最高的加载器,最先进行加载。负责加载JDK目录/jre/lib 中相关的jar包。包含了所有的核心类。
如:String、System等
2.扩展类加载器(Extension ClassLoader):负责加载扩展类。具体就是JDK目录/jre/lib/ext
3.应用程序加载器(Application ClassLoader)负责加载类路径中字节码文件,也就是classpath中的内容。
总结:
类加载器加载的都是class文件,加载到JVM中。
启动类加载器加载 核心jar包
扩展类加载器加载拓展类
应用程序加载器加载自己写的类
1.3 链接(Linking)
1.校验(verify):校验加载的字节码文件是否正确。
进行4 个阶段的校验:
①文件格式校验
②元数据校验
③字节码验证
④符号引用验证
2.准备(prepare):所有静态变量初始化(分配空间)并赋予默认值。
注意点:实例变量此时还没有分配内存;若静态变量被final修饰,不再赋予默认值而是赋开发者指定的值。
(1)该阶段进行的内存分配仅包括类的静态变量(static),不包括实例变量;实例变量是在对象被实例化时随着对象一起被分配在JVM的堆中。
(2)静态变量初始赋值通常是数据类型对应的默认值,而不是代码中赋的值
①基本数据类型:对于静态变量(static)和全局变量,会为其赋予默认值。而局部变量使用前都必须显示的在代码中赋值,否则编译时不通过。
②同时被static和final修饰的常量,必须在声明的时候就在代码中显式的为其赋值,否则编译时不同过;
而只被final修饰的常量则可以声明时显式赋值,也可以在类初始化时显示赋值,总之使用前必须赋值,因为系统不会为其赋默认值。
③引用数据类型:没有显示赋值就会被赋予默认值null
④如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素会根据各自的数据类型而被赋予对应的默认值。
3.解析(resolve):把符号引用转换为直接引用。
(1)实例变量不在该阶段分配内存
(2)因为类方法和私有方法符合”编译器可知、运行期不可变“的要求,即不会被继承或重写,所以适合在类加载过程中解析。
(3)若类变量为常量(final修饰),则直接赋值为开发者定义的值。
符号引用
比如程序中的main方法,写法是固定的,我们可以把它当做一个符号。
这些符号被加载到JVM中都会对应一个地址,“将符号引用转换为直接引用”指的就是:将这些符号转变成对应的地址,这些地址就是直接引用。根据这些地址能真正的对数据进行操作。
1.4 初始化(Initialization)
执行静态代码块和静态变量赋值。
1.5 运行
运行字节码文件。
1.6 卸载
运行结束后进行卸载。
四、类加载器源码分析
1.Launcher类介绍
sun.misc.Launcher是JVM的入口类
全局属性如下:
2.ClassLoader类
2.1 parent属性
ClassLoader是Java提供的类加载器的父类。
所有类加载器都是这个类的子类或子孙类。
提供了全局变量属性parent,这意味着类加载器之间具有逻辑父子关系(并不是继承关系)
2.2 native关键字
Java中除了abstract 修饰的方法没有方法体,native修饰的方法也没有方法体。
native修饰的方法表示该方法具体实现是通过其他语言进行实现的。
Java在操作内存或硬件多用C/C++进行实现,而这些方法都需要通过native修饰。
3.加载器的父子关系
3.1 获取类加载器
可以通过:类名.class.getClassLoader()进行查看类是由哪种加载器进行加载的。
getClassLoader() 返回值为ClassLoader类对象。
3.2 获取父加载器
ClassLoader中包含getParent()方法,表示获取当前加载器的父加载器。
3.3 为什么ExtClassLoader的父加载器是null
类加载器的逻辑父子关系 :从上到下为由父到子
为什么ExtClassLoader获取的父加载器不是BootstrapCalssLoader而是null呢?
在Launcher中并没有BootstrapClassLoader类。因为Java中并没有提供BootstrapClassLoader类,而是通过C/C++语言编写的。既然Java中没有这个类所以我们在获取ExtClassLoader的父加载器时自然为null。
但是这三个加载器依然是具有逻辑父子关系的(再次强调:不是继承)。
4. 双亲委派机制
代码体现在java.lang.ClassLoader中的loadClass()方法
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编译器
Java程序在运行的时候,主要就是执行字节码指令,一般这些指令会通过解释器(Interpreter)进行解释执行,这种就是解释执行。
当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为 热点代码。为了提高热点代码的执行效率,在运行时虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,简称 JIT 编译器)。
5.3 探测器
负责探测多次被调用的代码。
5.4 垃圾回收器 GC
负责回收不在被使用的对象。GC是JVM中非常重要的一块,在后面我们会单独讲解GC。
6. 本地库接口
在Java代码中使用native修饰的方法表示方法具体实现使用其他编程语言实现的。例如:C语言。通过本地库接口为Java程序提供调用其他语言的实现方案。
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. 堆
堆是所有线程共享的,存储类的实例和数组。
堆是在虚拟机启动时创建的,由GC负责回收。
堆可以是一块不连续的内存空间。
在Java 8 中,String是存在于堆中的。
堆被分为二大部分:
在Java 7时分为:新生代(Young Generation)、老年代(Old Generation)、永久代。且新生代、老年代和永久代是连续的。
新生代又被分为Eden区、From Survivor区、To Survivor区。官方说明默认分配比例为8:1:1。但是使用jmap工具进行测试时发现比例为6:1:1。
在Java 8时把永久代替换为元空间(MetaSpace),也就是说在Java8中使用元空间来实现方法区。且在Java8中把元空间移植到本地内存上(Native Memory),其实在Java 7 时,部分数据已经移植到本地内存上了。例如:符号引用(Symbols)。
字符串常量池在堆中。
11. 方法区
方法区是线程共享的。
在虚拟机启动时自动创建方法区,方法区可以是一块不连续的内存空间。
方法区可以理解为编译代码存储区。在方法区中存储每个类的结构、运行时常量池、字段、方法、构造方法。
在JVM规范上方法区是一个独立的区域,但是在Java SE7 的HotSpot 上方法区使用永久代作为实现,永久代和堆是一块连续空间。在Java SE8的JVM规范实现上,HotSpot使用元空间(本地内存)实现方法区。
六、GC垃圾回收器
1.简介
垃圾回收器( garbage collection,简称GC)负责回收JVM运行时数据区的堆内存和方法区中数据。而虚拟机栈、程序计数器、本地方法栈都是根据线程创建而创建,随着线程销毁而销毁,所以不需要进行回收。
2.GC判断对象是否可以回收
2.1 引用计数(已淘汰)
引用计数算法就是看对象是否被引用。如果引用则对象计数器加一。如果释放引用计数器减一。但是引用计数算法最大的问题就是循环引用问题。当出现循环引用时对象计数器至少为1.这时候对象可能已经是垃圾了,但是无法被回收。
2.2 可达性分析
可达性算法没有引用计数算法中循环引用无法被回收的问题。
其主要思路是通过一系列名为GC Roots的对象作为根,从根开始往下搜索,搜索过程经过的路径称为引用链(Reference Chain),当一个对象到达GC Roots时表示当前对象还在使用,如果没有引用的或者和其他非GC Roots循环引用的内容都是垃圾。静态变量、线程变量、常量池、JNI(指针)都是GC Roots
3. GC回收算法
3.1 标记清除算法(Mark-sweep)
先标记出所有需要回收的对象。标记完成后统一回收所有标记的对象。
缺点:内存碎片多(造成不连续的内存空间)。
3.2 标记压缩算法(Mark-Compact)
又叫标记整理算法。
和标记清除算法类似。主要区别是标记完成后并不会直接清除,而是把所有不回收对象先向一端移动,然后在清除掉边界外面的对象。这样就不会产生内存碎片。
3.3 复制算法(copying)
目的:为了解决标记清除算法的碎片问题。
步骤:内存按照容量分为大小相等的两块。每次只使用一块。当一块使用完成后,把存活的对象复制到另一个空间,然后把空间一次清除掉。
缺点:可用内存减少。
3.4 分代收集算法
把堆分为新生代和老年代。
新生代采用一种算法,老年代采用一种算法。具体新生代和老年代采用的算法需要看使用的哪种辣鸡回收器。
4. GC种类
4.1 种类
4.2 可以组合的关系
5.GC 详细介绍
5.1 Serial、Serial Old 串行收集器
5.1.1 Serial 收集器
起源于JDK 1.3,单线程执行,每次回收必须STW。
应用场景:虚拟机在client模式下默认的GC。
优点:简单高效。
5.1.2 Serial Old 收集器
老年代收集器。标记整理算法。单线程。主要应用在client模式下老年代收集。在JDK1.5之前可以与Parallel Scavenge配合使用。可作为CMS的备选。
5.2 ParNew 收集器
Serial 收集器多线程版本,用于收集新生代。可与CMS配合使用。
ParNew可以并行执行,主要为了减少STW的时间,加快程序响应,给用户提供良好的体验。
5.3 Parallel Scavenge 收集器
新生代收集器。采用复制算法。可以并行执行。
优点:
具备自适应调节能力。-XX:+UseAdaptiveSizePolicy
主要解决吞吐量问题。也被称为“吞吐量优先”收集器。即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。高吞吐量可以高效的利用CPU时间,尽快完成运算任务。
5.4 Parallel Old 收集器
老年代收集器。标记整理算法。多线程。JDK 1.6中出现。
5.5 CMS收集器(Concurrent Mark-Sweep Collector)
5.5.1介绍
主要为了减少STW时间。
5.5.2步骤
采用标记清除算法:
初始标记:初始标记只是标记下GC Roots能够关联的对象。速度很快。需要STW
并发标记:进行GC Roots Tracing的过程。不需要STW
重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。需要STW
并发清除:并发的清除对象。不需要STW
5.5.3优缺点
优点:
并发收集。低停顿。
缺点:
对CPU非常敏感,对于CPU大于4核时要求并发收集时使用的线程数不小于1/4。但随着CPU增加而下降。
可能产生浮动垃圾。因为CMS清理阶段程序还在运行,所以就可能产生新的垃圾,这部分垃圾只能等到下次才能被清理。所以称为浮动垃圾。
可能产生大量空间碎片。
5.6 G1收集器(garbage-first)
5.6.1介绍
JDK8中主推的收集器。属于CMS的替代品。
G1收集器时堆中的年轻代和老年代只是逻辑上的概念,实际上把堆(一块连续内存)分为很多Region(分区)每个分区里面又被分为多个卡片(Card)。所以里面可能有很多年轻代和老年代。G1收集器里面多了一个新的概念:humongous(巨型对象)。当对象达到或超过Region一半时称为巨型对象。巨行对象独占一个或多个连续的Region。
5.6.2步骤:
- 初始标记:初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值。此过程需要STW,但是耗时很短。
- 并发标记:并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象。此过程耗时可能较长于用户操作并发执行,不需要STW。
- 重新标记。标记因为并发标记时用户执行产生的垃圾。需要STW(也可以并行)
- 筛选回收。对各个Region收回价值和时间进行计算,筛选出符合用户设定的预期回收时间。
6. G1收集器和CMS收集器的区别
- G1是用在新生代和老年代同时使用。CMS是老年代收集器。
- G1是Java8主推的收集器。CMS是java5出现的收集器。
- G1的STW时间可由用户设定,在筛选回收过程“可预测”的想办法满足设定要求。CMS是尽可能的减少STW时间
- G1使用的是标记整理算法,CMS使用的是标记清除算法(所以可能有内存碎片)。
- G1回收的流程是初始标记、并发标记、最终标记、筛选回收。CMS的流程是:初始标记、并发标记、重新标记、并发清除。