JVM(Java虚拟机)

目录

1.JVM 简介

1.1 JVM 发展史

1.Sun Classic VM

2.Exact VM

3.HotSpot VM

4.JRockit

5.J9 JVM

6.Taobao JVM(国产研发)

1.2 JVM 和《Java虚拟机规范》

2. JVM 运行流程

JVM 执行流程

3. JVM 运行时数据区

3.1 堆(线程共享)

3.2 Java虚拟机栈(线程私有)

什么是线程私有?

3.3 本地方法栈(线程私有)

3.4 程序计数器(线程私有)

3.5 方法区(线程共享)

JDK 1.8 元空间的变化

运行时常量池

小结 

3.6执行引擎

3.7本地方法接口

3.8内存布局中的异常问题

① Java堆溢出

② 虚拟机栈和本地方法栈溢出

4.JVM 类加载

4.1 类加载过程

1) 加载

2) 验证

3) 准备

4) 解析

5) 初始化

4.2 双亲委派模型

什么是双亲委派模型?

​编辑

双亲委派模型的优点

4.3 破坏双亲委派模型

5.垃圾回收相关

5.1 死亡对象的判断算法

a) 引用计数算法

b) 可达性分析算法(Java中使用) 

5.2垃圾回收算法

a) 标记-清除算法

b) 复制算法(新生代)

c) 标记-整理算法(老年代)

d) 分代算法

垃圾回收过程

哪些对象会进入新生代?哪些对象会进入老年代?

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

5.3 垃圾收集器

1) Serial收集器(新生代收集器,串行GC)

2) ParNew收集器(新生代收集器,并行GC)

3) Parallel Scavenge收集器(新生代收集器,并行GC)

4) Serial Old收集器(老年代收集器,串行GC)

5) Parallel Old收集器(老年代收集器,并行GC)

6) CMS收集器(老年代收集器,并发GC)

7) G1收集器(唯一一款全区域的垃圾回收器)

5.4总结:一个对象的一生


1.JVM 简介

JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。

虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。

常见的虚拟机:JVM、VMwave、Virtual Box。

JVM 和其他两个虚拟机的区别:

  • 1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
  • 2. JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进 行了裁剪。

JVM 是一台被定制过的现实当中不存在的计算机。

1.1 JVM 发展史

1.Sun Classic VM

在1996年Java1.0版本的时候,Sun公司发不了一款名为Sun Classic vm的java虚拟机,它同时也是世 界上第一款商业java虚拟机,jdk1.4 时完全被淘汰。

这款虚拟机内部只提供解释器。

现在Hotspot内置了此虚拟机;

2.Exact VM

为了解决上一个虚拟机问题,jdk1.2时,sun提供了此虚拟机。

Exact 具备现代高性能虚拟机的雏形,包含了一下功能:

  • 1. 热点探测(将热点代码编译为字节码加速程序执行);
  • 2. 编译器与解析器混合工作模式。

3.HotSpot VM

HotSpot 历史

  • 1. 最初由一家名为“Longview Technologies”的小公司设计;
  • 2. 1997年,此公司被Sun收购;2009年,Sun公司被甲骨文收购。
  • 3. JDK1.3时,HotSpot VM成为默认虚拟机

4.JRockit

JRockit 是专注于服务器端应用,目前在HotSpot的基础上,移植JRockit的优秀特性。

5.J9 JVM

全称:IBM Technology for Java Virtual Machine,简称IT4J,内部代号:J9。

市场定位于HotSpot接近,服务器端、桌面应用、嵌入式等多用途JVM,广泛用于IBM的各种Java产品。

目前,有影响力的三大商用虚拟机之一,也号称是世界上最快的Java虚拟机(在IBM自己的产品上稳定);

6.Taobao JVM(国产研发)

由 AliJVM 团队发布。阿里,国内使用Java最强大的公司,覆盖云计算、金融、物流、电商等众多领域, 需要解决高并发、高可用、分布式的复合问题。有大量的开源产品。

它具有以下特点:

  • 1. 创新的GCIH(GC invisible heap)技术实现了off-heap,即将生命周期较长的Java对象从heap中移 到heap之外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收评率和提升GC的回 收效率的目的。
  • 2. GCIH中的对象还能够在多个Java虚拟机进程中实现共享。
  • 3. 使用crc32指令实现JVM intrinsic降低JNI的调用开销;
  • 4. PMU hardware的Java profiling tool和诊断协助功能;
  • 5. 针对大数据场景的ZenGC。

