文章目录
- 1. Java的垃圾回收机制?
-
- 1.1 Java的引用类型
- 1.2 哪些内存需要回收?
- 1.3 怎么定义垃圾?
- 1.4 怎么回收垃圾?
- 2. JVM一次完整的GC流程?
- 3.触发GC之后,会立刻进行GC吗?
- 4. Full GC会导致什么?
- 5. 什么时候触发GC,如何减少Full GC的次数?
- 6. 对象如何晋升到老年代?
- 7. 为什么老年代不能使用标记复制?
- 8. 为什么要设置两个Survivor区域?
- 9. 经典的垃圾回收器
-
- 9.1 Serial收集器
- 9.2 ParNew收集器
- 9.3 Parallel Scavenge收集器
- 9.4 Serial Old收集器
- 9.5 Parallel Old收集器
- 9.6 G1垃圾收集器
- 9.7 CMS垃圾回收器
1. Java的垃圾回收机制?
1.1 Java的引用类型
- 强引用:在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
- 软引用:软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
- 弱引用:弱引用需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。
- 虚引用:虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
1.2 哪些内存需要回收?
主要关注堆和方法区。
1.3 怎么定义垃圾?
- 引用计数法:在Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
- 可达性分析
- 如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。
- 可作为GC Roots的对象:
- 虚拟栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- JVM内部的引用
- 所有被同步锁(Synchronized关键字)持有的对象
- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
- 回收方法区:
- 主要回收废弃的常量和不再使用的类型
- 废弃的常量:没有对象引用此常量
- 不再使用的类型:
- 该类所有的实例都已被回收(包括子类的实例)
- 加载该类的类加载器已被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 主要回收废弃的常量和不再使用的类型
1.4 怎么回收垃圾?
- 分代收集理论——经验法则
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象越难消亡
基于以上两点,收集器应该将Java堆划分出不同的区域,然后将回收对象依据年龄等分配到不同的区域中存储。但是可能会有跨代引用,于是就有了
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
只需在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代划分为若干小块,标识出老年代的哪一块内存会存在跨代引用。当发生MinorGC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
- 标记-清除——效率不稳定、空间碎片化。最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
- 标记-复制——为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。新生代MinorGC使用这个。
- 标记-整理——结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。老年代FullGC使用这个。
2. JVM一次完整的GC流程?
- 1)新创建的对象会被分配在新生代中。
- 新生代按照8:1:1将新生代划分成Eden区、两个Survivor区。
- 当Eden满之后,Minor GC就触发了。
- 2)在Minor GC前,JVM先比较新生代中对象和老年代剩余空间的大小:
- 如果老年代剩余空间 > 新生代中对象的大小:直接GC。
- 如果老年代剩余空间 < 新生代中对象的大小:就要看“老年代空间分配担保规则”:
- 老年代剩余空间 > 历次MinorGC后剩余对象的大小,进行Minor GC;
- 老年代剩余空间 < 历次MinorGC后剩余对象的大小,进行Full GC,把老年代空出来再检查。
- 3)开启老年代空间担保分配规则只能说是大概率上来说,Minor GC剩余的对象能够放到老年代,但也有可能出现意外:
Minor GC之后的对象:
- 足够放到Survivor区,GC结束;
- 不够放到Survivor区,接着进入老年代,
- 如果老年代能放下,GC结束。
- 老年代放不下,只能触发Full GC。
- 4)3种OOM:
- Full GC之后,老年代仍然放不下剩余对象;
- 未开启老年代分配担保机制,且第一次Full GC后,老年代仍放不下剩余对象;
- 开启老年代分配担保,但是担保不通过,一次Full GC后,老年代仍放不下剩余对象。
3.触发GC之后,会立刻进行GC吗?
不会。
虚拟机为“会长时间执行”的程序设置了安全点,这就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点之后才能够暂停。
如何在垃圾回收时让所有线程都跑到最近的安全点然后停顿下来?
- 抢占式中断(几乎不会用)
- 主动式中断:而主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。
但是这种情况下,程序“不执行”的时候呢?
所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
4. Full GC会导致什么?
会“Stop The World”,即在GC期间全程暂停用户的应用程序。
5. 什么时候触发GC,如何减少Full GC的次数?
- 触发GC
- 新生代的Eden区的空间耗尽时,触发Minor GC
- Minor GC前,老年代内存剩余 < 年轻代晋升老年代的对象大小,触发Full GC;
- PS:在CMS等并发收集器中,每隔一段时间检查一下老年代内存的使用量,超过一定比例则进行Full GC回收。
- 减少Full GC次数的措施
- 增加方法区的空间
- 增加老年代的空间
- 减少新生代的空间
- 禁止使用System.gc()方法
- 使用标记-整理算法,尽量保持较大的连续内存空间
- 排查代码中无用的大对象
6. 对象如何晋升到老年代?
- 活过Eden,进入Surivor并设置1岁,超龄变老年
虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头中。在Eden区中,如果经过第一次MinorGC后仍然能够存活,该对象会被移动到Survivor空间中,并且将其年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(15),就会晋升到老年代。
7. 为什么老年代不能使用标记复制?
因为老年代保留的对象都是难以消亡的,而标记复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。
8. 为什么要设置两个Survivor区域?
解决内存碎片化。
9. 经典的垃圾回收器
9.1 Serial收集器
- 新生代;单线程;复制算法。
它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
- 优点:简单。对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
- 缺点:单线程,stop the world并且时间长。
- 场景:对于运行在客户端模式下的虚拟机来说是一个很好的选择。
9.2 ParNew收集器
- 新生代;多线程;复制算法。
实质上是Serial的多线程并行版本。除了同时使用多个线程进行垃圾回收之外,其余的行为与实现都与Serial收集器一样。
- 默认开启的收集线程数与处理器核心数量相同。
9.3 Parallel Scavenge收集器
- 新生代;多线程;标记-复制。
- 目标是达到一个可控制的吞吐量,所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
- 高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
9.4 Serial Old收集器
- 老年代;单线程;标记-整理。
- 这个收集器的主要意义是供客户端模式下的HotSpot虚拟机使用。
- 如果在服务端模式下,它也可能有两种用途:
- 一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用;
- 另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
9.5 Parallel Old收集器
- 老年代;多线程;标记-整理。
9.6 G1垃圾收集器
G1仍遵循分代收集理论,但G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域,每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间、老年代空间。收集器能够对扮演不饿哦那个角色的Region采用不同的策略区处理。
(Region还有一类特殊的Humongous区域,专门来存放大对象(超过Region的一半)。对于超过整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来看待。)
G1收集器建立了可预测的停顿时间模型,每次回收的空间都是Region大小的整数倍,这样可以避免在整个Java堆中进行全区域的垃圾回收。
G1收集器的运作过程:
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运作时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,并且是借助运行Minor GC的时候同步完成的,所以G1收集器在这个阶段并没有额外的停顿。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后的那少量的SATB记录。
- 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来指定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作设计存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
CSET :G1收集器出现之前的收集器,垃圾回收的目标范围要么是整个新生代(Minor GC) 或 老年代(Major GC)、要么是整个Java堆(Full GC)。而G1跳出了这个限制,它可以面向堆内存任何部分来组成**回收集(CSet)**进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。这就是G1收集器的Mixed GC模式。
解决跨Region引用的对象:记忆集。
9.7 CMS垃圾回收器
- CMS收集器是一种以获取最短回收停顿时间为目标的收集器。
- 标记-清除
CMS的运作过程分为四个步骤:
- 1)初始标记
- 仅仅标记一下GC Roots能直接关联到的对象,速度很快。会“Stop The World”。
- 2)并发标记
- 从GCRoots的直接关联开始遍历整个对象图。比较耗时。但是不用停顿其他用户线程,可以与GC线程并发运行。
- 3)重新标记
- 为了修正并发标记期间,因用户程序继续运作而导致标记产A生变动的那一部分对象的标记记录。会“Stop The World”。
- 耗时比初始标记长,但比并发标记短
- 4)并发清除
- 清理删除标记阶段判断的已经死亡的对象。
- 并发进行,不需要移动存活对象。
- 缺点:
- 并发阶段,占用线程,降低总吞吐量
- 无法处理“浮动垃圾”,有可能导致另一次完全“Stop The World”的Full GC产生。——解决:留待下一次垃圾收集时再清理掉。
- 产生大量碎片,不得不触发Full GC——移动整理来消除碎片。
- PS:浮动垃圾:在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/183856.html原文链接:https://javaforall.cn