JVM学习笔记(二) JVM的垃圾回收及调优

一、如何判断对象可以回收

1.引用计数法(reference-counting)

每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。

缺点:若两个对象相互引用,他们的count都不会是0,所以一直不能被垃圾回收。

2.可达性分析算法(GC Roots Tracing)

扫描堆中的对象,从GC Roots作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于GC Roots无法到达的对象便成了垃圾回收的对象,随时可被GC回收。

可以作为GC Roots的对象:

  • 虚拟机栈的栈帧的局部变量表所引用的对象;
  • 本地方法栈的JNI所引用的对象;
  • 方法区的静态变量和常量所引用的对象;

3.四种引用

强引用

  • 只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。

软引用(SoftReference)

  • 仅有软引用引用该对象时,在垃圾回收后,内存不足时会再次出发垃圾回收,回收软引用对象
  • 可以配合引用对象来释放软引用自身

弱引用(WeakReference)

  • 仅有弱引用引用该对象时,在垃圾回收时,无论空间是否足够,都会回收弱引用对
  • 可以配合引用队列来释放弱引用自身

虚引用(PhantomReference)

  • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。

终结器引用(FinalReference)

  • 无需手动编码,其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalier线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC 时才能被回收引用的对象。

二、垃圾回收算法

1.标记-清除算法

算法分为“标记”和清除阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

  • 速度快
  • 会造成内存碎片

2.复制算法

将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完毕后,就将还存活的对象复制到另一块中去,然后把使用的空间一次清理掉。

  • 不会有内存碎片
  • 需要占用双倍内存空间

3.标记-整理算法

根据老年代的特点特别出的一种标记算法,标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收的对象进行垃圾回收,而是让所有存活的对象向另一端移动,然后直接清理掉端边界以外的内存。

  • 速度慢
  • 没有内存碎片

三、分代垃圾回收

  1. 对象首先在Eden区分配
  2. 新生代空间不足时,触发Minor GC,Eden区和Survivor From区存活的对象使用复制算法到Survivor To区,存活的对象年龄加1,并且交换From和To。
  3. Minor GC会引发Stop The World机制,暂停其他用户线程,等垃圾回收结束以后,用户线程才恢复运行。
  4. 当对象寿命超过阀值时,会移动到老年代,最大寿命是15次(4bit)。
  5. 当老年代空间不足,会先尝试触发Minor GC,如果之后空间仍不足,那么触发Full GC,STW的时间更长。  

相关JVM参数

含义                                                                  参数

堆初始大小                                     -Xms

堆最大大小                                     -Xmx 或 -XX:MaxHeapSize=size

新生代大小                                     -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size)

幸存区比例(动态)                       -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy

幸存区比例                                     -XX:SurvivorRatio=ratio

晋升阀值                                        -XX:MaxTenuringThreshold=threshold

晋升详情                                        -XX:+PrintTenuringDistribution

GC详情                                          -XX:+PrintGCDetails -verbose:gc

Full GC 前Minor GC                      -XX:+ScavengeBeforeFullGC

-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC

四、垃圾回收器

1.串行

  • 单线程
  • 堆内存较小,适合个人电脑

-XX:+UseSerialGC=Serial+SerialOld    (Serial:复制算法,SerialOld:标记-整理算法)

2.吞吐量优先

  • 多线程
  • 堆内存较大,多核CPU
  • 让单位时间内,STW的时间最短

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC    (复制 标记-整理)

-XX:UseAdaptiveSizePolicy (自适应的调整策略,调整新生代大小)

-XX:GCTimeRatio=ratio   计算公式:1 / (1+ratio)

-XX:MaxGCPauseMillis=ms  最大暂停时间默认200ms

-XX:ParallelGCThreads=n (并发线程数)

3.响应时间优先

  • 多线程
  • 堆内存较大,多核CPU
  • 尽可能让单次STW的时间最短

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

-XX:ParallelGCThreads=n(初始标记的并发线程数) ~ -XX:ConcGCThreads=threads(并发标记的线程数)

-XX:CMSInitiatingOccupancyFraction=percent(何时进行CMS垃圾回收的时机)

-XX:+CMSScavengeBeforeRemark(在重新标记之前做一次垃圾回收)

在内存碎片比较多的情况下,(标记-清除算法会产生内存碎片),可能造成将来,分配对象时,MinorGC不足,老年代由于碎片过多也不足,这样会造成并发失败,这时候CMS就不能正常工作了,会退化为SerialOld,做一次单线程串行的垃圾回收,这样垃圾回收的时间就会变的很长。

