程序的运行必然申请内存资源,如果无效的对象不清理一直占用资源,那么肯定会导致内存溢出,所以内存资源的管理就很重要了
假设有一个对象a,任何对对象A的引用,引用计数器都会加1,当引用失败时,对象A的引用计数器就-1,如果对象计数器的值为0,表示对象没有引用可以被回收了。
class TestA {
public TestB b;
}
class TestB {
public TestA a;
}
public class Main {
public static void main(String[] args){
A a = new A();
B b = new B();
a.b=b; b.a=a;
a = null;
b = null;
}
}
虽然 a 和 b 都为null,但是它们直接存在互相引用,所以永远不会被回收。
标记清除算法是将垃圾回收分为两个阶段,分别是标记和清除
根据老年代的特点提出的一种标记算法,标记过程与标记清除算法一致,在清理阶段则不是简单的清理,而是将存货的对象向一端压缩,然后清理边界以外的垃圾,解决碎片化的问题。
优缺点同标记清除算法一致,解决了内存碎片化的问题,但是多了一步,对象移动内存位置的步骤,其效率也有一定影响。
为了解决效率问题,标记复制算法出现了。复制算法的核心就是将内存一分为二,每次只用其中的一块,在垃圾回收时,将还存活的对象复制到另一个空间中,然后将内存空间清空,交换两个内存的角色,完成垃圾回收。在Young Region 中就使用到了标记复制算法(Suvivor TO 和 Suvivor From)
标记和清除都需要遍历所有对象,在运行中会一直产生对象,所以在遍历中可能产生新的对象,标记可能失败,也可能错误清除对象,清除了正在使用对象。
2.5、分代(收集)算法
当前虚拟机的垃圾回收算法都才用分代收集算法,这种算法没有什么新思想,只是根据对象的存活周期将内存分为几块。一般将java的内存分为新生代和老年代,所以我们可以根据内存区域的特点选择不同的垃圾回收算法。
新生代中每次收集都会有大量对象死去,所以我们可以使用标记-复制算法,只需要付出少量对象的复制成本就可以完成垃圾回收。而老年代的对象存活几率是最高的,而且没有额外的空间对它进行分配,所以我们选择标记清除算法
或标记整理算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wv3MhSj-1652694235241)(AD15D4A8022F4DD5A88B7F78E0DA04D0)]
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。
Serial (串行)是历史最悠久的垃圾收集器了,它是一个单线程收集器,垃圾回收时,只有一个线程在工作,且java应用中的所有工作线程都要暂停,等待垃圾回收的完成。这种现象为STW(Stop The World),直到它收集结束,才会重新唤醒程序其他工作线程。
新生代采用标记-复制算法,老年代采用标记-整理算法。
虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。javaweb应用不会使用串行垃圾收集器。
并行和并发概念补充:
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
ParNew 收集器其实就是Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样,它是工作在新生代上的,老年代用的还是串行收集器。
ParallelGC 收集器的工作机制和 ParNew 收集器一样,只是在此基础之上,新增了两个和 系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。
-XX:+UseParallelGC
年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器。
-XX:+UseParallelOldGC
年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。
-XX:MaxGCPauseMillis
设置最大的垃圾收集时的停顿时间,单位为毫秒 需要注意的时,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他 的参数,如果堆的大小设置的较小,就会导致GC工作变得很频繁,反而可能会 影响到性能。 该参数使用需谨慎。
-XX:GCTimeRatio
设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)。 它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%
-XX:UseAdaptiveSizePolicy
自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、 堆大小、停顿时间之间的平衡。 一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
这是 JDK1.8 默认收集器
使用 java -XX:+PrintCommandLineFlags -version
命令查看
-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征,它是jdk1.7正式使用的全新垃圾收集器,oracle官方计划在jdk1.9中将G1变成默认的垃圾收集器,以替代CMS
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优: 1. 第一步,开启G1垃圾收集器 2. 第二步,设置堆的最大内存 3. 第三步,设置最大的停顿时间 G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件 下被触发。
G1 垃圾收集器相对比其他收集器而言,最大的区别在于它取消了新生代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的新生代和老年代区域。
这样做的好处就是不用再对单个的空间对每个代进行设置了,不用担心每个代的内存是否足够。
在G1划分的区域中,年轻代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Suvivor空间中,G1收集器通过将对象从一个区域复制到另一个对象区域,完成了清理工作。
这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样处理了CMS中内存碎片化的问题。
在G1中,有一种特殊的区域,叫Humongous区域。
在GC年轻代的对象时,我们如何找到年轻代中对象的根对象呢? 根对象可能是在年轻代中,也可以在老年代中,那么老年代中的所有对象都是根么? 如果全量扫描老年代,那么这样扫描下来会耗费大量的时间。 于是,G1引进了RSet的概念。它的全称是Remembered Set,其作用是跟踪指向某个堆 内的对象引用。
每个Region初始化时,会初始化一个RSet,该集合用来记录并跟踪其它Region指向该 Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录 的东西应该是 xx Region的 xx Card。
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一 个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region,这里需要注意:是一部分老年代,而不是全部 老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。 也要注意的是Mixed GC 并不是 Full GC。 MixedGC什么时候触发? 由参数 -XX:InitiatingHeapOccupancyPercent=n 决定。默 认:45%,该参数的意思是:当老年代大小占整个堆大小百分比达到该阀值时触发。 它的GC步骤分2步: 1. 全局并发标记(global concurrent marking) 2. 拷贝存活对象(evacuation)
全局并发标记,执行过程分为五个步骤: 初始标记(initial mark,STW) 标记从根节点直接可达的对象,这个阶段会执行一次年轻代GC,会产生全局停 顿。
根区域扫描(root region scan) G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。 该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下 一次 STW 年轻代垃圾回收。
并发标记(Concurrent Marking) G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行, 可以被 STW 年轻代垃圾回收中断。
重新标记(Remark,STW) 该阶段是 STW 回收,因为程序在运行,针对上一次的标记进行修正。
清除垃圾 (clearup,STW)
清点和重置标记状态,该阶段会STW,这个阶段并不会实际去做垃圾的收集,等待evacuation阶段来回收。
Evacuation阶段是全暂停的。该阶段把一部分Region里的活对象拷贝到另一部分Region