JVM 垃圾回收机制、垃圾收集器、调优参数


 

概述

C++的内存回收很麻烦,不回收可能会造成内存泄漏,Java中由GC完成内存回收,不用手动回收内存。

程序计数器占据的内存较小,没有必要进行垃圾回收;虚拟机栈、本地方法栈在进行出栈操作后,会自动回收栈帧使用的内存空间,无需gc进行垃圾回收。

垃圾收集主要针对堆和方法区进行,其中方法区存放的对象十分稳定,存活率极高,通常只在类卸载、常量不被使用的情况下才会产生垃圾。在方法区上进行垃圾回收性价比不高。堆内存占用非常大,对象多,是垃圾回收的重点区域。
 

类的卸载条件很多,需要满足以下三个条件

  • 该类所有的实例都已经被回收
  • 加载该类的类加载器已经被回收
  • 该类对应的 Class 对象没有在任何地方被引用

即使满足了这3个条件也不一定会被卸载。

 

标记对象是否为垃圾的算法

对象被判定为垃圾的标准:没有被其它对象引用
 

引用计数算法

堆中每个对象都对应一个引用计数器,当一个变量引用此对象时,计数器+1;当引用此对象的变量生命周期结束或者被赋新值时,计数器-1;计数器为0时该对象成为垃圾,等待gc回收。
 

优点:简单、高效

缺点:需要存储每个对象对应的引用计数器,有额外的内存开销;没有处理循环引用问题,存在循环引用时,计数器永不为0,可能导致内存泄漏。

eg. a对象中引用了b对象,b对象中引用了a对象,即存在循环引用,使用引用计数法时,这2个对象的计数器永不为0,永远不会被回收。

即使把这2个变量都置为null,只是引用变成了null,堆中的这2个对象不变,依然持有彼此的引用,它们的计数器也不会为0。

因为引用计数法没有解决循环引用问题,所有主流垃圾收集器都不采用引用计数算法,而采用可达性分析算法。

 

可达性分析算法

又叫做根搜索算法,使用不同的GC Root,从GC Root开始寻找引用链上的对象,没在任何一条引用链中的对象标记为垃圾。

相比于引用计数法,可达性分析算法同样具备简单、高效的优点,且没有存在循环引用时不能被gc回收的问题。

 

可以作为GC Root的对象

  • java虚拟机栈中的局部变量表中引用的对象
  • 本地方法栈中 JNI 引用的对象
  • 方法区中类的静态成员、常量引用的对象
  • 被同步锁synchronized持有的对象

使用可达性分析算法进行分析时,整个分析过程必须在一个一致的堆内存快照中进行,否则不能保证分析结果的正确性,这也是进行gc时必须stop the world的一个重要原因。

即使是以系统最短停顿时间为目标的CMS收集器,枚举GC Root时也是要停顿的。

 

对象的finalize机制

根类Object提供了一个finalize()方法,默认是空实现,可以被子类重写。

finalize()类似 C++ 的析构函数,可以用于关闭外部资源,但此方法运行代价很高、不确定性大,无法保证各个对象的调用顺序,最好不要使用,关闭资源可以用 try-finally代替。

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
 

一个没被任何其它对象引用的对象,只是暂时不被使用,并不一定就是垃圾,可能该类重写了finalize()方法,在finalize()方法中复活、重新使用当前对象。
 

堆中对象可能的三种状态

  • 可触及的:在GC Root的引用链上
  • 可复活的:不在GC Root的引用链上,但可能调用finalize()方法进行复活
  • 不可触及的:调用了对象的finalize()方法,但该方法中并没有复活当前对象
     

可达性分析算法至少要经过2次标记,才会把对象标记为垃圾

  • 第一次使用GC Root的引用链进行筛选,将不在GC Root的引用链上的对象标记为可复活的
  • 第二次对可复活的对象进行筛选

如果对象所属的类没有重写finalize()方法,或者之前已经执行过finalize()方法但没有复活对象,则直接标记为不可触及的;

如果对象所属的类重写了finalize()方法,且之前没有执行过finalize()方法,则把对象放入finalize队列中,

