JVM虚拟机学习(一)
1. 概念
1.1 JVM基本概念
JVM(Java虚拟机)是一个抽象的计算模型。就如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。目的是为构建在其上运行的应用程序提供一个运行环境。JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。本文主要对JVM进行概述,并介绍Java程序是如何在上面执行的。
有趣的是,其实JVM并不关心Java语言或其他编程语言的语义和语法结构。当JVM执行一段程序的时候,它主要关注的是一种称为“类文件”的特定文件格式。.class类文件格式和Java代码定义的面向对象的类结构毫无关系。编译器将.java文件编译成*.class文件,然后JVM对*.class文件进行解译,它不关心这个类文件是由哪种编译器生成的,只要符合类文件的文件格式即可。Java编译器将一段程序编译为等价的类文件。这些类文件实际上包含了半编译的代码——字节码。之所以称之为半编译,是因为字节码并不像C/C++编译器编译的二进制文件一样会被直接执行。字节码要先被输入到JVM中,然后再转换为底层平台可以执行的最终指令。所以字节码包含了JVM的指令、符号表和其他的辅助信息。不管何种语言,能根据JVM的语法和结构约束编译生成字节码的编译器,都是一个可以在JVM上执行的候选者。
JVM将自身定位于字节码和底层平台之间。底层平台是指操作系统(OS)和硬件。操作系统和硬件体系结构在不同的机器上可能不同,但是同一段Java程序可以不用做任何的代码修改就能在不同的机器上运行。这是在虚拟环境中执行的程序语言的独特之处。例如,由其他程序语言编译器编译的目标代码如C++和Java相比的不同点在于,C++程序需要被特定平台的编译器重新编译,从而使它能在不同的体系结构上面运行。而Java代码并不需要做任何改变,因为由Java编译器编译的字节码是在外围的JVM上执行。因此,JVM负责重新解译由Java编译器生成的字节码,并和底层平台协调工作。也就是说,尽管Java编译器生成的结果是平台独立的,但JVM与特定平台相关的。除非两台机器有相同的体系结构,在某个体系结构上安装和使用的JVM可能换一台机器就不能正常工作了。
划重点:
JVM由三个主要的子系统构成:
- 类加载器子系统
- 运行时数据区(内存)
- 执行引擎
1.2 JVM、JDK、JRE
想要运行Java程序,我们需要JVM因为它提供了字节码的运行环境。Oracle提供了两种不同的产品:JDK(Java开发工具)和JRE(Java运行环境)。JRE是我们安装运行Java程序的最基本软件。它和Java类库以及运行Java程序所需要的其他组件一起够成了JVM的一个实现。所以,如果我们想运行一个类文件或一段字节码,仅需要JRE就够了。而JDK(Java开发工具)是JRE的超集。它包含了JRE提供的所有东西,包括创建类文件的工具如Java编译器、调试器和其他许多开发Java程序相关的工具。所以,当我们要创建类文件(编译Java源码)时,我们就需要JDK。
简单地说,Java运行时编译源码(.java)成字节码,由jre运行。jre由java虚拟机(jvm)实现。Jvm分析字节码,后解释并执行。
此部分内容可以参考。
2. 垃圾回收(GC)
1. 如何识别垃圾,判定对象是否可被回收?
引用计数法:给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象计数器是否为0来判断对象是否可被回收。
缺点:无法解决循环引用的问题
根搜索算法:也称可达性分析法,通过“GC ROOTs”的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收(可作为GC ROOTs的对象:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象)
2. Java 中的堆是 GC 收集垃圾的主要区域,GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
Minor GC:新生代(Young Gen)空间不足时触发收集,由于Java 中的大部分对象通常不需长久存活,新生代是GC收集频繁区域,所以采用复制算法。
Full GC:老年代(Old Gen )空间不足或元空间达到高水位线执行收集动作,由于存放大对象及长久存活下的对象,占用内存空间大,回收效率低,所以采用标记-清除算法。
3. 常见垃圾回收算法
3.1 引用计数法
实际上,引用计数法,就是对一个对象被引用的次数进行计数,当增加一个引用计数就加1,减少一个引用计数就减1。
引用计数算法原理非常简单,是最原始的回收算法,但是java中没有使用这种算法,原因主要有以下两点:
- 频繁的计数影响性能
- 无法处理循环引用的问题
此处,想解释一下什么是循环引用,比如
public void YTT()
{
Dog d = new Dog();
Cat cl = new Cat();
d.c= c;
c.d = d;
}
在这里,c中拿着对d的引用,d中拿着对c的引用。如果d要被回收,前提是c被先回收,这样才能释放对d的引用。但是反回过来,c要被回收的前提是d要被先回收。当d函数退出后,看起来垃圾回收管理似乎就始终无法回收这两个实际已经不再需要的对象。
3.2 标记-清除(Mark-Sweep)算法
标记清除算法,它是很多垃圾回收算法的基础,简单来说有两个步骤:标记、清除。
就是当程序运行期间,若可以使用的内存被耗尽的时候,垃圾回收线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。
缺点:
- 效率比较低(递归与全堆对象遍历),而且在进行垃圾回收的时候,需要停止应用程序,这会导致用户体验非常差劲。
- 这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
3.3 标记-整理算法
标记-整理算法:分为两阶段“标记”和“整理”。首先标记出哪些对象可被回收,在标记完成后,将对象向一端移动,然后直接清理掉边界以外的内存。实际上,标记-整理算法就是多了一个整理内存的工作,力求解决内存碎片问题。
3.4 复制算法
把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。
不足之处:
- 内存利用率问题
- 在对象存活率较高时,其效率会变低。
3.5 增量收集
实时垃圾回收算法,即:在应用进行的同时进行垃圾回收,理论上可以解决传统分代方式带来的问题。增量收集把对堆空间划分成一系列内存块,使用时先使用其中一部分,垃圾收集时把之前用掉的部分中的存活对象再放到后面没有用的空间中,这样可以实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的回收的情况。
3.6 分代收集
基于对象生命周期划分为新生代、老年代、元空间,对不同生命周期的对象使用不同的算法进行回收。
3.7 串行收集算法
使用单线程处理垃圾回收工作,实现容易,效率较高。
不足之处:
- 无法发挥多处理器的优势
- 需要暂停用户线程
3.8 并行收集
使用多线程处理垃圾回收工作,速度快,效率高。理论上CPU数目越多,越能体现出并行收集器的优势。
垃圾线程与用户线程同时工作。系统在垃圾回收时不需要暂停用户线程。