分代收集理论

将内存分区,不同区域使用不同的收集方式

部分收集

  • 新生代收集(Minor GC/Young GC): 指目标只是新生代的垃圾收集。
    - 老年代收集(Major GC/Old GC): 指目标只是老年代的垃圾收集,目前只有CMS收集器会有单 独收集老年代的行为。

    整堆收集

FullGC,当新生代和老年代都满了的时候部分收集无法解决内存不足的问题时,FullGC就开始工作了。
混合收集(Mixed GC): 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有G1收集器会有这种行为。(特指G1)

垃圾回收算法⭐

标记-清除算法

1.标记需要回收的对象,进行清除
2.标记不需要回收的对象,剩下全部清除
image.png
缺点:
1.效率低,由于标记和清除是两步操作,如果堆中存在大量对象那么这个效率就会很低,标记过程也会很长
2.清除完的内存并不是连续的,会存在内存碎片。若此时存入一个大对象,有可能某一段内存不够用了,那么就会触发下一次GC

标记-复制算法

标记要清理的对象,预留出一半的内存。将清理的对象和不用清理的对象分隔开,并且通过复制让两部分自己的内存是连续的,之后对要清理的部分进行清理。
image.png
缺点:
1.要满足复制这个操作,还需要预留出一半的内存区域用来存放存活的对象,使总体的内存更小了,GC会更加频繁
2.存活的对象少的时候可以使用标记复制算法,不然做大量的复制操作效率很低(年轻代使用是OK的)
3.当存活对象高达98%或者近似这个数值,那么就不能使用这种算法(老年代)
新生代中的对象有98%熬不过第一轮收集。 因此 并不需要按照1∶1的比例来划分新生代的内存空间。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的 Survivor空间, 每次分配内存只使用Eden和其中一块Survivor。在年轻代的结构图中是存在两块survivor区域,但是每次只会用到一个survivor,之前被使用到的survivor会将存活对象放入下一个survivor后对该区做清理,也就是说年轻代每次GC都会有10%的空间被浪费

标记-整理算法

根据老年代的特点,产生了标记-整理这个过程,在标记后让存活的对象向一端移动,随后直接清理掉端边界以外的内存。
image.png
任何方法都不是完美的,移动过程在内存回收时更复杂,不移动则在内存分配时会更复杂。根据垃圾收集的停顿时间来看,不移动的对象停顿时间更短,甚至不需要停顿,从整体上看移动对象性能会更好

对象分配策略⭐

对象优先分配在Eden区

对象创建好后首先放入Eden区,当没有空间的时候便发起一次MinorGC。

大对象直接进入老年代(Minor GC后的对象太多无法放入Survivor区怎么办?)

image.png
当遇到这种情况,直接让大对象存入老年代中。那种很长的字符串以及数组对象。虚拟机提供了一个“-XX:PretenureSizeThreshold”参数,让大于这个设置值的对象直接在老年代中分配。如果survivor和eden区都没空间了,也会直接进入老年代。

长期存活的对象直接进入老年代

虚拟机给每个对象定义了一个年龄计数器(Age)。如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survior容纳的化,将被移动到Survior空间,并且设置对象年龄位1。在Survior中每熬过一次MinorGC,年龄就加一,当年龄加到一定程度(默认15岁),就会被晋升到老年代中。年龄可以通过-XX:MaxTenuringThreshold参数设置。

动态对象年龄判断

并不是一定要通过-XX:MaxTenuringThreshold参数去判断是不是转移进老年代中。
当前存放对象的Survivor区域中,如果相同年龄所有对象的总大小大于了这块Survivor内存大小的50%,年龄大于或等于该年龄的对象就可以直接进入老年代。
image.png

老年代空间分配担保规则

当老年代的空间不够用了,检查一下老年代可用的空间,是否大于新生代所有对象的总大小。(在极端情况下,可能Minor GC过后,所有对象都存活下来了),如果老年代空间大小大于新生代所有对象的总大小时,就可以放心的开始GC。