由jvm创建的一个低优先级的finalizer线程处理队列中对象,调用对象的finalize()方法,如果没有复活,则把对象标记为不可触及的。

状态为不可触及的对象才会成为垃圾,等待被gc回收。

 

引用类型

无论引用计数算法,还是可达性分析算法,判定对象是否可被回收都与对象的引用类型有关。java 提供了四种引用类型。
 

1、 Strong Reference 强引用

被强引用关联的对象不会被gc回收,一般都是以强引用方式进行关联。

与其它引用类型的区别:强引用禁止引用目标被垃圾收集器收集,而其他引用不禁止。

//eg.使用new来创建强引用,变量user强引用new出来的对象
User user = new User();

 

2、Soft Reference 软引用

被软引用关联的对象只有在内存不够的情况下才会被回收。

可以使用 SoftReference 类来创建软引用。

User user = new User();
SoftReference<User> sf = new SoftReference<>(user);
//置空对象,使对象只被软引用关联
user = null;  

 

3、Weak Reference 弱引用

被弱引用关联的对象在下次进行垃圾收集时一定会被回收,即只能存活到下一次垃圾回收发生之前。

可以使用 WeakReference 类来创建弱引用。

User user = new User();
WeakReference<User> wf = new WeakReference<>(user);
user = null;

 

4、Phantom Reference 虚引用

又称为幽灵引用、幻影引用,虚引用不会对关联对象的生存时间造成影响,设置虚引用的目的是在关联对象(目标对象)被回收时可以收到系统通知。

可以使用 PhantomReference类来创建虚引用。

User user = new User();
PhantomReference<User> pf = new PhantomReference<>(user, null);
user = null;

 

垃圾回收算法

标记-清除算法 Mark-Sweep
  • 标记需要回收的对象
  • 清除要回收的对象

清除并不是置空,只是把要清除的对象的地址保存在空闲地址列表中,后续分配内存时可以使用这些内存,再次分配时才覆盖原有内容。

缺点:容易产生内存碎片,可能导致后续给大对象分配内存空间时没有足够大的连续内存,从而提前触发下一次gc。

 

标记-整理算法 Mark- Compact
  • 标记需要回收的对象
  • 将所有存活的对象压缩(Compact)到内存的一端,然后直接清理掉边界以外的部分。

不会产生内存碎片,但效率要低于复制算法。

 

复制算法 Coping

将内存划分为2块,每次只使用一块,进行垃圾回收时将存活的对象复制到未使用的一块上,清理掉之前使用的那一块。

不会产生内存碎片,适合对象存活率低的场景,常用于新生代的垃圾回收。

新生代对象存活率低,一般使用复制算法;老年代对象存活率高,一般使用标记-整理算法。

 

分代收集算法

根据对象存活周期(年龄代)将堆内存划分为几块,不同块使用合适的收集算法。
 

把堆划分为2大块

  • 新生代(Young Gen):包括Eden区、2个Survivor区,默认大小比例8:1:1
  • 老年代(Old Gen):老年代内存空间比新生代大得多

新生代使用复制算法,老年代使用标记-清除或标记-整理算法。

 

对象|内存 分配策略

  • 对象主要分配在新生代的 Eden 区上
  • 如果启用了本地线程分配缓冲TLAB,则优先分配到线程各自的分配缓冲区中
  • 少数情况下会直接分配在老年代中,比如大对象

 

垃圾回收方式

  • 新生代使用复制算法进行垃圾回收,回收时把Eden区、Survivor from区中存活的对象复制到Survivor to中,然后清理掉Eden区、Survivor from区。Survivor from、to是相对的,不是固定的某一块内存空间。
  • 老年代使用标记-清除或标记-整理算法进行垃圾回收,不同的垃圾收集器,老年代使用的垃圾回收算法可能不同。

 

