前言
关于java虚拟机的学习,网上有许多学习文章。同时周志明老师编写的图书:《深入理解JVM虚拟机》也是JVM学习者必读图书之一。虽说之前也有相关的学习过,但是一直没有进行梳理,导致知识相对很零散,所以为了日后方便自己或他人学习,本文将对JVM进行简略的知识梳理。
内存模型
Java内存模型,往往是指Java程序在运行时内存的模型,而Java代码是运行在Java虚拟机之上的,由Java虚拟机通过解释执行(解释器)或编译执行(即时编译器)来完成,故Java内存模型,也就是指Java虚拟机的运行时内存模型。
JVM运行时数据区,包括5部分:堆,方法区,程序计数器,虚拟机栈,本地方法栈。在这5部分中,其中:堆,方法区是共享数据区,即所有线程共享的内存区域;程序计数器,虚拟机栈,本地方法栈是线程私有,即每个线程都会有自己的独立内存区域,各线程之间互不影响,独立存储
。
- 线程共享数据区
堆
存放对象实例的区域。对象实例以及数组的内存分配都是在堆中进行
方法区
存储已被虚拟机加载的 类信息,常量,静态变量,即时编译后的代码等数据。常量池位于方法区,并使用永久代来实现方法区。(JDK1.8对此进行了修改,在JDK1.8中没有了永久代的概念,取而代之的是元空间。关于这点,下文在讲解)
- 线程私有数据区
程序计数器
当前线程所执行的字节码的行号指示器。通过改变计数器来选取下一条需要执行的字节码指令。
1
2备注: 如果线程执行的是java方法,则记录的是正在执行的虚拟机字节码指令的地址;
如果执行的是Native方法,则计数器值为空。此区域也是JVM规范中唯一一个没有规定OOM的区域。java虚拟机栈
虚拟机栈描述的是java方法执行的内存模型:
每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息
本地方法栈
虚拟机的Native方法执行的内存区。本地方法栈与虚拟机栈作用非常相似。程序员可以不用考虑该区域。
JDK1.8变化
在 HotSpot JVM中,永久代中用于存放类和方法的元数据以及常量池。每当一个类初次被加载的时候,它的元数据都会放到永久代中。由于永久代有大小限制,常常会出现:java.lang.OutOfMemoryError:PermGen。在JDK1.8中,PermGen已经被彻底移除,取而代之的是metaspace数据区,使用本地内存。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
在JDK8下,旧的参数-XX:PermSize和-XX:MaxPermSize会被忽略并显示警告。新的Metaspace通过参数-XX:MetaspaceSize 和-XX:MaxMetaspaceSize设定。
对象创建
在上述讲解的所有JVM运行时数据区中,堆区是开发过程中接触最多的JVM内存区域,也是我们相对关注的。该区域被所有线程共享,所有创建的对象都在这个区域分配内存并初始化。其后要讲述的GC也是主要针对这个区域的。
由于现在的收集器基本都是采用分代收集算法
,所以java堆有可以分为:新生代和老年代。新生代有分为:Eden空间,From Survivor空间和To Survivor空间。见图:
在创建对象的时候,对象会首先在Eden区存放。经过垃圾回收且存活的对象会进入两个Survivor中的一个。此时,这个Survivor区就称为To Survivor。两个Survivor区在同一时刻会有一个区域是空的,用于存放下次垃圾回收后存活的对象。在经过了一定次数的垃圾收集后,然后存活的对象,会被移到老年代。
如果一个新的对象太大,以至于新生代经过一次垃圾回收后依然没有足够空间存放它。JVM会通过分配担保来把这个对象放在老年代。如果老年代空间不够,经过一次Full GC还是没有空间,那虚拟机无法为这个对象创建内存空间,只能抛出OOM异常停止运行。
对象在创建分配内存的时候,有两种方式:
指针碰撞
如果java堆中的内存绝对规整,用过的内存与空闲的内存有明确的分界点,并使用指针进行表示,在分配内存时,只需要将指针向空闲内存移动一段与对象大小相等的距离,这个分配方式为:指针碰撞
空闲列表
java堆中的内存不规整,并对已用内存与空闲内心进行标记,在分配对象内存时,从空闲内存列表中找出一块足够大的空间并更新,这个分配方式为:空闲列表
具体采用哪种方式,取决于垃圾收集器是否带有压缩整理功能决定。其中,Serial, PaeNew等带有整理功能的收集器,系统采用的分配方式为:指针碰撞;CMS这种基于标记清除算法的收集器,系统采用的分配方式为:空闲列表。
垃圾收集
如果创建后的对象系统没有再次使用,那么这些对象所占用的内存就要被收集,以供新建对象时使用。所以,在进行垃圾收集前,我们需要确认哪些对象需要被回收,在确认好哪些对象需要被回收后,我们还有考虑怎么回收这些对象。我们分两个方面学习
判断对象是否存活
如果确认哪些对象需要被回收,一般有两个方法:
引用计数法
可达性分析法
引用计数法
概念
对象中添加一个引用计数器,每当有地方引用时,计数器+1;当引用失效时,计数器-1。任何时刻引用计数器为0的对象为不可用对象。
缺点
不能解决对象相互引用问题
可达性分析法
概念
通过一系列称为 GC Roots 的对象为起始点,从节点向下搜索,搜索所走过的路径称为引用链,当一个对象没有任何引用链时,该对象不可达。
图中,对象object5,object6,object7没有引用链,因此他们都是不可达对象。
关于 GC Roots,在Java语言里包括:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI的引用的对象
垃圾收集算法
在确认需要回收的对象后,接下来就要把这些对象所占用的内存进行回收。下面讲解几种垃圾收集算法:
垃圾收集算法 | |
---|---|
标记-清除算法 | |
复制算法 | |
标记-整理算法 | |
分带收集算法 |
- 标记-清除算法
过程
该收集算法有两个阶段:
标记,清除
。首先标记出所有需要被回收的对象,在标记完成后统一回收。缺点
- 效率问题。
- 空间问题。标记清除后,会产生大量不连续的内存空间。
复制算法
为了解决
标记-清除算法
的效率问题,所以引入了复制算法。
过程
将可用内存划分为两个不同空间,每次只是用其中一块。当这一块内存用完后,就将其中存活的对象复制到另一块,然后再把已使用过的内存空间一次清空。
缺点
内存缩小为原来的一半
- 标记-整理算法
过程
标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。这样就不会产生内存碎片。
缺点
在标记-清除的基础上还需进行对象的移动,成本相对较高。
分代收集算法
根据对象存活周期的不同,一般将java堆分为新生代和老年代,这样就可以根据各代的特点采用不同的垃圾回收算法。在新生代中,由于大批对象会死去,只有少量存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;而老年代中因为对象的存活率高,没有额外空间对他进行分配担保,需使用标记-清除或者标记-整理算法。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
收集器 | 回收区域 | 算法 |
---|---|---|
Serial 收集器 | 新生代 | 使用复制算法 |
ParNew 收集器 | 新生代 | 使用多线程复制算法 |
Parallel Scavenge 收集器 | 新生代 | 使用多线程复制算法 |
Serial Old 收集器 | 老年代 | 使用标记-整理算法 |
Parallel Old 收集器 | 老年代 | 使用多线程标记-整理算法 |
CMS 收集器 | 老年代 | 使用多线程标记-清除算法 |
G1 收集器 | 针对java堆中的独立区域 | 使用多线程标记-整理算法 |
其中,图中的连线表示可以搭配使用的收集器。
- 新生代收集器
Serial 收集器
Serial收集器是最基本,发展历史最悠久的
单线程收集器
。它的单线程并不仅仅意味着它只会使用一个CPU 或一条收集线程去完成垃圾回收工作,更重要的是,在它进行垃圾收集时,必须停止其他所有的工作线程,直到它收集结束,即:Stop The World
。
优点
简单而高效。对于限定单个CPU的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获取最高的单线程收集效率。适用于Client模式下的虚拟机。
ParNew 收集器
ParNew 收集器其实是 Serial 收集器的多线程版本。也是 除了Serial 收集器以外,唯一一个可以与CMS 收集器配合使用的新生代收集器。
Parallel Scavenge 收集器
Parallel Scavenge 收集器是一个作用于新生代,使用了复制算法的多线程收集器。它的关注点与 CMS 收集器的不同:Parallel Scavenge 收集器的目标是:达到一个可控的吞吐量;而CMS 收集器的目标是:尽可能的缩短垃圾收集时用户线程停顿时间。
1 | 吞吐量:就是CPU用于执行用户代码的时间与CPU总消耗时间的比值。即: |
在Parallel Scavenge 收集器中,可以通过参数:-XX:UseAdaptiveSizePolicy
来开启收集器的自适应调节策略。在开启后,就不需要手动指定新生代的大小(-Xmn),Eden与Survivor去的比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整已提供最适合停顿时间或最大吞吐量。
- 老年代收集器
Serial Old 收集器
Serial Old 收集器是Serial 收集器的老年代版本。同样为单线程收集器,但使用的是:
标记-整理 收集算法
。- 用途:
- 在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old 收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用
多线程和“标记-整理”算法
。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种
以获取最短停顿时间为目标的,基于"标记-清除"算法实现的收集器
。其收集过程包括4步:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
其中,初始标记,重新标记仍需要 “Stop the World”,初始标记仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快;并发标记就是进行 GC Roots Tracing 的过程;而重新标记则是为了修改并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
- 缺点
- CMS收集器对CPU资源非常敏感
- CMS收集器无法处理浮动垃圾。(由于CMS并发清理阶段用户线程还在运行,所以还会产生新的垃圾,这些垃圾出现在标记过程之后,CMS无法在当次收集中处理他们,只好等待下一次GC在清理,这部分垃圾称为“浮动垃圾”)
- CMS收集器是一款基于:“标记-清除”算法实现的,所以会产生大量的空间碎片,有可能会提前触发一次Full GC。
G1 收集器
G1(Garbage First)收集器是当今收集器技术发展的最前沿。与以往的垃圾回收算法不同,在G1算法中,采用了一种不同的方式组织堆内存。之前的收集器把堆内存分为:新生代和老年代,则G1则是把堆内存划分为多个大小相同的内存块(即:Region),每个Region是逻辑上连续的一段内存。
每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时
,直接在新的一个或多个连续Region中分配,并标记为H。
1 | 可以通过配置:-XX:G1HeapRegionSize 来指定Region区域大小。且大小区间只能是2的幂次方。 |
- GC模式
G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。
young gc
一般对象(大对象除外)的分配都是在Eden region区,当所有Eden region被耗尽无法申请内存时,就会触发一次young gc。
mixed gc
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。
1
2
3
4
5
6
7
8
9
10
11mixed gc中也有一个阈值参数 -XX:InitiatingHeapOccupancyPercent,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc.
mixed gc的执行过程有点类似cms,主要分为以下几个步骤:
1. initial mark: 初始标记过程,整个过程STW(Stop The World),标记了从GC Root可达的对象
2. concurrent marking: 并发标记过程,整个过程gc collector线程与应用线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息
3. remark: 最终标记过程,整个过程STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
4. clean up: 垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中full gc
如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.
- 优点
- 并行与并发
- 分代收集
- 空间整理
- 可预测停顿
1 | Full GC 和 Minor GC 的区别 |
参考文章
- 《深入理解JAVA虚拟机》 周志明 著
- JVM内存模型
- Java 8新特性探究(九)跟OOM:Permgen说再见吧
- 什么是G1垃圾回收算法