万一老年代的空间也不够用了该怎么处理
前提:当老年代的可用内存小于新生代所有对象的总大小,并且新生代对象全部存活下来
处理操作:
1.GC之前,当老年代的空间不足时,会看一个“-XX:-HandlePromotionFailure”的参数是否设置了,如果设置就进行下一步判断(1.6后废弃,直接判断下一个)
2.老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小(类似做一个预测,意味着触发了minor gc是很有可能让对象存进来的)。
image.png
以上操作是按顺序进行,一个不满足就会触发Full GC。
都满足的话,就进行Minor GC。在Minor GC过后,还不满足对象需要的内存空间时,就会发生“HandlePromotion Failure”的情况,这个时候就会触发一次“Full GC”。
1.6之后废除了这个参数,只用考虑老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行MinorGC,否则进行FullGC
Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。因为这个时候必须得把老年代里的没人引用的对象给回收掉,然后才可能让Minor GC过后剩余的存活对象进入老年代里面。
要是Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的“OOM”内存溢出了

年轻代和老年代适合哪些回收算法⭐

年轻代垃圾回收算法

年轻代垃圾回收算法使用的是标记-复制算法
对标记-复制算法的优化,新生代为什么要有Eden,From和To?
新生代内存区域划分为三块:Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说Eden区有800MB内存,每一块Survivor区就100MB内存,
image.png
当触发GC的时候,会把Eden区中的存活对象都一次性转移到一块空着的Survivor区。接着Eden区就会被清空,然后再次分配新对象到Eden区里,然后就会如上图所示,Eden区和一块Survivor区里是有对象的,其中Survivor区里放的是上一次Minor GC后存活的对象。
这么做最大的好处,就是只有10%的内存空间是被闲置的,90%的内存都被使用上了,实际上是80%的内存被用上,因为GC同时也对survivor1,2 做清理。而原本在survivor区的对象通过GC,会在1(From),2(To)两者移动。

老年代垃圾回收算法

老年代采取的是标记整理算法(不同垃圾回收器用的不同)
老年代区域中的对象分散在不同的地方,此时会让这些存活对象在内存里进行移动,把存活对象尽量都挪动到一边去,让存活对象紧凑的靠在一起,避免垃圾回收过后出现过多的内存碎片,移动完成后将垃圾一次性回收掉。
image.png —> image.png
注意:老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况。

引申:从对象的存活时间,对象移动过程,新生代和老年代的gc方式和时效来看,jvm的调优就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。

垃圾回收案例

背景

数据计算系统,日处理数据量在上亿的规模。这个系统就是会不停的从MySQL数据库以及其他数据源里提取大量的数据,加载到自己的JVM内存里来进行计算处理,如下图所示。
image.png
这个数据计算系统会不停的通过SQL语句和其他方式从各种数据存储中提取数据到内存中来进行计算,大致当时的生产负载是每分钟大概需要执行500次数据提取和计算的任务。
这是一套分布式运行的系统,所以生产环境部署了多台机器,每台机器大概每分钟负责执行100次数据提取和计算的任务。每次会提取大概1万条左右的数据到内存里来计算,平均每次计算大概需要耗费10秒左右的时间。
每台机器是4核8G的配置,JVM内存给了4G,其中新生代和老年代分别是1.5G的内存空间。
image.png

Eden区何时被填满

分析一下每分钟都有多大的对象要进入Eden区。大概每条数据包含了平均20个字段,可以认为平均每条数据在1KB左右的大小。一次对一万条数据做计算,一万条数据大概就是10mb左右的大小。
按照8:1:1的比例分配Eden区最多1.2g。
image.png
同时一分钟内要有100次请求,100*10Mb大约一个g就没有了。也就是说一分钟左右这个Eden就要满了

触发Minor GC的时候会有多少对象进入老年代