taobao JVM应用在阿里产品上性能高,硬件严重依赖intel的cpu,损失了兼容性,但提高了性能,目前已 经在淘宝、天猫上线,把Oracle官方JVM版本全部替换了。

1.2 JVM 和《Java虚拟机规范》

以上的各种 JVM 版本,它们(JVM)产品的实现必须要符合《Java虚拟机规范》,《Java虚拟机规范》是 Oracle 发布 Java 领 域最重要和最权威的著作,它完整且详细的描述了 JVM 的各个组成部分。

(PS: 本文以下部分,默认都是使用 HotSpot,也就是 Oracle Java 默认的虚拟机为前提来进行介 绍的。)

2. JVM 运行流程

JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键,那么 JVM 是如何执行的呢?

JVM 执行流程

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

总结来看, JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:

  1.  类加载器(ClassLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库接口(Native Interface)

3. JVM 运行时数据区

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

3.1 堆(线程共享)

堆的作用:程序中创建的所有对象都在保存在堆中。

存放的是new出来的对象

我们常见的 JVM 参数设置 -Xms10m 最小启动内存是针对堆的,-Xmx10m 最大运行内存也是针对堆 的。

ms 是 memory start 简称,mx 是 memory max 的简称。

堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。

垃圾回收的时候会将 Endn 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用 的 Survivor 清楚掉。

3.2 Java虚拟机栈(线程私有)

Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数 栈、动态链接、方法出口等信息。常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。

栈主要记录的是方法的调用关系,还有可能会出现的栈溢出的错误

Java 虚拟机栈中包含了以下 4 部分:

1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表 所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变 量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变 量。

2. 操作栈:每个方法会生成一个先进后出的操作栈。

3. 动态链接:指向运行时常量池的方法引用。

4. 方法返回地址:PC 寄存器的地址。

什么是线程私有?

由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存。

每一个线程都有对应一个Java虚拟机栈,每调用一个方法都会以一个栈帧的形式加入到线程的栈中,方法执行完成之后栈帧就会被调出栈。

3.3 本地方法栈(线程私有)

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

工作原理和Java虚拟机栈一样,记录的是本地方法调用的关系

3.4 程序计数器(线程私有)

程序计数器的作用:用来记录当前线程执行的行号的。

记录当前线程的方法执行到哪一行(指令)。和线程强相关的都是线程私有的。

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是一个Native方法,这个计数器值为空。

程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!

3.5 方法区(线程共享)

方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。

方法区中存放的是类对象,可以理解为对象的模板

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

PS:永久代(PermGen)和元空间(Metaspace)是 HotSpot 中对《Java虚拟机规范》中方法 区的实现,它们三者之间的关系就好比,对于一辆汽车来说它定义了一个部分叫做“动能提供装 置”,但对于不同的汽车有不同的实现技术,比如对于燃油车来说,它的“动能提供装置”的实现技 术就是汽油发动机(简称发动机),而对于电动汽车来说,它的“动能提供装置”的实现就是电动 发动机(简称电机),发动机和电机就相当于永久代和元空间一样,它是对于“制动器”也就是方 法区定义的实现。

JDK 1.8 元空间的变化

1. 对于 HotSpot 来说,JDK 8 元空间的内存属于本地内存,这样元空间的大小就不在受 JVM 最大内 存的参数影响了,而是与本地内存的大小有关。

2. JDK 8 中将字符串常量池移动到了堆中。 运行时常量池 运行时常量池是方法区的一部分,存放字面量与符号引用。 字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。 符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符

运行时常量池

运行时常量池是方法区的一部分,存放字面量与符号引用。

字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。

符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。

小结 

3.6执行引擎

Java字节码 ----> CPU指令转换过程

3.7本地方法接口

调用不同系统的API

3.8内存布局中的异常问题

① Java堆溢出

Java堆内存的OOM异常是实际应用中最常见的内存溢出情况。

当出现Java堆内存溢出时,异常堆栈信 息"java.lang.OutOfMemoryError"会进一步提示"Java heap space"。

当出现"Java heap space"则很明 确的告知我们,OOM发生在堆上。

此时要对Dump出来的文件进行分析,以MAT为例。分析问题的产生到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

内存泄漏 : 泄漏对象无法被GC

内存溢出 : 内存对象确实还应该存活。此时要根据JVM堆参数与物理内存相比较检查是否还应该把JVM 堆内存调大;或者检查对象的生命周期是否过长。

② 虚拟机栈和本地方法栈溢出

关于虚拟机栈会产生的两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
  • 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常

4.JVM 类加载

4.1 类加载过程

对于一个类来说,它的生命周期是这样的:

其中前 5 步是固定的顺序并且也是类加载的过程,其中中间的 3 步我们都属于连接,所以对于类加载来说总共分为以下几个步骤:

1. 加载

2. 连接

  • 1. 验证
  • 2. 准备
  • 3. 解析

3. 初始化

1) 加载

