1 垃圾
1.1 前言
众所周知,JVM
拥有着垃圾回收器,自动回收内存,让开发人员只专注于编程,而不需要手动进行内存管理。
那么到底什么样的对象才会被定义为垃圾,且被JVM
的垃圾回收器回收掉
1.2 定位
1.2.1 简介
在JVM
中当一个对象没有任何引用指向他的时候就会被认为是一个垃圾,例如下图中
当栈里面的person
引用不再指向堆里面的Person
对象时,Person
对象就会被认为是一个垃圾对象,那么就会被回收掉
但有时候在堆里面,对象相互指向,但是栈里面并没有任何一个引用指向对象,那么那些对象会被认为是一堆垃圾。如下图所示
image-20210317110743157栈里面没有一个引用指向堆里面的对象,那么对象1 对象2 对象3 就会被认为是一块垃圾,从而被回收掉
1.2.2 寻找
那么JVM
是如何寻找到垃圾对象,并且进行回收掉的呢? 在JVM
有两种算法去定位垃圾的存在:
- 引用计数算法
- 根可达算法
接下来就来仔细探讨这两种算法的区别是什么
引用计数
所谓的引用计数指的的就是记录一个对象有多少个引用指向该对象。当引用为0,即没有任何引用指向该对象时,就认为该对象为垃圾。
如图
image-20210319095808432.pngimage-20210319100015328当没有任何引用指向对象时,即对象存储的计数为0时,则认为是一个垃圾
注意:这种方式的坏处就是当几个对象相互指向的时候,是没有办法通过引用计数来判断对象是否为垃圾
因此
JVM
则采用另外一个算法(根可达算法)来确定垃圾
根可达算法
所谓的根可达算法就是一系列称为GCRoots
的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(ReferenceChain
)。
如果某个对象到GCRoots间没有任何引用链相连,或者用图论的话来说就是从GCRoots到这个对象不可达时,则证明此对象是不可能再被使用的
如图所示
image-20210319102446798图中,可以通过根“找到”对象1,对象2,对象3,就认为这三个对象不是垃圾
图中,对象10,对象20,对象30 通过 根引用无法“找到”,所以这三个对象是垃圾
在JVM
规范中以下对象会认为是根:
- 在虚拟机栈(栈帧中的本地变量表)中引用
- 在本地方法栈中
JNI
(即通常所说的Native方法)引用 - 运行常量池引用
- 方法区的静态引用
-
Class
类型引用还有一些常驻对象引用(NullPointerException
)
2. 回收
在JVM
中关于常见垃圾回收的算法,一共有三个:
- 标记清除算法(
Mark-Sweep
) - 复制算法(
Copy
) - 标记压缩算法(
Mark-Compact
)
下面是对这三种算法的详细描述和对比
2.1 Mark-Sweep
JVM
中最早出现的垃圾回收算法,算法主要分为标记和清除两部分,简单了解就是先标记所有的垃圾对象,然后统一回收掉所有的标记对象。如下图所示
注意
这种方式回收垃圾,会产生碎片化的内容,从而有可能导致无法分配连续的内存空间
2.2 Mark-Copying
标记-复制算法也叫复制算法,这种算法的出现主要是为了解决标记-清除算法的缺陷。标记-复制算法就是将一个可用内存划分为两块等同的区域。
每次都使用其中某一块区域,当一块区域(A
)使用完毕就会把存活的对象复制到另外一块区域(B
),复制完成,再将之前的区域(A
)进行清空。如图
这种算法好处如下:
- 实现简单,运行高效
- 可以为一个对象分配一个连续的内存空间,不用考虑碎片化内存
- 回收时只需要对半个内存空间全部清空即可
但是缺点也显而易见,如下:
- 存在内存空间的复制开销
- 可使用的内存缩小为原来的一半,存在空间浪费
纵观有这样那样的缺陷,但是现在商用的
JVM
,例如HotSpot
,也是有限采用这种算法去对新生代的对象进行垃圾回收
2.3 Mark-Compact
标记-压缩算法也是针对标记-清除算法,进行改进,当标记的对象清除以后,存货的对象进行移动,从而防止碎片化的内存出现。具体如下:
image-20210323100228305 image-20210323100418347这种算法与之前的标记-清除算法本质的区别就是,标记-清除算法只是清除,而不会移动存活的对象。标记-压缩则是清除后会进行移动,从而防止碎片化的内存出现
这种算法也有很大的缺陷:
-
移动存活对象操作必须全程暂停用户程序才能进行。
-
这种算法主要是针对老年代,老年代存活对象较多,如果频繁对老年代进行垃圾回收,会导致程序卡顿,因此这种现象称为
STW
(Stop The World
)
3. 分代
在经典的垃圾回收器中,JVM
采用了分代模型(主要是针对jdk1.7 - jdk1.9
),JVM
将堆分为了两部分
-
新生代
- 新出生的对象都在新生代
- 新生代的对象朝生夕死
- 采用标记-复制算法进行垃圾回收
-
老年代
- 垃圾较少,存活对象较多
- 一般采用标记-压缩算法,如果采用的是G1垃圾回收器则采用标记-复制算法
-
永久代
- 在
JDK1.8
之后没有永久代了,取而代之的是元数据区(Metaspace
) - 永久代 和 元数据区都是用来存放 Class对象
- 永久代可以指定大小,元数据区则不需要指定大小,而是直接依赖物理内存
- 在
注意:如果JVM采用最新的垃圾回收器,则不区分新生代和老年代了
堆内存模型图如下:
image-202103231037483843.1 新生代
新生代主要分为三个区域:
- 伊甸区(
eden
) - 幸存0区(
survivor0
) - 幸存1区(
survivor1
)
其中他们的比例为 8 :1 : 1
,同时,新生代采用的是复制算法进行垃圾回收。如下:
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。
HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次
Minor GC
GC开始时,对象只会存在于Eden区和Survivor 0区,S1区是空的(作为保留区域)。GC进行时,
Eden
区中所有存活的对象都会被复制到S1区,而在S0
区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header
中)的对象会被移到老年代中。没有达到阀值的对象会被复制到
s1
区。接着清空Eden
区和s0
区,新生代中存活的对象都在s1
区。接着,s0
区和s1
区会交换它们的角色,也就是新的s1
区就是上次GC清空的s0
区,新的s0
区就是上次GC的s1
区,总之,不管怎样都会保s1
区在一轮GC后是空的。GC时当s1
区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
3.2 老年代
老年代与新生代比例为3:1
,如下:
在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
注意:当较大的对象无法在新生代分配内存时,则会直接进入老年代
注意:老年代的垃圾回收称为
FullGC
,老年代频繁垃圾回收则会出现STW
现象,因此JVM调优主要就是尽量减少FGC
4. 回收器
上述都是对JVM
进行理论阐述,而垃圾回收器则是对上述理论的实现。在《JVM规范》中没有对垃圾回收器做各种规定,因此不同的厂商的不同虚拟机会存在不同的区别。
在这里主要是探讨HotSpot
虚拟机的垃圾回收器,关于所有垃圾回收器(jdk1.0-jdk13
)如下图
图中的G1
,ZGC
,Shenandoah
回收器不再区分新生代和老年代,Epsilon
是JDK内部调试的回收器
在目前的环境中,向后面三个用的还是比较少的,因此主要是还是集中讨论新生代和老年代的垃圾回收
图中虚线部分连接起来代表两个垃圾回收器可以结合使用
4.1 新生代
4.1.1 Serial
Serial
回收器是一个用来新生代垃圾回收器,Serial old
是一个用在老年代的垃圾回收器
这个回收器是一个单线程工程的垃圾回收器,这个“单线程”回收器是指使用一个处理器或者一条收集线程去执行。
当进行垃圾回收的时候,必须暂停其他所有的工作线程,直到回收结束。当然这样就会产生STW
现象。如下:
从JDK1.3开始,一直到现在最新的JDK13,HotSpot虚拟机开发团队为消除或者降低用户线程因垃圾收集而导致停顿的努力一直持续进行着,从Serial收集器到Parallel收集器,再到ConcurrentMarkSweep(CMS)和GarbageFirst(G1)收集器,最终至现在垃圾收集器的最前沿成果Shenandoah和ZGC等,我们看到了一个个越来越构思精巧,越来越优秀,也越来越复杂的垃圾收集器不断涌现,用户线程的停顿时间在持续缩短,但是仍然没有办法彻底消除(这里不去讨论RTSJ中的收集器),探索更优秀垃圾收集器的工作仍在继续(出自《深入理解JVM第三版》)
4.1.2 ParNew
ParNew
垃圾回收器本质上相当于Serial
垃圾回收器的多线程并行版本,除了在回收的时候使用多线程并行回收,其他的与Serial
回收器一摸一样。
具体工作流程如下:
image-20210324102804638注意:上图中的老年代垃圾回收器采用的还是
Serial Old
,当然也可以与CMS
回收器配合使用,当然也推荐与CMS
配合使用
4.1.3 Parallel Scavenge
Parallel Scavenge
与ParNew
回收器类似,也是一个新生代的垃圾回收器,也是基于标记-复制算法实现的垃圾回收器。同时也是一款并行的垃圾回收器,
只不过Parallel Scavenge
更加专注于一个可以达到的吞吐量
吞吐量 = 用户代码运行时间 / (用户代码运行时间 + 垃圾回收时间)
吞吐量越高就意味着垃圾回收时间越短,例如用户代码运行时间是20s
,垃圾回收时间1s
,那么意味着吞吐量为20/21 =95%
当然也可以通过一些参数去设置用户的吞吐量:
-
-XX: MaxGCPauseMillis
设置最大垃圾收集停顿时间 -
-XX: GCTimeRatio
设置吞吐量大小
工作流程图如下:
image-202103241043024074.2 老年代
4.2.1 Serial Old
Serial Old
就相当于Serial
垃圾回收器的老年代版本,同样的是单线程,采用标记-压缩算法。这里就不再追诉了
4.2.2 Parallel Old
Parallel Old
回收器相当于是Parallel Scavenge
回收器的老年代版本,这个回收器同样是一个基于标记-压缩算的多线程垃圾回收器。
这个垃圾回收器是在jdk1.6
以后才开始提供,之前Parallel Scavenge
垃圾回收器都是于Serial Old
垃圾回收器配合使用。
直到Parallel Old
垃圾回收器的出现,老年代才慢慢开始以吞吐量优先饿垃圾回收器
工作流程图如下:
image-202103241051442994.2.3 CMS
-
简介
CMS(Concurrent Mark Sweep)
回收器是一个老年代的垃圾回收器,从名字可以看到是一个并发的,标记-清除垃圾回收器。
这个回收器是以最短回收停顿时间 为目标的垃圾回收器,从上述的介绍中可以得到老年代的的垃圾回收器,都需要暂停用户线程在进行垃圾回收,因此都会产生STW
现象
而当今大部分Java程序都是以B/S
架构为主,在开发这类程序时,更加关注服务端的响应时间,也就说尽量减少系统的停顿时间。
那么CMS
回收器就是一个尽量减少停顿时间的的垃圾回收器。
-
工作原理
与之前的标记-压缩算法不用,
CMS
垃圾回收器则是基于标记-清除算法,该垃圾回收器主要基于以下几部分:- 初始标记
- 并发标记
- 重新标记
- 并发清除
其中初始标记,重新标记还是会产生
STW
,但是这个时间非常短,不会占据特别长的时间初始标记只是标记以下
image-20210324110848531GC ROOT
能够直接关联的对象,如下图,初始标记就是标记对象1 和 对象11初始标记还是会暂停其他用户线程,因此还是会产生
STW
,但是这个标记速度很快
并发标记 是从
image-20210324111253954GC Root
遍历整个对象的过程,这个速度需要的时间较长,但是不会暂停用户所有的线程,而是和用户线程一起执行,这样也不会产生停顿的现象
重新标记则是修改并发标记期间,因为用户程序的运行,从而导致标记的对象发生变量
例如:上图中有可能在程序运行过程中
GC ROOT
不再指向对象11,因此需要重新标记但是这个标记时间比初始标记长,但是比并发标记时间短。
注意 重新标记也会暂停所有用户线程去进行标记
并发清理清除上述标记中需要清除的垃圾对象,由于是采用标记-清除算法,因此也不需要向标记-压缩算法一样,移动存活的对象,所以这个时间也是比较快的。
同时这个过程也不需要暂停用户所有线程,而是与用户线程一起运行,如下图所示:
image-20210324112320181
当然这个垃圾收器也有一些缺点,例如最明显由于采用标记-清除算法,所以就会导致会产生过多的随便化空间,这样会给大对象分配带来很大的麻烦。
当没有足够大的空间为大内存,不得不提前触发一次FullGC的情况。为了解决这个问题,CMS收集器提供了一个
-XX:+UseCMS-CompactAtFullCollection
开关参数用于在CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,所以就会导致停顿时间过长等
5. 指令
JVM
的指令主要分为三大类:
-
标准指令
以
-
开头,所有版本的HotSpot
的都支持 -
非标准指令
以
-X
开头 特定版本的HotSpot
都支持 -
高级运行时指令
以
-XX
开头,可能下个版本会取消的指令
所有详细命令可以参考以下网址:
http://www.oracle.com/technetwork/java/javase/documentation/index.html
5.1 非标准
输入java -X
即可看到常用的非标准指令,如下:
-Xmixed 混合模式执行 (默认)
-Xint 仅解释模式执行
-Xbootclasspath:<用 ; 分隔的目录和 zip/jar 文件>
设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 ; 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 ; 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag 显示附加诊断消息
-Xnoclassgc 禁用类垃圾收集
-Xincgc 启用增量垃圾收集
-Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳)
-Xbatch 禁用后台编译
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小
-Xss<size> 设置 Java 线程堆栈大小
-Xprof 输出 cpu 配置文件数据
-Xfuture 启用最严格的检查, 预期将来的默认值
-Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni 对 JNI 函数执行其他检查
-Xshare:off 不尝试使用共享类数据
-Xshare:auto 在可能的情况下使用共享类数据 (默认)
-Xshare:on 要求使用共享类数据, 否则将失败。
-XshowSettings 显示所有设置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续
-X 选项是非标准选项, 如有更改, 恕不另行通知。
例如在这里可以设置堆的初始化内存大小和最大堆
java -Xms128m -Xmx1024m
5.2 高级运行时指令
常用的高级指令如下:
-XX:+PrintFlagsFinal
# 设置最终生效值
-XX:+PrintFlagsInitial
# 查看默认值
-XX:+PrintCommandLineFlags
# 查看命令行参数
例如输入
java -XX:PrintCommandLineFlags
image-20210324125033378
上图中选中的代表现在HotSpot
虚拟机采用的是Parallel Scavenage
+ Parallel Old
垃圾回收器
网友评论