1.触发Minor GC首先要判断老年代的可用内存是不要大于年轻代中所有对象的大小
此时这是符合这个情况的。
image.png
2.估算一下有多少对象是无法被回收的,由于10w条数据处理需要10秒钟的时间,假设有80个计算任务执行结束了,还剩20个任务没有跑完,这20个任务的数据就不能被回收也就是200mb。剩下1g的是可以被回收走的。
image.png
此时一次Minor GC就会回收掉1GB的对象,然后200MB的对象也不能放入Survivor区中,因为存在空间担保的机制,这200MB就直接进入老年代了。
image.png

系统运行多久,老年代大概就会填满?

每分钟执行一次MinorGC,同时会存在200mb的对象进入老年代
假设两分钟过去了,此时存活对象有400mb了,老年代只有1.1gb,如果第三分钟运行完毕,又要进行MinorGC,此时由于老年代的大小不再大于Eden区总对象大小,就要做别的判断了。
检查“-XX:-HandlePromotionFailure”参数被打开了,当然一般都会打开,此时会进入第二步检查,就是看看老年代可用空间是否大于历次Minor GC过后进入老年代的对象的平均大小。之前的几次GC都是将200mb大小的对象放入老年代,那么以上的判断也成立,接下来就往老年代中存放对象。
1.1g-200mb-200mb-200mb-200mb-200mb-200mb=126.4mb。这个计算代表从现在开始最多执行5次MinorGC就再也不能往老年代中放对象了,此时还没算from和to这两个区域的对象。因此,老年代在MinorGC执行了大约6-7次就满了。

系统运行多久就要出发FullGC了?

按上述分析,由于每分钟基本上都要触发一次MinorGC,顶多7分钟老年代就被填满了,当第8分钟的时候Eden区数据也满了,此时就要触发FullGC了。
按照这个运行,基本上平均就是七八分钟一次Full GC,这个频率就相当高了。因为每次Full GC速度都是很慢的,性能很差。

如何优化这种情况

因为这个系统是数据计算系统,每次Minor GC的时候,必然会有一批数据没计算完毕,在上述分析的时候我们的survior区几乎没有被用到,因为form和to大小太小了放不下自然不走“年龄判定规则”这个判断。白白浪费了20%的空间,同时本该清理的200mb对象由于没有计算完就要被移动到了老年代,就很浪费老年代的空间。如果Survivor的空间大一点,这200mb的对象在下一次MinorGC的时候就被清理掉了。
就需要调整内存比例,增加了新生代的内存比例,3GB左右的堆内存,其中2GB分配给新生代,1GB留给老年代。这样Survivor区大概就是200MB,每次刚好能放得下Minor GC过后存活的对象了。
image.png
通过这个分析和优化,把生产系统的老年代Full GC的频率从几分钟一次降低到了几个小时一次,大幅度提升了系统的性能,避免了频繁Full GC对系统运行的影响。
同时也可以增加survivor区的比例,为了避免动态年龄判定规则把Survivor区中的对象直接升入老年代,在这里如果新生代内存有限,那么可以调整”-XX:SurvivorRatio=8”这个参数,默认是说Eden区比例为80%,也可以降低Eden区的比例,给两块Survivor区更多的内存空间,然后让每次Minor GC后的对象进入Survivor区中,还可以避免动态年龄判定规则直接把他们升入老年代。

公式研究: 每台机器可以提供给JVM的最大内存: each_m,比如2核4G机器,可提供JVM最大内存2G 栈占用:stack_m = QPS估值 1M 20倍数,估值30QPS, 栈约为600M 新生代以30分钟一次GC计算总内存:30(Monitor GC间隔) 60 QPS估值 接口内存估值,young_m 所需机器数量,假设等于N 方法区:200M,一般够用,method_m 老年代:500M,一般不大,300M也行,像我们结算服务,100M都够用 old_m 演算公式: JVM最大内存N = stack_m + young_m + old_m + method_m * N 机器数N,也同时估算出来,是这样吗