读取.class文件

2) 验证

验证.class文件是否符合JVM规范

官方文档中这样描述

3) 准备

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

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

public static int value = 123;

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

4) 解析

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

5) 初始化

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

4.2 双亲委派模型

提到类加载机制,不得不提的一个概念就是“双亲委派模型”。

站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另外一种就是其他所有的 类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

站在 Java 开发人员的角度来看,类加载器就应当划分得更细致一 些。自 JDK 1.2 以来,Java 一直保持 着三层类加载器、双亲委派的类加载架构器。

什么是双亲委派模型?

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

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

双亲委派模型的优点

1. 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那 么在 B 类进行加载时就不需要在重复加载 C 类了。

2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模 型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户 自己提供的因此安全性就不能得到保证了。

4.3 破坏双亲委派模型

双亲委派模型虽然有其优点,但在某些情况下也存在一定的问题,比如 Java 中 SPI(Service Provider Interface,服务提供接口)机制中的 JDBC 实现。

小知识:SPI 全称 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的 API 寻找服务实现。

 这里放一个别人写的文章

http://t.csdnimg.cn/6rMMf

5.垃圾回收相关

对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性, 因为当方法结束或者线程结束时,内存就自然跟着线程回收了。因此有关内存分配和回收的为Java堆与方法区这两个区域。

Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下几种算法

内存 VS 对象

在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收。

5.1 死亡对象的判断算法

a) 引用计数算法

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

引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采 用引用计数法进行内存管理。

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

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

b) 可达性分析算法(Java中使用) 

Java并不采用引用计数法来判断对象是否已"死",而采用"可达性分析"来判断对象是 否存活(同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言)。

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

对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为 可回收对象。

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

1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;

2. 方法区中类静态属性引用的对象;

3. 方法区中常量引用的对象;

4. 本地方法栈中 JNI(Native方法)引用的对象。

在 JDK1.2 时,Java 对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。

1. 强引用 : 强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类 的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。

2. 软引用 : 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在 系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。

3. 弱引用 : 弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的 对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是 否够用,都会回收掉只被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现 弱引用。

4. 虚引用 : 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否 有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实 例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通 知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。

 

5.2垃圾回收算法

a) 标记-清除算法

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

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

1. 效率问题 : 标记和清除这两个过程的效率都不高

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

b) 复制算法(新生代)

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

现在的商用虚拟机(包括HotSpot都是采用这种收集算法来回收新生代) 

缺点:每次只能使用一半的内存

c) 标记-整理算法(老年代)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。 针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步 骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的 内存。流程图如下:

缺点:在回收之后多了一步整理内存的工作

优点:可以由大量连续的内存空间 

d) 分代算法

分代算法和上面讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略, 从而实现更好的垃圾回收。

当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只 是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每 次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没 有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

垃圾回收过程

哪些对象会进入新生代?哪些对象会进入老年代?

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

垃圾回收的过程如下:

首先,创建对象进入Eden中:

当Eden满了之后,(下图)将还活着的对象移入S0中,剩余的都是“死去“的对象(打红叉的对象为已死的对象),清空所有死去对象(垃圾回收)。再次创建新的对象。

当Eden又满了之后,(下图)将还活着的对象移入S1中,清空所有”死去的对象“。再次创建新的对象。

 交换S1和S0.

Eden又满了之后,(下图)将还活着的对象移入S0中,清空所有”死去的对象“。交换S1和S0.

面试题 : 请问了解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倍以上。

