1、部分收集(Partial GC):只针对部分区域进行垃圾收集。其中又分为:
1.1、新生代收集(Minor GC/Young GC):只针对新生代的垃圾收集。具体点的是Eden区满时触发GC。
Survivor满不会触发Minor GC 。
1.2、老年代收集(Major GC/Old GC):只针对 老年代的垃圾收集。
目前,只有CMS收集器会有单独收集老年代的行为。
注意,很多时候,Major GC 会和Full GC混淆使用,需要具体分辨是老年代的回收还是整堆回收。
1.3、混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。
目前只有G1收集器会有这种行为。
2、整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
分配担保机制;
大对象直接进入老年代;大对象是需要大量连续内存空间的对象,比如是数组、字符串;
大对象直接进入老年代的行为是直接由虚拟机动态决定的。
长期存活的对象将进入老年代
Hotspot遍历所有对象时,按照年龄从小到大对其占用的大小进行累积,当累积到某个年龄大小超过了survivor区的50%时,(默认是50%,可以通过 -XX:TargetSurvivorRatio=percent 来设置),取这个年龄和MaxTenuringThreshold中更小的一个值(`年龄用于区间范围内取值,从大于等于这个年龄的对象都进行晋升老年代;参数是设置新的survivor区晋升到老年代的年龄阈值` ),作为新的晋升年龄阈值。
老年代最大可用的连续空间 是否大于 历次晋升到老年代对象的平均大小(新生代晋升的平均大小)。
发现统计数据说之前的 young gc 的平均晋升 比 目前 old gen剩余空间大。(新>老)
finalize()方法:将对象从内存中清除出去之前做必要的清理工作。
finalize 的工作方式是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将会首先调用 finalize 方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
JDK 默认垃圾收集器(使用 `java -XX:+PrintCommandLineFlags -version` 命令查看):
- JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
- JDK 9 ~ JDK20: G1
JConsole:Java监视与管理控制器
是基于JMX的可视化监视。管理工具。可以很方便的监视本地及远程的java进程的内存使用情况。可以在控制台输入jconsole
命令启动或在JDK目录下的bin目录下找到jconsole.exe
然后双击启动。
Visual VM:多合一故障处理工具
基于NetBeans平台开发,一开始具备插件扩展功能特性,通过插件扩展支持。
具备功能:
Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java自动内存管理最核心的功能是堆内存中对象的分配与回收。
Java堆是垃圾收集器管理的主要区域,因此被称为GC堆。
从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆被划分了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。
在JDK7版本及JDK7版本之前,堆内存被通常分为下面三部分:
新生代内存(Young Generation):包含Eden区、两个Survivor区S0和S1区。
老年代(Old Generation)
永久代(Permanent Generation):JDK8版本之后PermGen(永久)已被Metaspace(元空间)取代,元空间使用的是直接内存。
大多数情况下,对象在新生代中Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
定义 `allocation1` 对象,分配内存空间,占满Eden区;再给 `allocation2` 对象分配内存空间。
当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。GC期间虚拟机又发现`allocation1` 无法存入Survivor空间,所以只好通过 分配担保机制 把新生代的对象提前专一到老年代中去,老年代上的空间足够存放`allocation1` ,所以不会出现 Full GC。执行 Minor GC后,后面分配的对象如果能够存在Eden区的话,还是会在Eden区分配内存。
Minor GC的问题与卡表分析
Minor GC存在一个问题是,老年代的对象可能引用新生代的对象。在标记存活对象的时候,就需要扫描老年代的对象,如果该对象拥有对新生代对象的引用,那么这个引用也会被作为GC Roots。这相当做了全堆扫描。
JVM如何避免Minor GC扫描全堆
HotSpot给出解决方案是一项叫做“卡表”的技术:
卡表的具体策略是 将老年代空间氛围512B的若干张卡,并且维护一个卡表,卡表本身是字节数组,数组中的每个元素对应着一张卡,其实就是个标识位,这个标识位代表对应的卡是否可能存有指向新生代对象的引用,如果可能存在,那么我们认为这张卡是脏卡。
在进行Minor GC时,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的老年代指向新生代的引用加入到Minor GC的GC Roots里,当完成所有脏卡的扫描之后,Java虚拟机便会将所有脏卡的标识位清零,这样虚拟机以空间换时间,避免了全表扫描。
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
大对象直接进入老年代的行为是由虚拟机动态决定的,它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。
-XX:G1HeapRegionSize
参数设置的堆区大小和 -XX:G1MixedGCLiveThresholdPercent
参数设置的阈值,来决定哪些对象会直接进入老年代。XX:ThresholdTolerance
是动态调整的)来决定何时直接在老年代分配大对象。而是有虚拟机根据当前堆内存情况和历史数据动态决定。既然虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
大部分情况,对象都会首先在Eden区域分配。如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0或s1)中,并将对象年龄设为1(Eden区-》Survivor区后对象的初始年龄变为1)。
对象在Survivor 中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度(默认15),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold
来设置。
Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的50%时(默认值是50%,可以通过 -XX:TargetSurvivorRatio=percent来设置),取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。
默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6。
1、部分收集(Partial GC):只针对部分区域进行垃圾收集。其中又分为:
1.1、新生代收集(Minor GC/Young GC):只针对新生代的垃圾收集。具体点的是Eden区满时触发GC。Survivor满不会触发Minor GC 。
1.2、老年代收集(Major GC/Old GC):只针对 老年代的垃圾收集。
目前,只有CMS收集器会有单独收集老年代的行为。
注意,很多时候,Major GC 会和Full GC混淆使用,需要具体分辨是老年代的回收还是整堆回收。
1.3、混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。
目前只有G1收集器会有这种行为。
2、整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
是为了确保在Minor GC之前老年代本身还有容纳新生代所有对象的剩余空间。
规则 :只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
给对象中添加一个引用计数器:
每当有一个地方引用它,计数器就加1;
当引用失效,计数器就减1;
任何时候计数器为0的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之前循环引用的问题。
所谓对象之间的相互引用问题,除了对象objA
和 objB
相互引用着对方之外,这两个对象之前在任何引用。但是它们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知GC回收器回收它们。
这个算法的基本思想就是通过一系列的称为“GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。(从 GC Root 开始进行对象搜索,可以被搜索到的对象即为可达对象,不可达对象便可以作为垃圾被回收掉。)
哪些对象可以作为 GC Roots 呢?
虚拟机栈(栈帧中的局部变量表)中引用的对象
本地方法栈(Native方法)中的引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
JNI(Java Native Interface)引用的对象
对象可以被回收,就代表一定会被回收吗?
即使在可达性分析法不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。当对象没有覆盖finalize方法,或finalize方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用(引用强度逐渐减弱)
强引用:
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器角不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用:
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用:
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用:
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
特别注意,在程序设计中很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生。
运行时常量池主要回收的是废弃的常量。那么,如何判断一个常量是废弃常量呢?
JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代
JDK1.7字符串常量池从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代。
JDK1.8 hotspot 移除了永久代用元空间取而代之,这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变为元空间。
假如在字符串常量池中存在字符串“abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量“abc”就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc”就会被系统清理出常量池了。
方法区主要回收的是无用类,那么如何判断一个类是无用的类呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算“无用的类”:
ClassLoader
已经被回收。java.lang.Class
对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。虚拟机在满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
标记-清除(Mark-and-Sweep)算法分为 “标记” 和 “清除” 阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收所有没有被标记的对象。
它是最基础的手机算法,后续的算法都是对其不足进行改进得到。这种垃圾回收算法会带来明显的两个问题:
效率问题:标记和清除两个过程效率都不高。
空间问题:标记清除后会产生大量不连续的内存碎片。
整个标记-清除过程大致流程:
1.当一个对象被创建时,给一个标记为,假设为0(false);
2.在标记阶段,我们将所有可达对象(或用户可以引用的对象)的标记位设置为1(true);
3.扫描阶段清除的就是标记位0(false)的对象。
为了解决标记-清除算法的效率和内存锁片问题,复制(Copying)收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是堆内存区间的一半进行回收。
虽然改进了标记-清除算法,但依然存在下面这些问题:
标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思路,只是根据对象存活周期的不同将内存分为几块。一般将Java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择“标记-复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间堆它进行分配担保,所以我们必须选择“标记-清除” 或 “标记-整理”算法进行垃圾收集。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器的出现,更加没有万能的垃圾收集器,我们能做就是根据具体应用过场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同垃圾收集器了。
JDK默认垃圾收集器(使用java -XX:+PrintCommandLineFlags -version
命令查看):
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的“单线程”的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(“Stop The World”),直到它收集结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
新生代采用标记-复制算法,老年代采用标记-整理算法。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器)配合工作。
并行和并发概念:
Parallel Scavenge 收集器也是使用标记-复制算法
的多线程收集器,它看上去几乎和ParNew都一样。区别之处在于:
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用标记-复制算法,老年代采用标记-整理算法。
JDK1.8默认使用的是 Parallel Scavenge + Parallel Old,如果指定了 -XX:+UseParallelGC
参数,则默认指定了 -XX:+UseParallelOldGC
,可以使用 -XX:-UseParallelOldGC
来禁用该功能。
在此收集器下,默认是需要触发full GC之前先触发一次young GC,并且两次GC之前能让应用程序稍微运行一下,以降低full GC的暂停时间(因为young GC会尽可量的去清空young Gen的死对象,减少full GC的工作量)。控制这个行为的参数是:-XX:+ScavengeBeforeFullGC。
并发GC的触发条件不一样,以CMS GC为例,它主要是定时去检查old gen的使用量,但使用超过了触发比例就会启动一次CMS GC,对old gen做并发收集。
Serial 收集器的老年代版本 ,它同样是一个单线程收集器。主要两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge 收集器配合使用;另一种用途作为CMS 收集器的后备方案。
Parallel Scavenge 收集器的老年代版本 。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑Parallel Scavenge 收集器和 Parallel Old收集器。
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS收集器是HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS收集器是一种 “标记-清除”算法 实现的,它运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
主要优点:并发收集、低停顿。
缺点:
G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
被视为JDK1.7中HotSpot虚拟机的一个重要进化特征,他具备以下特点:
并行与并发:
G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World 停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
分代收集:
虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还保留了分代的概念。
空间整合:
与CMS的 “标记-清除”算法不同,G1从整体来看是基于 “标记-整理”算法实现的收集器;从局部上来看是基于 “标记-复制”算法实现的。
可预测的停顿:
这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1收集器的运作大致分为以下步骤:
初始标记 -》 并发标记 -》 最终标记 -》 筛选回收
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它名字Carbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
从JDK1.9开始,G1收集器成为了默认的垃圾收集器。
与CMS中的ParNew和G1类似,ZGC也采用了 标记-复制算法,不过ZGC对该算法做了重大改进。在ZGC中出现 Stop The World的情况会更少!
ZGC在JDK15正式使用,不过默认还是G1。可以通过以下参数设置ZGC:
$ java -XX:+UseZGC className