【JVM】浅看JVM的运行流程和垃圾回收

1.JVM是什么

JVM( Java Virtual Machine)就是Java虚拟机。

Java的程序都运行在JVM中。

2.JVM的运行流程

JVM的执行流程:

【JVM】浅看JVM的运行流程和垃圾回收_第1张图片
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。

2.1类加载子系统

对于一个类来说,它的生命周期是这样的:
【JVM】浅看JVM的运行流程和垃圾回收_第2张图片所以对于类加载来说,总共分为以下几个步骤:

  1. 加载
  2. 连接
    ①验证
    ②准备
    ③解析
  3. 初始化

1.连接

在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

总结:读取.class文件

2.连接

①验证

验证.class是否符合JVM的规范。

验证选项:

  • 文件格式验证
  • 字节码验证
  • 符号引用验证…

②准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

比如此时有这样一行代码:

public static int value = 123;

它是初始化 value 的 int 值为 0,而非 123。

③解析

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。

3.初始化

初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程。

4.双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

【JVM】浅看JVM的运行流程和垃圾回收_第3张图片

如上图所示:

  • BootStrap :启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即$JAVA_HOME/lib目录。
  • ExtClassLoader:扩展类加载器。加载 lib/ext 目录下的类。
  • AppClassLoader:应用程序类加载器:加载我们写的应用程序。
  • 自定义类加载器:根据自己的需求定制类加载器。

【JVM】浅看JVM的运行流程和垃圾回收_第4张图片

new一个我们自己创建的类时,先向上加载,到扩展类,如果没有找到这个类,再向上加载,询问启动类是否有,如果没有,再向下加载,一直到我们写的应用程序。

避免了恶意代码去修改JDK的风险。

2.2运行时数据区

JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称JMM)完全不同,属于完全不同的两个概念)

1.方法区

存放的是类对象,可以理解为对象的模板。(存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据)

在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)。

2.堆

存放的是new出来的对象。真正的对象的地址。

3.JVM虚拟机栈

每一个线程都有对应一个Java虚拟机栈,每调用一个方法都会以栈帧的形式加入到线程的栈中,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,方法执行完成之后栈帧就会被调出栈。栈主要记录的是方法的调用关系,还有可能会出现的栈溢出的错误。

【JVM】浅看JVM的运行流程和垃圾回收_第5张图片

4.本地方法栈

本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的。

5.程序计数器

记录当前线程的方法执行到哪一行。

3.垃圾回收

当方法结束或者线程结束时,内存就会跟着线程被回收。这种就称之为垃圾回收。
java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下几种算法。

3.1垃圾回收的过程

【JVM】浅看JVM的运行流程和垃圾回收_第6张图片

  • 新生代:一般创建的对象都会进入新生代的Eden中;
  • 老年代:大对象和经历了 N 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代 移动到老年代。
  • 老年代的大小时新生代的两倍。

垃圾回收的过程如下:

首先,创建对象进入Eden中:
【JVM】浅看JVM的运行流程和垃圾回收_第7张图片当Eden满了之后,(下图)将还活着的对象移入S0中,剩余的都是“死去“的对象(打红叉的对象为已死的对象),清空所有死去对象(垃圾回收)。再次创建新的对象。
【JVM】浅看JVM的运行流程和垃圾回收_第8张图片
当Eden又满了之后,(下图)将还活着的对象移入S1中,清空所有”死去的对象“。再次创建新的对象。
【JVM】浅看JVM的运行流程和垃圾回收_第9张图片交换S1和S0.
【JVM】浅看JVM的运行流程和垃圾回收_第10张图片当Eden又满了之后,(下图)将还活着的对象移入S0中,清空所有”死去的对象“。交换S1和S0.
【JVM】浅看JVM的运行流程和垃圾回收_第11张图片重复上述过程。存活了15轮的对象会被放入老年区,当老年区满了之后会进行一次老年区的垃圾回收。

3.2死亡对象的判断算法(JVM采用)

1.引用计数算法

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任
何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。

但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的
循环引用问题。

示例:

public class Test {
 	public Object instance = null;
 	private static int _1MB = 1024 * 1024;
 	private byte[] bigSize = new byte[2 * _1MB];
 	public static void testGC() {
 		Test test1 = new Test();  //第1行
 		Test test2 = new Test();  //第2行
 		test1.instance = test2;  //第3行
 		test2.instance = test1;  //第4行
 		test1 = null;  //第5行
 		test2 = null;  //第6行
 		// 强制jvm进行垃圾回收
 		System.gc();
	 }
 	public static void main(String[] args) {
 		testGC();
 	}
}

在这个代码中,编号按照代码中注释给出,第1,2行分别调用了GCDemo01一次,那么在堆上它们的计数器分别+1。第3,4行又分别再次调用了GCDemo01一次,那么在堆上它们的计数器都变为2.。但在第5,6行中,计算器虽然分别都减一,但test1和test2的instance再也无法访问到,所以堆中的引用计数器无法归0,导致垃圾无法被回收。
【JVM】浅看JVM的运行流程和垃圾回收_第12张图片

2.可达性分析算法

通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。以下图为例:

有引用关系的就会被标记为灰色。
【JVM】浅看JVM的运行流程和垃圾回收_第13张图片
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。

在Java语言中,可作为GC Roots的对象包含下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中 JNI(Native方法)引用的对象

3.3垃圾回收算法

通过上面的算法我们可以将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了,在正式学习垃圾收集器之前,我们先看下垃圾回收机器使用的几种算法(这些算法是垃圾收集器的指导思想)。

1.标记-清除算法

"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路并对其不足加以改进而已。

"标记-清除"算法的不足主要有两个 :

  1. 效率问题 : 标记和清除这两个过程的效率都不高
  2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

【JVM】浅看JVM的运行流程和垃圾回收_第14张图片

2.复制算法(新生代使用)

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图 :
【JVM】浅看JVM的运行流程和垃圾回收_第15张图片

3.标记-整理算法(老年代使用)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。

针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:

优点:在回收过后多了一步整理内存的工作
缺点:可以有大量连续的内存空间

【JVM】浅看JVM的运行流程和垃圾回收_第16张图片

3.4垃圾收集器(7种)

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。

垃圾收集器不断更新的目的减少STW的时间(Stop The World)(STW:每次进行垃圾回收的时候,程序会进入暂停状态)

以下这些收集器是 HotSpot 虚拟机随着不同版本推出的重要的垃圾收集器:
【JVM】浅看JVM的运行流程和垃圾回收_第17张图片

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使用。

  • Serial收集器:Serial 收集器是最基本、发展历史最悠久的收集器,这个收集器是一个单线程的收集器,是虚拟机运行在Client模式下的默认新生代收集器,与其他收集器的单线程比简单而高效。与Serial Old配对使用。
  • ParNew收集器:其实就是Serial收集器的多线程版本。对于Serial的优化,从串行变成并行,用多线程的方式扫描内存,提高垃圾回收的效率,减少STW的时间。
  • Parallel Scavenge收集器(新生代收集器,并行GC): Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。与Parallel Old配对使用。
  • CMS收集器(老年代收集器,并发GC):使用的是三色标记算法。
  • G1收集器(唯一一款全区域的垃圾回收器):G1收集器不分老年代和新生代。

补充

1.Minor GC和Full GC的区别

面试题 : 请问了解Minor GC和Full GC么,这两种GC有什么不一样吗?

  1. Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
  2. Full GC 又称为老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

2.4个引用

【JVM】浅看JVM的运行流程和垃圾回收_第18张图片

你可能感兴趣的:(Javaee,jvm,算法,java)