5.3 垃圾收集器

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

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

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

以下这些收集器是 HotSpot 虚拟机随着不同版本推出的重要的垃圾收集器:

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使 用。所处的区域,表示它是属于新生代收集器还是老年代收集器。在讲具体的收集器之前我们先来明确 三个概念:

  • 并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态
  • 并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程 序继续运行,而垃圾收集程序在另外一个CPU上。
  • 吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

吞 吐 量  = 运 行 用 户 代 码 时 间 /(运 行 用 户 代 码 时 间 +  垃 圾 收 集 时 间)

例如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

1) Serial收集器(新生代收集器,串行GC)

Serial 收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯 一选择。

  • 特性: 这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一 条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线 程,直到它收集结束(Stop The World,译为停止整个程序,简称 STW)。
  • 应用场景: Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
  • 优势: 简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线 程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 实际上到现在为止 : 它依然是虚 拟机运行在Client模式下的默认新生代收集器

2) ParNew收集器(新生代收集器,并行GC)

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包 括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。

  • 特性 : Serial收集器的多线程版本
  • 应用场景 : ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。
  • 对比分析: 与Serial收集器对比: ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。 然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。

3) Parallel Scavenge收集器(新生代收集器,并行GC)

  • 特性: Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
  • 应用场景: 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高 效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
  • 对比分析:
  • Parallel Scavenge收集器 VS CMS等收集器: Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是 尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到 一个可控制的吞吐量(Throughput)。 由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
  • Parallel Scavenge收集器 VS ParNew收集器: Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。

GC自适应的调节策略: Parallel Scavenge收集器有一个参数- XX:+UseAdaptiveSizePolicy 。当这个参数打开之后,就 不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了, 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间 或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。

4) Serial Old收集器(老年代收集器,串行GC)

  • 特性: Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
  • 应用场景:
  • Client模式 Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
  • Server模式 如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与 Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收 集发生Concurrent Mode Failure时使用。

 

5) Parallel Old收集器(老年代收集器,并行GC)

  • 特性: Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
  • 应用场景: 在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

 

6) CMS收集器(老年代收集器,并发GC)

  • 特性: CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前 很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速 度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需 求。
  • 优点: CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
  • 缺点:CMS收集器对CPU资源非常敏感、CMS收集器无法处理浮动垃圾、CMS收集器会产生大量空间碎片

7) G1收集器(唯一一款全区域的垃圾回收器)

G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的 region块,然后并行的对其进行垃圾回收。

G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。

G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标 记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的。

一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区 域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。 G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域 主要用于存储大对象-即大小超过一个region大小的50%的对象。 

5.4总结:一个对象的一生

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要实现一个JVM虚拟机,需要深入了解JVM的内部实现原理和Java语言规范。一般来说,JVM虚拟机由以下几个模块组成: 1. 类加载器:负责从文件系统、网络或其他来源加载Java类文件,并将其转换为JVM能够理解的格式。 2. 运行时数据区:Java程序运行时需要的内存空间,包括Java堆、方法区、虚拟机栈、本地方法栈、程序计数器等。 3. 执行引擎:负责执行Java字节码,将它们转换为机器码并执行。 4. 垃圾收集器:负责回收未使用的对象,释放内存空间。 5. 本地方法接口:允许Java代码调用本地方法(C/C++代码)。 下面是一个简单的Java虚拟机实现的示例: ```java public class JVM { private ClassLoader classLoader; private RuntimeDataArea runtimeDataArea; private ExecutionEngine executionEngine; private GarbageCollector garbageCollector; private NativeMethodInterface nativeMethodInterface; public JVM() { classLoader = new ClassLoader(); runtimeDataArea = new RuntimeDataArea(); executionEngine = new ExecutionEngine(); garbageCollector = new GarbageCollector(); nativeMethodInterface = new NativeMethodInterface(); } public void run(String className) { // 加载类 Class clazz = classLoader.loadClass(className); // 初始化类 clazz.initialize(runtimeDataArea); // 执行方法 Method mainMethod = clazz.getMethod("main", String[].class); executionEngine.execute(mainMethod); } } ``` 这个简单的JVM实现只包含了类加载器、运行时数据区和执行引擎三个部分。在实现时,还需要考虑Java语言规范中的各种细节,如异常处理、线程安全等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值