JVM 速览

545 阅读8分钟

虚拟机 与 java 虚拟机

虚拟机:指以软件的方式模拟具有完整硬件系统功能、运行在一个完全隔离环境中的完整计算机系统 ,是物理机的软件实现

JVM 全称 Java Virtual Machine(Java虚拟机),是一个能把 .class字节码 当成机器码运行的计算机。

Java虚拟机发展到现在已经不单单是一个虚拟机了,而是一个标准。遵循这个标准,各大公司开发了自己到 jvm 产品,例如

  • Sun HotSpot VM
  • BEA JRockit VM
  • IBM J9 VM
  • Azul VM
  • Apache Harmony
  • Google Dalvik VM
  • Microsoft JVM

以上这些都是 java 虚拟机,那我们常说到 java 虚拟机、jvm 指定又是什么呢?

指的是HotSpot VM ,这是 sun 公司出品的 jvm,也是JDK 自带的 JVM,sun 公司被 oracle 收购后,现在归 oracle 所有

不管是面试、还是日常交流、还是网上的 jvm 文章,谈论的都是 HotSpot VM,现在大家知道当我们在谈论 jvm 时,我们在谈论什么

启动流程

基本架构

Java编译器将源码(.java)编译成字节码,由类加载器加载到jvm中后,进行分析校验,最后由执行引擎执行。

JVM由三个主要的子系统构成:

  1. 类加载器子系统
  2. 运行时数据区(内存)
  3. 执行引擎

类装载流程

类加载的生命周期:加载(Loading)-->验证(Verification)-->准备(Preparation)-->解析(Resolution)-->初始化(Initialization)-->使用(Using)-->卸载(Unloading)

类加载器

类加载器又三种

  • Bootstrap(启动类加载器):它是用本地代码实现的类装入器,负责将 /lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
  • Extension(标准扩展类加载器):负责将/lib/ext 中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
  • System(系统类加载器):负责将系统类路径(CLASSPATH)中指定的类库(也就是我们写都代码)加载到内存中。开发者可以直接使用系统类加载器。

双亲委派机制

JVM在加载类时默认采用的是双亲委派机制

通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

运行时数据区

方法区

方法区是 JVM 规范的一个概念定义,并不是一个具体的实现,每一个具体的 JVM 都可以有各自的实现

例如,在 Java 官方的 HotSpot 虚拟机中, Java8 之前的版本,是用永久代实现的方法区, Java8 版本以后,是用元空间来实现的方法区。

因此,不能说 “JVM 的元空间是方法区”,但是可以说在 Java8 以后的 HotSpot 中 “元空间用来实现了方法区”。

顺便提一句,元空间是使用本地内存(Native Memory)实现的,也就是说它的内存是不在虚拟机内的,理论上物理机器还有多个内存就可以分配多少,而不用再受JVM 本身的内存的限制。

垃圾收集(GC:Garbage Collection)

自动内存管理,是 java 刚“出道”时的核心“卖点”。而自动内存管理的核心技术就是垃圾收集。

垃圾收集可以简单分为两个阶段

  • 识别垃圾
  • 收集垃圾

垃圾的定义

在 java 中万事万物皆对象,而不再使用的对象就是垃圾。

Java 中的堆是垃圾收集的主要区域

垃圾识别

主要有两种算法

引用计数法:给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象计数器是否为0来判断对象是否可被回收。缺点:无法解决循环引用的问题

可达性分析法:也称根搜索法,通过“GC ROOTs”的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收。

可作为GC ROOTs的对象有虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象

HotSpot VM 中使用的是 可达性分析法

分代

垃圾收集的主体是对象,jvm 根据对象所处生命周期的不同阶段, 划分成出新生代老年代

新生代又划分成 Eden区、survivor区。survivor区又划分出两块空间 From Space 和 To Space。 新生代 中Eden区、 From Space 、 To Space 占比为 8:1:1

GC 针对 新生代老年代 中对象的特点不同,划分出3类GC

  • Minor GC:有的地方又叫 Young GC ,新生代(Young Gen)空间不足时触发收集。由于Java 中的大部分对象通常不需长久存活,新生代是GC收集频繁区域,所以采用复制算法
  • Full GC:有的地方又叫 Major GCOld GC,老年代(Old Gen )空间不足时触发收集,由于存放的是大对象及长久存活下的对象,占用内存空间大,回收效率低,所以采用标记-清除算法

GC算法

按照回收策略划分

标记-清除算法

标记-清除算法分为两阶段,标记阶段清除阶段

  • 标记阶段:首先标记出要回收的对象,但不立刻回收
  • 清除阶段:等待所有要回收的对象都被标记好之后,触发回收

不足之处:

  • 效率不高,需要等待标记完成
  • 产生大量内存碎片(ps:空间碎片太多可能会导致以后在分配大对象的时候而无法申请到足够的连续内存空间,导致提前触发新一轮gc)

标记-整理算法

标记-整理算法分为标记阶段整理阶段

  • 标记阶段:标记出所有要回收的对象
  • 整理阶段:先将存活对象移到一起,然后清理掉存活对象边界以外的内存

复制算法

把内存空间划为两个相等的区域,每次只使用其中一个区域。GC 时遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。

算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。

不足之处

  • 内存利用率低
  • 在对象存活率较高时,其效率会变低

按分区对待划分

增量收集算法

增量收集是一种实时垃圾回收算法,即在应用进行的同时进行垃圾回收。

增量收集把对堆空间划分成一系列内存块,使用时先使用其中一部分,垃圾收集时把之前用掉的部分中的存活对象再放到后面没有用的空间中,这样可以实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的回收的情况。

分代收集算法

分代收集算法:(商用默认)基于对象生命周期划分为新生代、老年代、元空间,对不同生命周期的对象使用不同的算法进行回收。

按系统线程划分

串行收集算法

使用单线程处理垃圾回收工作,实现容易,效率较高。

不足之处

  • 无法发挥多处理器的优势
  • 需要暂停用户线程

并行收集算法

使用多线程处理垃圾回收工作,速度快,效率高。理论上CPU数目越多,越能体现出并行收集器的优势。

不足之处:需要暂停用户线程

并发收集算法

垃圾线程与用户线程同时工作。系统在垃圾回收时不需要暂停用户线程

GC收集器常用组合

GC日志

[GC [PSYoungGen: 8192K->1000K(9216K)] 16004K->14604K(29696K), 0.0317424 secs] [Times: user=0.06 sys=0.00, real=0.03 secs]
[GC [PSYoungGen: 9192K->1016K(9216K)] 22796K->20780K(29696K), 0.0314567 secs] [Times: user=0.06 sys=0.00, real=0.03 secs]
[Full GC [PSYoungGen: 8192K->8192K(9216K)] [ParOldGen: 20435K->20435K(20480K)] 28627K->28627K(29696K), [Metaspace: 8469K->8469K(1056768K)], 0.1307495 secs] [Times: user=0.50 sys=0.00, real=0.13 secs]
[Full GC [PSYoungGen: 8192K->8192K(9216K)] [ParOldGen: 20437K->20437K(20480K)] 28629K->28629K(29696K), [Metaspace: 8469K->8469K(1056768K)], 0.1240311 secs] [Times: user=0.42 sys=0.00, real=0.12 secs]

内存溢出

StackOverflowError:(栈溢出) OutOfMemoryError: Java heap space(堆空间不足) OutOfMemoryError: GC overhead limit exceeded (GC花费的时间超过 98%, 并且GC回收的内存少于 2%)

参考JVM看这篇就够了!