GC

GC 的内容挺多的,也是面试官爱问的点之一,所以单独拿出来,独立于JVM总结一下。

博客参考:https://blog.csdn.net/dc_726/article/details/7934101

       https://www.cnblogs.com/xiaoxi/p/6486852.html

 

关于GC,首先我们要搞清楚垃圾回收的范围(栈需要GC去回收吗?);然后就是回收的前提条件:如何判断一个对象已经可以被回收(重点学习搜索算法),之后便是建立在 搜索基础上的三种回收策略,最后便是JVM中对这三种策略的具体实现。

一、范围:要回收哪些区域?

java方法栈、本地方法栈以及PC计数器随方法或线程的结束而自然被回收,所以这些区域不需要考虑回收问题。java堆和方法区是GC回收的重点区域,因为一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一样,而这两个区域随时都会有对象不再被引用,因此这部分内存的分配和回收都是动态的。

 

二、前提:如何判断对象已死?

1)引用计数法:

这个算法的实现就是给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能被使用的。

这种算法使用的很多,但是Java中却没有使用,因为这种算法很难解决对象间互相引用的情况。比如对象A包含指向对象B的引用,对象B也包含指向对象A的引用,但没有引用指向A和B,如果使用的是引用计数法,那么对象A和B的引用次数都为1,都不会被回收。

2)可达性分析法:

这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(GC Roots到对象不可达)时,则证明此对象是不可用的。

那么问题来了,如何选取GC Roots对象呢?Java中,GC Roots包含以下几种:

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象;
  • 方法区中的类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(Native方法)引用的对象。

入下图:

四种引用状态:

1)强引用:Object obj = new Object();这类的引用,只有强引用还存在,垃圾回收器永远不会回收掉被引用的对象;

2)弱引用:描述非必须对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用;

3)软引用:描述有些还有用但并非必须的对象。在系统将要发生内存溢出之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java中的类SoftReference表示软引用;

4)虚引用:

这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java中的类PhantomReference表示虚引用。

入下图:

对于可达性分析算法而言,未达到的对象并非是“非死不可”的,若要宣判一个对象死亡,至少需要经历两次标记阶段。

1)如果对象在进行可达性分析之后发现没有与GC Roots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法已经被虚拟机执行过了,则均视作不必执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法且该finalize方法并没有被执行过,那么这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立,优先级低的finalize线程去执行,而虚拟机不必等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理;

2)对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GC Roots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除;如果对象还是没有拯救自己,那就会被回收。

 

方法区的垃圾回收

方法区的垃圾回收主要有两部分:1、废弃常量;2、无用的类。既然进行垃圾回收,就需要判断哪些是废弃常量,哪些是无用的类。

如何判断废弃常量呢?以字面量回收为例,如果一个字符串“abc”已经进入常量池,但当前系统没有任何一个String对象引用了叫做“abc”的字面量,那么,如果发生垃圾回收并且有必要的时候,“abc”就会被系统移除常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

如何判断无用的类呢?需要满足以下三个条件:

1)该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例;

2)加载该类的ClassLoader已经被回收;

3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

满足以上三个条件的类可以进行垃圾回收,但是并不是无用就被回收,虚拟机提供了一些参数供我们配置。

 

垃圾收集算法

1)标记-清除(Mark-Sweep)算法:

这是最基础的算法,标记-清除算法就如同它的名字一样,分为 标记 和 清除 两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。这种算法的不足主要体现在效率和空间,从效率上来讲,标记和清除两个过程效率都不算高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

2)复制(Copping)算法:

复制算法是为了解决效率问题出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块的上面,然后再把已经使用过的内存空间一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。

不过这种算法有个缺点,内存缩小为原来的一半,这样代价太高了。现在商用虚拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,即每次新生代中可用内存空间为新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保。

3)标记-整理(Mark-Compact)方法:

复制算法在对象存活率较高的场景下要进行大量的复制操作,效率很低。万一对象100%存活,那么需要有额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,因此一般不能直接选用复制算法。根据老年代的特点,有人提出了另外一种标记-整理算法,过程与标记-清除算法 一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。

4)分代收集算法:

我们知道,内存的布局如下图:

现代商业虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的,无非就是上面内容的结合罢了,根据对象生命周期的不同将内存划分为几块,然后根据各块的特点采用最适合的收集算法。大批对象死去,少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高,没有额外空间进行分配担保的(老年代),采用标记清理算法或标记-整理算法。

 

垃圾收集器

垃圾收集器就是上面讲的理论知识具体实现了。不同虚拟机所提供的垃圾收集器可能会有很大的差别,我们使用的是HotSpot,HotSpot这个虚拟机所包含的所有收集器如图:

上图展示了7种作用于不同代的收集器,如果两个收集器之间存在连线,那说明它们可以搭配使用。虚拟机所处的区域说明它是属于新生代收集器还是老年代收集器。

1)Serial收集器:

最基本、发展历史最久的收集器,这个收集器是一个采用复制算法的单线程收集器,单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作,直达它收集结束为止。后者意味着,在用户不可见的情况下,要把用户正常工作的线程全部停掉,这对很多应用是难以接受的。不过目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,因为它简单高效。用户桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆或者一两百兆的新生代停顿时间在几十毫秒最多一百毫秒,只要不是频繁发生,这点停顿完全是可以接受的。

说明:

  • 需要STW(stop the world),停顿时间长;
  • 简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。

