博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
JVM 垃圾回收机制、垃圾收集器、调优参数
阅读量:2420 次
发布时间:2019-05-10

本文共 6725 字,大约阅读时间需要 22 分钟。

目录

 

C++的内存回收很麻烦,不回收可能会造成内存泄漏,Java中由GC完成内存回收,不用手动回收内存。堆是垃圾回收的重点区域。

 

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

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

 

引用计数算法

堆中每个对象都对应一个引用计数器,当一个变量引用此对象时,计数器+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虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类的静态成员、常量引用的对象
  • 被同步锁synchronized持有的对象

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

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

 

对象的finalization机制

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

 

堆中对象可能的三种状态

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

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

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

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

如果对象所属的类重写了finalize()方法,且没有执行过finalize()方法,则把对象放入finalize队列中,
由jvm创建的一个低优先级的finalizer线程处理队列中对象,调用对象的finalize()方法,如果没有复活,则把对象标记为不可触及的。

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

 

垃圾回收算法

标记-清除算法 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,发生在新生代的垃圾收集,新生代对象回收率通常高达98%,Minor GC 非常频繁。
  • Full GC:又叫做Major GC,老年代 GC,发生在老年代的垃圾收集,老年代内存大、对象多,回收速度慢,Full GC的时间花销一般是 Minor GC 的10 倍以上。

 

触发full gc的条件

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

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

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

 

对象如何晋升到老年代

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

 

垃圾收集器

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模式下主流的老年代垃圾收集器

在这里插入图片描述

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

web服务端重视响应速度,希望gc引起的系统停顿时间尽可能短,以带给用户更好的体验。

CMS只在初始标记阶段出现stop the world,其它回收阶段可以和用户线程并发执行,回收垃圾引起的系统停顿时间几乎可以忽略不计,是server模式下主流的老年代垃圾收集器。

 

#启用CMS收集器,CMS是ConcMarkSweep的缩写-XX:+UseConcMarkSweepGC

 

优点:系统停顿时间短

缺点:使用标记-清除算法,容易产生大量的内存碎片。

 

整堆收集器 G1

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

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

这也是 Garbage- Firsti 名称的由来、以及可预测的原因,这种方式保证了 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日志,比较效果以找到合适的值。

转载地址:http://zkhlb.baihongyu.com/

你可能感兴趣的文章
如何用 160 行代码,实现动态炫酷的可视化图表?
查看>>
山东到底有没有互联网?
查看>>
10 步教你接手同事的代码!
查看>>
买不到口罩怎么办?Python 爬虫帮你时刻盯着自动下单!| 原力计划
查看>>
一图读懂浏览器演变史 | 每日趣闻
查看>>
苹果安全漏洞曝光:可能有 5 亿部 iPhone 易受攻击
查看>>
打造金融科技银行,招行的底气源自……
查看>>
火爆全网的动态曲线图是怎么做的?
查看>>
程序员感叹一年只能存下15万太少了……网友:潸然泪下
查看>>
文科出身敲出 Instagram,被小札“挤”走,建新冠追踪网站,这个程序员有点牛!...
查看>>
面对 Python,Java 中枪了 | 每日趣闻
查看>>
地方普通院校的计算机专业「科班」学生如何突围而出?| 原力计划
查看>>
小白也能看懂的 Java 异常处理
查看>>
C++ 是如何从代码到游戏的?
查看>>
程序员惊魂 12 小时:“���”引发线上事故
查看>>
调查了 10,975 位 Go 语言开发者,我们有了这些发现!
查看>>
面试官吐槽:“Python程序员就是不行!”网友:我能把你面哭!
查看>>
太真实!深刻解读论文里的话术| 每日趣闻
查看>>
拿来就能用!Python 每天定时发送一句情话 | 原力计划
查看>>
Java“拍了拍”你,面试其实没那么难...
查看>>