gc分类

  • Minor GC:新生代 GC,回收新生代。新生代对象存活时间很短,回收率极高,Minor GC 非常频繁,回收执行速度也很快。
  • Full GC:又叫做Major GC,主要回收老年代,但通常伴随着Minor GC,所以可以看做老年代、新生代都在回收,收集整个堆。老年代内存大、对象多,对象存活时间长,相比于Minor GC,Full GC执行次数较少、回收速度慢。

 

minor gc的触发条件

  • Eden区空间不足

 

full gc的触发条件

  • 老年代空间不足
  • 调用System.gc()。虽然只是建议进行Full GC,但一般都会进行
  • 新生代空间不足时使用老年代的内存空间进行分配担保(Handle Promotion),分配担保失败时会触发full gc

分配担保:新生代对象存活率偏高,Survivor to中放不下时,会使用老年代的空间进行分配担保,即把Survivor to中放不下的对象直接放到老年代中。

full gc时间花销大,造成的停顿时间较长,看到jvm频繁进行full gc时要引起注意,应该进行优化。

 

对象如何晋升到老年代

  • 经历指定的Minor GC次数仍然存活,默认15次
  • 新生代空间不足时使用老年代的内存空间进行分配担保,Survivor to中放不下的对象直接放到老年代中
  • 大对象直接在老年代进行分配

 

堆外内存(直接内存)的回收

DirectByteBuffer对象本身是直接分配在老年代的,对象本身可在Full GC时被回收,但DirectByteBuffer申请使用的是直接内存,所引用的直接内存不在gc回收范围内,不会被gc回收。

jdk提供了一种机制:可以给堆中的对象注册一个钩子函数(其实就是实现 Runnable的一个子接口),当堆中的对象被GC回收的时候,会回调run()方法。

Unsafe类提供了大量的native方法,可以在run()方法中调用Unsafe类的freeMemory()方法,释放DirectByteBuffer对象引用的直接内存。

 

垃圾收集器

stop the word:jvm进行垃圾回收时会暂停应用程序的执行(暂停所有用户线程),gc完成才会继续执行应用程序,主流垃圾收集器或多或少都存在这各个情况。

safepoint:安全点,标记阶段对象引用关系不会发生变化的点,比如方法调用、循环跳转处。

 

JVM的2种运行模式
  • client模式:使用轻量级虚拟机,适合对性能要求不高的项目,常用于桌面程序。桌面程序内存占用小,一般就几十兆、两三百兆。
  • server模式:使用重量级虚拟机,启动慢,但做了更多的优化,稳定运行后性能更高,适合对性能要求高的项目,常用于java web项目。

使用java -version可以查看jvm的种类、运行模式,HotSpot默认使用server模式。

 

新生代常见的3种收集器
Serial收集器
  • 使用单线程进行垃圾回收
  • 使用复制算法
  • 性能低,常用于桌面程序,是HotSpot虚拟机client模式下默认的新生代垃圾收集器

 

ParNew收集器
  • 使用多线程进行垃圾回收,相当于Serial的多线程版本
  • 使用复制算法
  • 是web应用主流使用的新生代垃圾收集器,一个重要原因是ParNew是能与CMS搭配使用少数新生代收集器之一

Par是Parallel的缩写

#使用ParNew收集器,+是启用,-是取消
-XX:+UseParNewGC

#可以指定进行垃圾回收的线程数,默认为cpu核心数
-XX:ParallelGCThreads=8

 

Parallel Scavenge收集器
  • 使用多线程进行垃圾回收
  • 使用复制算法
  • 吞吐量优先
  • 是HotSpot虚拟机在server模式下默认的新生代垃圾收集器

与其它收集器不同,其它收集器关注缩短系统停顿时间,而 Parallel Scavenge关注吞吐量

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

高吞吐量可以高效利用cpu执行程序代码,适合在不需要太多交互的应用中使用。
 

Parallel Scavenge可以使用gc自适应调节:jvm根据当前运行状况,动态调整设置最适合的gc停顿时间、吞吐量

#启用gc自适应调节
-XX:UseAdaptiveSizePolicy


#也可以使用以下方式进行手动设置,但一般不手动设置