2)ParNew收集器:

ParNew收集器其实就是Serial收集器的多线程版本。除了使用多线程进行垃圾收集外,其余行为和Serial收集器完全一样,包括使用的也是复制算法。ParNew收集器除了多线程外和Serial收集器并没有太大区别,但是它确实Server模式下虚拟机首选的新生代收集器,其中一个很重要的和性能无关的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合使用。CMS收集器是一款几乎可以认为具有划时代意义的垃圾收集器,因为它第一次实现了让垃圾收集线程与用户线程基本上同时工作。ParNew在单CPU的环境下绝对不会有比Serial收集器更好的效果,甚至由于线程交互的开销,该收集器在两个CPU的环境中都不能百分百保证可以超越Serial收集器。当然,随着可用CPU数量的增加,它对于GC时系统资源的有效利用还是很有好处的。ParNew默认开启的收集线程数与CPU数量相同,在CPU数量非常多的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

3)Parallel Scavenge收集器:

Parallel Scavenge收集器也是一个新生代收集器,也是使用复制算法,并行的多线程收集器。但是它的特点是它的关注点和其他收集器不同。介绍这个收集器之前我们先了解下 吞吐量的概念。CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量的意思就是CPU用于运行用户代码时间与CPU总消耗时间的比值,即 吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间)。例如,虚拟机总运行100分钟,垃圾收集1分钟,那吞吐量就是99%。另外,Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。

停顿时间短适合需要与用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以高效率利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多的交互任务。

虚拟机提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio两个参数来精确控制最大垃圾收集停顿时间和吞吐量的大小。不过不要以为前者越小越好,GC停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。由于与吞吐量关系密切,Parallel Scavenge收集器也被称为“吞吐量优先收集器”。Parallel Scavenge收集器有一个-XX:+UseAdaptiveSizePolicy参数,这是一个开关参数,这个参数打开之后,就不需要手动指定新生代大小、Eden区和Survivor区等细节参数了,虚拟机会根据当前系统的运行情况,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。如果对于垃圾收集器运作原理不太了解,以至于在优化比较困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成是个不错的选择。

4)Serial Old收集器:

Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法,这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

5)Parallel Old收集器:

Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK1.6之后出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注意吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器+Parallel Old 收集器的组合。

6)CMS收集器:

CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用“标记-清除”算法。步骤如下:

  • 初始标记,标记GC Roots能直接关联到的对象,时间很短;
  • 并发标记,进行 GC Roots Tracing(可达性分析)过程,时间很长;
  • 重新标记,修改并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长;
  • 并发清除,回收内存空间,时间较长。

其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。

说明:

  • 对CPU资源非常敏感,可能会导致应用程序变慢,吞吐量下降;
  • 无法处理浮动垃圾,因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集它们,只能留到下次收集,这部分垃圾称为浮动垃圾,同时由于用户线程并发运行,所以需要预留一部分老年代空间提供并发收集时程序运行使用;
  • 由于采用的 “标记-清除” 算法,会产生大量的内存碎片,不利于大对象的分配,可能会提前触发一次 Full GC。虚拟机提供了-XX:+UseCMSCompactAtFullCollection参数来进行碎片的合并整理过程,这样会使得停顿时间变长;虚拟机还提供了一个参数配置,-XX:+CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,接着来一次带压缩的GC。

7)GI收集器:

GI是目前技术发展的最前沿成果之一,HotSpot团队赋予它的使命是未来可替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,GI收集器有以下特点:

1)并行和并发。使用多个CPU来缩短stop the world 停顿时间,与用户线程并发执行;

2)分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果;

3)空间整合。基于 “标记-整理”算法,无内存碎片的产生;

4)可预测的停顿。能建立可预测的时间停顿模型,能让使用者明确指定在一个长度在M毫秒的时间片段内,消耗在垃圾收集器上的时间不能超过N毫秒。

在GI之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而GI不是这样。使用GI收集器时,Java堆得内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域,虽然还保留着新生代和老年代的概念,但新生代和老年代不再与物理隔离的了,它们是一部分独立区域的集合。

 

何时触发GC?

Minor GC(新生代回收)的触发条件比较简单,Eden空间不足就开始进行Minor GC回收新生代。

而Full GC(老年代回收,一般伴随一次Minor GC)则有几种触发条件:

1)老年代空间不足;

2)PermSpace空间不足;

3)统计得到的Minor GC 晋升到老年代的平均大小大于老年代的剩余空间。

需要注意一点的是:PermSpace并不等同于方法区,只不过是HotSpot JVM用PermSpace来实现方法区而已,有些虚拟机没有PermSize而使用其他机制来实现方法区。

 

对象的空间分配和晋升

1)对象优先在Eden上分配;

2)大对象直接进入老年代;

虚拟机提供了-XX:PretenureSizeThreshold参数,大于这个参数值的对象将直接分配到老年代中。因为新生代采用的是 “标记-复制”策略,在Eden中分配大对象将会导致Eden区和两个Survivor区之间大量的内存拷贝。

3)长期存活的对象将进入老年代。

对象在Survivor区中没熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会晋升到老年代中。

 

https://blog.csdn.net/pfnie/article/details/52819427

https://blog.csdn.net/xybelieve1990/article/details/54891660

https://blog.csdn.net/liu765023051/article/details/75127361

 

posted @ 2019-07-23 18:15  一枚路过的小码农  阅读(681)  评论(0编辑  收藏  举报