堆内存示意图
垃圾收集算法
1.标记-清除算法
算法分为标记和清除两个阶段。标记出所有需要回收的对象,在标记完成后,统一回收。
缺点:
- 执行效率不稳定,若堆中有大量对象要被回收,这是必须进行大量标记和清除动作,执行效率随对象增长而降低。
- 内存空间的碎片化问题,标记、清除后会产生大量不连续的碎片,当程序需要分配大对象时无法找到足够的连续内存而不得不提取触发垃圾收集。
2.标记-复制算法
多数对象是可回收的情况下,算法需要复制占少数的存活对象;每次针对半个区域进行内存回收,分配内存时不用考虑内存碎片情况,只要移动堆顶指针,按顺序分配即可。实现简单、运行高效。
缺点:
- 代价是将可用内存缩小为原来的一半,浪费内存空间。
3.标记-整理算法
标记过程与标记清除算法一样,后续步骤不一样。后续步骤是将存货对象向一端移动,然后直接清除掉端边界以外的内存。
优点:
- 相比标记清除算法,没有碎化的内存
- 相比标记复制算法,没有内存减半的消耗
缺点:
- 整理活动对象时,因对象位置变动,需要调整虚拟机栈中的引用地址
- 整理活动对象时,需要全程暂停用户线程,STW
- 效率相比于标记复制算法低一些
四种引用
1.强引用
最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
当一个对象被强引用变量引用时,它处于“活着”状态,它不可能被回收的。
2.软引用
SoftReference,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
/**
* java -Xms10m -Xmx10m SoftReferenceTest
*/
public class SoftReferenceTest {
static class HeapObject {
byte[] bs = new byte[1024 * 1024];
}
public static void main(String[] args) {
SoftReference<HeapObject> softReference = new SoftReference<>(new HeapObject());
List<HeapObject> list = new ArrayList<>();
while (true) {
if (softReference.get() != null) {
list.add(new HeapObject());
System.out.println("list.add");
} else {
System.out.println("---------软引用已被回收---------");
break;
}
System.gc();
}
}
}
3.弱引用
WeakReference,弱引用也是用来描述非必需对象的,它的强度比软引用更弱一些。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
/**
* jdk 1.8
*/
public class WeakReferenceTest {
static class TestObject{
}
public static void main(String[] args) throws InterruptedException {
WeakReference<TestObject> weakReference=new WeakReference<>(new TestObject());
System.out.println(weakReference.get() == null);//false
System.gc();
TimeUnit.SECONDS.sleep(1);//暂停一秒钟
System.out.println(weakReference.get() == null);//true
}
}
Tips:软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
4.虚引用
PhantomReference,最弱的一种引用关系,随时都有可能被垃圾回收器回收。
虚引用必须和引用队列(ReferenceQueue)联合使用,主要作用是跟踪对象被垃圾回收的状态。
JDK7/8后HotSpot虚拟机所有收集器
1.Serial收集器
新生代收集器
串行
单线程收集器,只用一个线程完成垃圾收集,且收集时必须暂停工作线程。
使用标记-复制算法。
- -XX:+UseSerialGC 指定年轻代和老年代都使用串行回收器,这是client模式下的默认选项
2.ParNew收集器
新生代收集器
串行
多线程收集,Serial收集器的多线程版本,GC时需要暂停工作线程
使用标记-复制算法
- -XX:+UseConcMarkSweepGC 指定使用CMS后,会默认使用ParNew作为新生代收集器;
- -XX:+UseParNewGC 强制指定使用ParNew;
- -XX:ParallelGCThreads 指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
3.Parallel Scavenge收集器
新生代收集器
并行
多线程收集,GC时需要暂停工作线程
高吞吐量为目标
标记-复制算法
虚拟机运行在Server模式下的默认垃圾收集器,适合那种交互少、运算多的场景.例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序.
- -XX:+MaxGCPauseMillis 控制最大垃圾收集停顿时间,大于0的毫秒数;这个参数设置的越小,停顿时间可能会缩短,但也会导致吞吐量下降,导致垃圾收集发生得更频繁。
- -XX:GCTimeRatio 设置垃圾收集时间占总时间的比率,0<n<100的整数,就相当于设置吞吐量的大小。
- -XX:+UseAdptiveSizePolicy 开启这个参数后,就不用手工指定一些细节参数,如: 1)新生代的大小(-Xmn);2)Eden与Survivor区的比例(-XX:SurvivorRation);3)晋升老年代的对象年龄(-XX:PretenureSizeThreshold)。(JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs))
Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。
CMS等收集器的关注点是尽可能地缩短STW停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
4.Serial Old收集器
Serial 收集器在新生代和老年代都有对应的版本,除了算法不同,两个版本没有其他的差异
老年代收集器
串行
单线程收集器,垃圾收集时必须暂停工作线程
标记-整理算法
Client模式:默认使用Serial Old 收集器。
Server模式:在JDK1.5以及之前的版本中,与Parallel Scavenge收集器配合使用;最为CMS收集器的后备预案,在并发收集发生“Concurrent Mode Failure”时使用。
5.Parallel Old 收集器
Parallel Old是Parallel Scavenge 的老年版本,除了算法不同,两个版本没有其他差异
老年代收集器
并行
多线程收集器,GC时需要暂停工作线程
高吞吐量为目标
标记-整理算法
在注重吞吐量及CPU资源敏感的场合,可以优先考虑 Parallel Scavenge + Parallel Old收集器组合。
JDK1.6及之后来代替Serial Old收集器,特别是在 Server模式和多CPU情况下。
- -XX:UseParallel Old:指定使用Parallel Old收集器。
6.CMS(Concurrent Mark Sweep)收集器
CMS是HotSpot在JDK5推出的第一款真正意义上的并发(Concurrent)收集器,让垃圾收集线程与用户线程(基本上)同时工作。
老年代收集器
并发
多线程,收集过程基本不需要暂停用户线程
标记-清除算法
以最短停顿时间为目标
适用于用户较多的场景,应用集中在互联网或B/S系统的服务器上。这类服务注重响应速度,希望停顿时间最短,以给用户带来极好的体验。
CMS GC 过程的步骤
初始标记
- 单线程执行;
- 需要STW;
- 仅把GC Roots的直接关联的对象给标记下(对象量较小),速度非常快
并发标记
- 从GC Roots的直接关联对象开始遍历整个对象图进行标记的过程
- 虽然处理时间长,但不需要STW
- 不能保证标记出所有的存货对象
重新标记
- 并发标记过程中,可能会产生新的垃圾,所有需要重新标记
- 此时需要STW,停顿时间笔初始标记稍长,但远比并发标记短。
并发清除
- 并发清除标记的垃圾
- 不会STW
参数
- -XX:+UseConcMarkSweepGC:使用CMS收集器
- -XX:+ UseCMSCompactAtFullCollection:Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
- -XX:+CMSFullGCsBeforeCompaction:设置进行几次Full GC后,进行一次碎片整理
- -XX:ParallelCMSThreads:设定CMS的线程数量(一般情况约等于可用CPU数量)
缺点
1.对CPU资源敏感
虽然不会暂停用户线程,但因占用一部分CPU资源,会导致应用程序变慢、总吞吐量降低。
CMS的默认收集线程数量 = (CPU+3) / 4;
当CPU多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大。
2.CMS所需要的空间比其他垃圾收集器大
由于在垃圾收集阶段用户线程还需要运行,那就需要预留足够的内存给用户线程使用,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集。可以理解为CMS所需要的空间比其他垃圾收集器大;
-XX:CMSInitiatingOccupancyFraction 指定当老年代空间使用的阈值达到多少才进行一次CMS垃圾回收。(JDK1.5默认值为68%;JDK1.6变为大约92%)
CMS并发清理阶段用户线程还在运行着,会产生新的垃圾(留待下一次清理),这一部分垃圾称为“浮动垃圾”。
3.Concurrent Mode Failure 失败
CMS的垃圾清理和应用线程是并行进行的,如果在并行清理的过程中老年代的空间不足以容纳应用产生的垃圾,则会抛出“concurrent mode failure”。
会临时启用Serial Old收集器来进行垃圾收集,此时应用线程暂停,停顿时间更长。
所以 "-XX:CMSInitiatingOccupancyFraction"不能设置得太大。
4.产生大量碎片
由于CMS基于"标记-清除"算法,所以会产生碎片。产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
- -XX:+UseCMSCompactAtFullCollection (此参数从JDK 9开始废弃)用于CMS不得不进行Full GC时开启内存碎片的合并整理过程;合并整理过程无法并发,停顿时间变长;默认开启;
- -XX:+CMSFullGCsBeforeCompaction (此参数从JDK 9开始废弃)设置执行多少次不压缩的Full GC后,来一次压缩整理;默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
7.G1收集器
G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。
G1 最主要的设计目标是:降低STW的停顿时间,将停顿的时间和分布变的可预期、可配置。
G1收集器的特点
- Region:G1将真个堆划分为多个大小相等的独立区域(regison)。
- 并行与并发:利用 多个 CPU来缩短 Stop-The-World 停顿时间(并行);让垃圾收集与用户程序同时执行(并发)。
- 分代收集:虽然保留了新生代和老年代,但能独立管理整个GC堆,无需与其他收集器搭配;
- 空间整合:从整体看是基于“标记-整理”算法;从局部上看是基于“标记-复制”算法实现的。所以不会产生内存碎片。
- 可预测的停顿:
G1收集器之所以能建立可预测的停顿时间模型,是因为:
- 它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
- G1跟踪各个Region获得其收集价值大小,后台维护一个优先列表
- 每次根据允许的收集时间,优先回收价值最大的Region,这样就保证了效率。
应用背景
1.面向服务端应用
针对具有大内存、多处理器的机器;为需要低GC延迟,并具有大堆的程序提供解决方案。如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
2.用来替换掉JDK1.5的CMS收集器
- 超过50%的Java堆被活动数据占用;
- 对象分配频率或年代提升频率变化很大;
- GC停顿时间过长(长与0.5至1秒)。
参数
- "-XX:+UseG1GC":指定使用G1收集器;
- "-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
- "-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;
- "-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;
回收价值:回收所获得的空间大小以及回收所需时间
注意:G1与前面的垃圾收集器有很大不同,它把新生代、老年代的划分取消了!
这意味着,在收集过程中完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
- G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。
- 部分区域为新生代,收集依然暂停应用线程,将存活对象拷贝到老年代或者Survivor空间。
- 部分区域为老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。
- 还有一种特殊的区域,叫Humongous区域。
Humongous区域
- 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象,默认分配在 年老代,但如果它存活期短,就会对垃圾收集器造成负面影响。
- 为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。
- 如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有 时候不得不启动Full GC。