#设置gc最大暂停时间,在这个时间范围内,至少进行一次gc
-XX:MaxGCPauseMillis=600000
#设置吞吐量,默认99,即吞吐量为99%
-XX:GCTimeRatio=99

 

老年代常见的3种收集器
Serial Old
  • 和Serial 一样使用单线程进行垃圾回收
  • 使用标记整理算法
  • 用于老年代的垃圾回收,是HotSpot虚拟机 Client模式下默认的老年代垃圾回收器

 

Parallel Old(默认)
  • 使用多线程进行垃圾回收
  • 使用标记整理算法
  • 吞吐量优先
  • 是HotSpot虚拟机在server模式下默认的老年代收集器
#取消默认的Parallel Old收集器
-XX:-UseParallelOldGC

 

CMS收集器
  • 使用标记-清除算法
  • 关注系统停顿时间,以系统最短停顿时间为目标,适合与用户交互多的程序。
  • 是server模式下主流的老年代垃圾收集器
     

cms进行垃圾回收的主要步骤

  • 初始标记 :标记作为GC Root的对象(stop the world)
  • 并发标记:根据可达性分析算法找出所有的引用链
  • 重新标记:修正并发标记期间因用户程序导致的标记变动(stop the world)
  • 并发清除:回收标记对象

只在初始标记、重新标记阶段出现stop the world,其它回收阶段可以和用户线程并发执行,回收垃圾引起的系统停顿时间几乎可以忽略不计。

web服务端重视响应速度,希望gc引起的系统停顿时间尽可能短,以带给用户更好的体验,cms正好符合web应用的需求,是java web应用老年代主流使用的垃圾收集器。

在这里插入图片描述

说明

和其它收集器不同,cms在垃圾收集阶(并发清除)段还需要运行应用,需要预留足够的内存空间供应用使用,所以cms不能像其它收集器一样等到老年代快满了才进行垃圾回收。

如果cms预留的内存空间不能满足应用运行的需要,jvm会临时使用Serial Old代替cms收集老年代,引起的停顿时间较长。

 

cms的优缺点

  • 优点:并发收集,系统停顿时间短
  • 缺点:本次回收时不能清除并发标记阶段新产生的垃圾;进行并发清除时还要运行应用(运行应用时还需要使用一些线程来并发清除垃圾),对cpu很敏感;使用标记-清除算法,会产生大量的内存碎片。
     
#启用CMS收集器,CMS是ConcMarkSweep的缩写
-XX:+UseConcMarkSweepGC

 

整堆收集器 G1
  • G1收集器将整个堆划分为多个大小相等的独立区域(Region),可以对整个堆进行垃圾回收
  • 使用标记-整理算法,不会产生内存碎片
  • 和CMS一样都关注于缩短系统停顿时间,但G1的系统停顿时间是可预测的
  • 回收价值大

G1 会跟踪各个 Region 里面垃圾的回收价值,记录回收所获得的空间、所花费的时间,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

这也是 Garbage- Firsti 名称的由来、以及可预测的原因,这种方式保证了 G1 在有限时间内获取尽可能高的回收价值。‘

 

g1进行垃圾回收的主要步骤

  • 初始标记(Initial Marking) :标记作为GC Root的对象
  • 并发标记(Concurrent Marking):根据可达性分析算法找出各个Region所有的引用链
  • 最终标记(Final Marking) :修正并发标记期间因用户程序导致的标记变动
  • 筛选回收(Live Data Counting and Evacuation):回收各个Regin中Remembered Set 之外的部分
     
    在这里插入图片描述

 

垃圾收集器总结、选择建议
名称收集年代使用的垃圾回收算法gc线程关注点地位
Serial收集器新生代复制算法单线程client模式下默认的新生代收集器
ParNew收集器新生代复制算法多线程新生代主流使用的收集器
Parallel Scavenge收集器新生代复制算法多线程关注吞吐量server模式下默认的新生代收集器
Serial Old收集器老年代标记整理算法单线程client模式下默认的老年代收集器
Parallel Old收集器老年代标记整理算法多线程关注吞吐量server模式下默认的老年代收集器
CMS收集器老年代标记清除算法单线程、多线程混合关注缩短系统停顿时间老年代主流使用的收集器
G1收集器所有年代标记整理算法单线程、多线程混合关注缩短系统停顿时间、高价值回收优秀,但低版本jdk中包含的G1版本尚不成熟

 

