写在前
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
垃圾回收机制思维导图
实际上,Java技术体系中所提倡的 自动内存管理 最终可以归结为自动化地解决了两个问题:给对象分配内存 以及回收分配给对象的内存,而且这两个问题针对的内存区域就是Java内存模型中的堆区。
我们知道垃圾回收机制是Java语言一个显著的特点,其可以有效的防止内存泄露、保证内存的有效使用(Ps: 内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度),从而使得Java程序员在编写程序的时候不再需要考虑内存管理问题。Java 垃圾回收机制要考虑的问题很复杂,这里主要阐述其三个核心问题,包括:哪些内存需要回收?什么时候回收?如何回收?
在探讨Java垃圾回收机制之前,我们首先应该记住一个单词:Stop-the-World。Stop-the-world意味着 JVM由于要执行GC而停止了应用程序的执行,并且这种情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有 高吞吐 、低停顿 的特点。
1、内存如何分配和回收的
(1)Java内存分配模型
Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 堆 内存中对象的分配与回收。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
堆空间的基本结构:
(2)Java堆内存分配策略
堆内存常见的分配策略
- 在堆中,如果待分配的对象所需内存大于eden区大小,那么将直接送入老年代。
- 如果eden区可以放下,会经历一下的分配过程:
- 对象都会首先在 Eden 区域分配
- 第一次垃圾回收,将Eden存活对象放入From区,清空Eden区
- 第二次垃圾回收,将Eden和From存活对象放到To区,清空Eden和From区,ps:如果上次From区对象还存活,将对象的年龄还会加 1(初始为0)当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
- 经过这次GC后,交换From和To区,保证To区始终是空的。
(3)新生代内存中,为什么要有Survivor区域
- 如果没有Survivor,Eden区每进行一次Minor GC(发生在新生代的垃圾回收),存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。
- 从上面可以看出来,Survivor区域带来的最大的优势就是防止老年代被很快填满,从而增大老年代垃圾回收时间上的浪费
好,那我们来想想在没有Survivor的情况下,有没有什么解决办法,可以避免上述情况:
- 增加老年代空间 ,更多存活对象才能填满老年代。降低Full GC频率 随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长
- 减少老年代空间 Full GC所需时间减少 老年代很快被存活对象填满,Full GC频率增加。显而易见,没有Survivor的话,上述两种解决方案都不能从根本上解决问题。
(4)为什么要设置两个Survivor区
这个问题也就是复制算法的原理,堆中新生代采用的就是复制算法,下面来看一下它的魅力:
设置两个Survivor区最大的好处就是解决了内存碎片化,我们来分析一下:
1)首先使用单个Survivor区
- 刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
- 碎片化带来的风险是极大的,严重影响JAVA程序的性能。堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存,就会造成很可怕的结果。下面用图简单说明一下只有一个Survivor会出现什么样的情况
- 第一次GC发生之前,Survivor区为空, GC过后, Eden清空, Survivor填上
- 第二次GC的时候,Survivor区域中有部分被标记清除,Eden又加入了一些新的元素, 那么继续清空如下,可以看到此轮GC过后,Survivor区因为此轮也清楚了两个红点,所以产生了两个碎片,而Eden区新加入的四个绿点紧跟着之前的Survivor加入
- 第三次GC的时候,同样的Eden区域和Survivor区域都产生了要回收的对象,看看这会出现了什么样的情况
经过上面的GC,可以看到最终再Survivor区出现了大量的碎片,那么解决这个问题的最好的方式就是使用两个Survivor区。
2)使用两个Survivor区
咱们再来看看,用两个Survivor会出现什么样的情况。
- 首先第一次GC,将Eden区红色全部干掉,绿色(存活的对象)全部扔到from里面去。
- 这里简单看一下第二次GC是怎么进行操作的,首先Eden区和from区这个时候又出现了许多红点(待清理的对象),这次JVM的操作就不是直接再Survivor区域上将对象干掉,这次他会收先将from区域里面的红点全部干掉,然后剩余的绿点顺位进入to区域,eden区域同样按这个原理清空, 放入to区域之后,再将from区域和to区域交换,始终保证to区域是空闲的状态,这样就可以非常完美的解决碎片化问题。
2.哪些内存需要回收?
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。
(1)怎么判断对象已死亡
判断对象是否已经死亡通常有两个方法,引用计数法和可达性分析算法。
1)引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。
2)可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
可达性算法
ps:a, b 对象可回收,就一定会被回收吗?
并不是,对象的 finalize 方法给了对象一次垂死挣扎的机会,当对象不可达(可回收)时,当发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法,我们可以在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!
注意: finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收!这一点切记!
ps:哪些对象可以作为 GC Root 呢?
便于记忆,称他为两栈两方法(引用的对象)!
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
- 方法区中类静态属性引用的对象(如类中使用的static声明的引用类型字段)
- 方法区中常量引用的对象(如类中使用final声明的引用类型字段)
(2)四种引用是怎么进行垃圾回收的
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
1)强引用(StrongReference)
有强引用的对象,垃圾回收器绝不会回收它,当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2)软引用(SoftReference)
如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。常见应用场景:缓存
3)弱引用(WeakReference)
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
4)虚引用(PhantomReference)
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。你声明虚引用的时候是要传入一个queue的。当你的虚引用所引用的对象已经执行完finalize函数的时候,就会把对象加到queue里面。你可以通过判断queue里面是不是有对象来判断你的对象是不是要被回收了【这是重点,让你知道你的对象什么时候会被回收。因为对普通的对象,gc要回收它的,你是知道它什么时候会被回收】。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
(3)JVM的方法区可以实现垃圾回收吗?
方法区的垃圾回收主要有两种,分别是对废弃常量的回收和对无用类的回收。
- 当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收。假如在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。
类需要同时满足下面 3 个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
3.什么时候回收?
(1)分代垃圾回收器工作流程
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
- 清空 Eden 和 From Survivor 分区;
- From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回(full gc),一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
(2)Minor GC和Major GC内存回收策略
多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
ps:分配担保机制,在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。当大量对象在Minor GC后仍然存活,就需要老年代进行空间分配担保,把Survivor无法容纳的对象直接进入老年代。如果老年代判断到剩余空间不足(根据以往每一次回收晋升到老年代对象空间的平均值作为经验值),则进行一次Full GC。
这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。
- Minor GC:对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
- Full GC:也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。
ps:永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。
(3)可以主动通知虚拟机进行垃圾回收吗
可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
4.如何回收(回收策略)?
(1)垃圾收集算法
1)标记清除算法
该算法分为“标记”和“清除”阶段:首先比较出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
标记清除算法
2)复制算法(效率优化,不会产生内存碎片)
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。缺点:降低了内存利用率。
复制算法
3)标记整理算法(不会产生内存碎片)
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。(存活对象较多,不会产生内存碎片)
标记整理算法
4)分代收集算法
虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
- 在新生代使用复制算法
- 在老年代使用标记整理算法
(2)垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。垃圾回收器的工作流程大体如下:
- 标记出哪些对象是存活的,哪些是垃圾(可回收);
- 进行回收(清除/复制/整理),如果有移动过对象(复制/整理),还需要更新引用。
下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
垃圾回收器
注意:jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代),但是一般我们都不会使用,但是在jdk1.9 默认垃圾收集器G1。
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
Serial收集器
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;ps:也需要stop the world
ParNew收集器
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
Parallel Scavenge 收集器
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,它非常符合在注重用户体验的应用上使用,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
因为CMS和G1收集器相对比较特殊,下面单独介绍一下他们
(3)CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间【也就是指Stop The World的停顿时间】为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,只是标记一下GC Roots能直接关联到的对象,速度很快;
- 并发标记: 同时开启 GC 和用户线程(不会阻碍业务线程继续执行),就是从GC Roots开始找到它能引用的所有对象的过程。原因:不能保证当前所有对象可达,因为用户线程可能会不断的更新引用域,所以GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
ps:初始标记、重新标记这两个步骤仍然需要“Stop The World”。
CMS 垃圾收集器
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿(停止用户线程)。但是它有下面三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;ps:就是指在之前判断该对象不是垃圾,由于用户线程同时也是在运行过程中的,所以会导致判断不准确的, 可能在判断完成之后在清除之前这个对像已经变成了垃圾对象,所以有可能本该此垃圾被回收但是没有被回收,只能等待下一次GC再将该对象回收,所以这种对像就是浮动垃圾)
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前进行一次Full GC。
(4)G1收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 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 毫秒的时间片段内。
G1 收集器的运作大致分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
CMS与G1区别
- CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
- CMS收集器以最小的停顿时间为目标的收集器;G1收集器在最小停顿基础上可预测垃圾回收的停顿时间;
- CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片;G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
拓展:标记阶段算法 - 三色标记法
CMS和G1都是使用三色标记法,简单说能标记的都是可用的,未标记的都是垃圾(可回收)。三色标记法:
- 白色:未标记
- 黑色:本对象已经访问完了,本对象所引用的对象也都访问完了(都不是垃圾)
- 灰色:本对象已经访问完了,本对象所引用的对象还有没有全部访问完。
注意:在STW期间,对象间的引用不会发生变化,但是并发标记阶段(应用程序也在跑),引用就可能改变、多标或漏标都可能出现。
(1)多标 - 浮动垃圾
假设已经遍历到 E(变为灰色了),此时应用执行了 objD.fieldE = null (D > E 的引用断开):
- 此刻之后,对象 E/F/G 是“应该”被回收的。然而因为 E 已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮 GC 不会回收这部分内存。
- 这部分本应该回收 但是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除。
- 另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。
(2)漏标 - 内存屏障
假设 GC 线程已经遍历到 E(变为灰色了),此时应用线程先执行了:var G = objE.fieldG; objE.fieldG = null; // 灰色E 断开引用 白色G objD.fieldG = G; // 黑色D 引用 白色G
- 此时切回 GC 线程继续跑,因为 E 已经没有对 G 的引用了,所以不会将 G 放到灰色集合;尽管因为 D 重新引用了 G,但因为 D 已经是黑色了,不会再重新做遍历处理。
- 最终导致的结果是:G 会一直停留在白色集合中,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。
不难分析,漏标只有同时满足以下两个条件时才会发生:
- 灰色对象断开了白色对象的引用(直接或间接的引用);即灰色对象原来成员变量的引用发生了变化。
- 黑色对象重新引用了该白色对象;即黑色对象成员变量增加了新的引用。
4.拓展问题(总结)
(1)GC是什么时候触发的
由于对象进行了分代处理(便于内存分配与回收),因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GC和Full GC。
- Minor GC:一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
- Full GC:对整个堆进行整理,包括Young、Tenured(老年代)和Perm(永久代)。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。
(2)JVM常见参数
- 堆栈配置相关
-Xmx3550m: 最大堆大小为3550m。 -Xms3550m: 设置初始堆大小为3550m。 -Xmn2g: 设置年轻代大小为2g。 -Xss128k: 每个线程的堆栈大小为128k。 -XX:MaxPermSize: 设置持久代大小为16m -XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。 -XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6 -XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。默认15
- 垃圾收集器相关
-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。 -XX:ParallelGCThreads=20: 配置并行收集器的线程数 -XX:+UseConcMarkSweepGC: 设置年老代为并发收集。 -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
- 辅助信息相关
-XX:+PrintGC 输出形式: [GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs] -XX:+PrintGCDetails 输出形式: [GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs
(3)频繁full gc排查方案
对于 Full GC 较多的情况,其主要有如下两个特征:
- 线上多个线程的 CPU 都超过了 100%,通过 jstack 命令可以看到这些线程主要是垃圾回收线程。
- 通过 jstat 命令监控 GC 情况,可以看到 Full GC 次数非常多,并且次数在不断增加。
首先我们可以使用 top 命令查看系统 CPU 的占用情况。找到占用cpu多的进程id, 查看该进程的各个线程运行情况。接下来我们可以通过 jstack 命令查看线程id,那个线程为什么耗费 CPU 最高。如果当前系统缓慢的原因主要是垃圾回收过于频繁,导致内存溢出。那么接着查看你是哪些对象导致的内存溢出的,这个可以 Dump 出内存日志,然后通过 Eclipse 的 Mat 工具进行查看。经过 Mat 工具分析之后,我们基本上就能确定内存中主要是哪个对象比较消耗内存,然后找到该对象的创建位置,进行处理即可。
(4)jvm调优基本思路
- 监控GC的状态。使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。
- 生成堆的dump文件。通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。
- 分析dump文件。建议使用Eclipse专门的静态内存分析工具Mat打开分析。
- 分析结果,判断是否需要优化。如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。
(5)故障处理工具
- jstat:虚拟机统计信息监视工具。用于监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据,在没有 GUI 界面的服务器上是运行期定位虚拟机性能问题的常用工具。
- jmap:Java 内存映像工具。用于生成堆转储快照,还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率,当前使用的是哪种收集器等。
- jstack:Java 堆栈跟踪工具。用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。
(6)JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代
- Java堆 = 老年代 + 新生代(默认1 : 2),可以通过参数 –XX:NewRatio 配置。新生代 = Eden + S0 + S1(8 : 1 : 1),以通过参数 –XX:SurvivorRatio 来设定。
- 当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor区。
- 大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态;
- 如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。(对应虚拟机参数 -XX:+MaxTenuringThreshold设定)
- 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。
- Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。
(7)如果老年代对象引用年轻代对象,年轻代对象是否会被垃圾回收?
(8)逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。
编译器会对代码进行优化:
- 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
- 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
5.参考资料
https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/jvm/Java
https://blog.csdn.net/javazejian/article/details/72828483)
https://www.jianshu.com/p/e74fe532e35e
https://mp.weixin.qq.com/s/6Laqv1ryS_EobJNOvKcSCA
https://blog.csdn.net/weixin_42510600/article/details/114964301