G1(Garbage First)垃圾回收器

适应场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region
  • 整体上是标记-整理算法,两个区域之间是复制算法

相关JVM参数

-XX:+UseG1GC

-XX:G1HeapRegionSize=size

-XX:MaxGCPauseMillis=time

G1垃圾回收阶段

Young Collection

  • 会STW

Young Collection +CM

  • 在Young GC时会进行GC Root的初始标记
  • 老年代占用堆空间比例达到阀值时,进行并发标记(不会STW),由下面的JVM参数决定
  • -XX:InitiatingHeapOccupancyPercent=percent(默认是45%)

Mixed Collection

会对E、S、O进行全面垃圾回收

  • 最终拷贝(Remark)会STW
  • 拷贝存活(Evacuation)会STW

-XX:MaxGCPauseMillis=ms

JDK 8u60回收巨型对象

  • 一个对象大于region的一半时,称之为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所以incoming引用,这样老年代incoming引用为0的巨型对象可以在新生代垃圾回收时处理掉。

HotSpot官方调优文档

五、垃圾回收调优

查看虚拟机运行参数
"C:\Program Files\Java\jdk1.8.0_271\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"

1.调优领域

  • 内存
  • 锁竞争
  • CPU占用
  • IO

2.确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • 【低延迟】CMS,G1,ZGC
  • 【高吞吐量】ParallelGC

3.最快的GC是不发生GC

  • 查看FullGC前后的内存占用,考虑下面几个问题
    • 数据量是不是太多了?
      • resultSet = statement.executeQuery("select * from 大表 limit n");
    • 数据表示是否臃肿?
      • 对象图
      • 对象大小 16bit Integer24bit int 4bit
    • 是否存在内存泄漏?
      • static Map map =
      • 第三方缓存实现

4.新生代调优

  • 新生代的特点
    • 所有的new操作的内存分配非常廉价
      • TLAB (thread-local allocation buffer):让每个线程用自己私有的内存进行对象分配
    • 死亡对象的回收代价是零
    • 大部分对象用过即死
    • Minor GC的时间远远低于Full GC
  • 越大越好吗?
    • -Xmn 设置新生代初始和最大值
    • 当新生代内存越大时,老年代内存就会越小,会频繁发生Full GC,会占用更长的时间
    • 建议新生代内存占整个堆内存:大于25%小于50%
    • 复制算法中:标记耗费时间短,复制耗费时间长
  • 新生代能容纳所以【并发量*(请求-响应)】的数据
    • 1000个用户,请求-响应的数据是512K,则总共需要512M的内存
  • 幸存区大到能够保留【当前活跃对象+需要晋升对象】,若到老年代,在FullGC发生时间需要再老年代内存满了才发生,所以尽可能在Minor GC时就回收。
  • 晋升阀值配置得当,让长时间存放对象加快晋升
    • -XX:MaxTenuringThreshold=threshold
    • -XX:+PrintTenuingDistribution

5.老年代代调优

以CMS为例

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有Full GC那么已经...,否则先尝试调优新生代
  • 观察发生Full GC时老年代内存占用,将老年代的内存预设调大1/4 ~ 1/3
    • -XX:CMSInitiatingOccupancyFraction=precent (内存占用老年代总内存多少的是否使用CMS进行垃圾回收)

6.案例

案例1:Full GC和Minor GC频繁

  • 先增大新生代内存,增大幸存区空间
  • 让生命周期较短的对象尽可能的留在新生代,不要晋升到老年代,这样Full GC就不会频繁出现

案例2:请求高峰期发生Full GC,单次暂停时间特别长(CMS)

  • 查看CMS日志,看哪个阶段耗费时间比较长,通过日志可以发现重新标记耗费时间长,会扫描整个堆内存,若新生代对象比较多,则耗费时间比较长;
  • 在重新标记之前做一次垃圾回收,-XX:+CMSScavengeBeforeRemark(在重新标记之前做一次垃圾回收),可以将2s左右降低到20ms左右

案例3:老年代充裕情况下,发生Full GC(环境:CMS jdk1.7)

  • 1.8有一个元空间作为方法区的实现,1.7使用的是永久代,而永久代的空间不足也会导致Full GC
  • 1.8以后,使用的操作系统的内存,空间比较充裕
  • 结果:方法区的空间不足,调大方法区的内存。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值