java web应用的收集器选择

  • jdk9及之后版本的jdk:默认收集器是G1,也推荐使用G1
  • jdk8及之前版本的jdk:包含的G1版本尚不成熟,推荐使用ParNew收集新生代,使用CMS收集老年代

 

常用的JVM调优参数

jvm内存不是越大越好,内存太小会频繁GC,内存太大触发GC时停顿时间会较长。根据压测结果不断调整jvm内存大小,合适就好,并非越大越好。
 

优化指标

  • 吞吐量=非GC停顿时间/系统运行总时间,一般要将吞吐量优化到95%甚至98%以上,即GC停顿时间控制在系统运行总时间的2%或5%以内。
  • 尽量减少Full GC次数,将单次Full GC造成停顿时间控制在1s以内。

 

jvm通用调优参数
#启动应用的时候可以设置jvm参数,单位直接用 k、m、g
#在IDEA中同样可以设置jvm参数,参数之间都是用空格分隔
java -Xms512m -Xmx512m -jar xxx.jar


#控制台打印gc信息
-verbose:gc -XX:+PrintGCDetails


#一般将初始堆内存、最大堆内存设置为一样的,防止堆扩容时引起内存抖动、影响程序运行的稳定性
-Xms10g  #初始堆内存,默认为物理内存的1/64
-Xmx10g  #最大堆内存,默认为物理内存的1/4


-Xss256k  #每个线程 java虚拟机栈的大小

-XX:MetaspaceSize=1g  #元空间的初始内存
#-XX:MaxMetaspaceSize  #元空间的最大内存,默认不限制。jdk的元空间直接使用本地内存,不占用堆内存,不用指定元空间的最大内存。

 

基于分代收集算法实现的GC的通用调优参数

主流的垃圾回收算法是分代收集算法,以下的jvm调优参数也是针对分代收集算法的

-Xmn512m  #新生代大小

-Xx:NewRatio=3  #老年代、新生代的大小比例,默认3
-Xx:SurvivorRatio=8  #Eden区与一个Servivor区的大小比例,默认8。2个servivor区的大小比例默认1:1

-XX:MaxTenuringThreshold=15  #新生代对象晋升到老年代需经历的Minor GC次数,默认15

-XX:PretenureSizeThreshold=3145728  #大对象阈值,单位字节,体积超过这个值就认为是大对象,直接在老年代分配空间

 

主流收集器各自的调优参数
#ParNew
-XX:+UseParNewGC  #启用ParNew收集器
-XX:ParallelGCThreads=n  #ParNew回收垃圾使用的线程数。默认为cpu核心数,但使用docker部署应用时,可能取的是物理机的cpu核数,而非分配给容器的cpu核数,最好手动指定,避免踩坑


#CMS
-XX:+UseConcMarkSweepGC  #启用CMS收集器,CMS是ConcMarkSweep的缩写
-XX:ParallelCMSThreads=n  #CMS回收垃圾使用的线程数,和ParNew的一样,在docker下容易踩坑,应该手动配置

-XX:+UseCMSCompactAtFullCollection  #在FullGC后压缩整理内存碎片
-XX:CMSFullGCBeforeCompaction=4  #每隔4次FullGC才整理压缩一次内存碎片

-XX:UseCMSInitiatingOccupancyOnly  #使用内存占用阈值触发GC
-XX:CMSInitiatingOccupancyFraction=70  #(老年代)内存占用达到70%就触发GC


#G1
-XX:+UseG1GC  #启用G1收集器
-XX:MaxGCPauseMillis=n  #GC最大停顿时间,G1会尽可能满足这个参数
-XX:G1HeapReginSize=n  #每个Regin的大小

 

这些jvm调优参数并非1次就确定下来,需要不断调整参数,反复测试查看GC日志,比较效果以找到合适的值。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值