java 中的gc问题

前几天服务器出现了一些问题,然后组内同学就这些问题写过一篇文章,下面是顺着那篇文章,接着写一些内容。

一:GC算法的类型

GC也就是垃圾回收,和我们日常中的垃圾回收一样,语言中的垃圾回收也是表示对已经不再使用的对象进行清理,获得更多的内存空间;日常生活中也是一样的,比如去餐厅吃饭,每个人都要用到碗筷进行回收,一直都用之前的话,肯定是会用完的,那这个碗筷回收的算法时怎么样的呢?对于餐厅来说最好的方式是,在每个人吃完之后,自己将碗筷放到垃圾回收的地方,但是这样有两个问题,一个是餐厅就餐人的素质要比较高,并且比较自觉,另外是针对就餐完后的一些其他剩余(比如吐过的鱼刺等)还是要进行清理。然后餐厅出台一个政策就是吃完之后,不用就餐人自己清理,餐厅来自动清理。餐厅自己清理的策略就有以下几种

1、引用计数法(reference counting)

餐厅会记录每幅碗筷的使用情况,比如在使用的时候,会记录为1,如果父亲的筷子,儿子也要用,那么就记录为2,以此类推,如果我用完了,我需要自觉的将这个计数值减一,如果儿子也用完了,也会减一,有一个专门的机器人,会来看这个数字,当看到这个数字为0时,会自动的将这幅碗筷给清理。

这个算法比较简单,但是问题也比较突出,一是必须有一个专门的地方来记录这个数字,并且要不停的检查这个数字,增加了餐厅的负担,另一个是不能解循环引用的问题,三个对象之间互相引用,此时引用计数都是1,都不会为0,因此永远不会被回收,影响使用效率。

2、标记-清除算法(mark-sweep)

这个是第一种实用的垃圾回收算法,J. McCarthy 等人在 1960 年提出并成功地应用于 Lisp 语言的。仍然以餐厅清理碗筷为例,当回收机器人想要回收的时候,会大喝一声,大家停止用餐,机器人开始工作,会走到每一个就餐的人旁边,问:你是吃饭的么?你用的是那副碗筷?用到的碗筷会被做标记,说明正在被使用。餐厅全部问完之后,回收机器人会做第二部,回收掉那些没有做过标记的碗筷。

从名字就可以看出,标记-清除算法分为两个步骤,首先进行标记,这个过程是暂停运行(stop-the-world)的,然后标记完成后,再进行清除工作,和引用计数不同的是,不需要运行环境监控每一次内存的分配和指针的操作,仅仅是在标记的时候,记录每一个指针对应的引用。并且可以完美的解决循环引用问题,因此在lisp中得到的广泛的引用,后面的很多算法,都有这个算法的影子。

3、复制算法(copying)

标记-清除算法虽然比引用计数方法好,但是有一个很严重的问题,就是效率问题,标记和清除两个步骤都是耗时操作。特别是标记操作是在暂停引用的情况询问每一个对象,这个耗时就特别的引人注目。为了解决这个效率的缺陷,M. L. Minsky 于 1963 年发表了著名的论文“一种使用双存储区的 Lisp 语言垃圾收集器( A LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。 M. L. Minsky 在该论文中描述的算法被人们称为复制算法。

这个算法的特别之处是将内存分为两块,通过简单的复制操作,就能完成垃圾回收工作,不能不说这个想法非常奇妙。以餐厅回收碗筷为例,此时餐厅老板为了提高效率,想了一个高明的办法,将餐厅就餐区域分为了两个相同的区块(先不要考虑浪费问题),同一时刻只能在一个区块就餐,比如A,B两个区块,开始时,大家现在A区块就餐,吃完后的碗筷也会遗留在A区块,在某一个时刻(A区块位置快被占满了),回收机器人大喝一声,大家都统一到B区块就餐,然后就餐的人群就带着自己的东西去了B区块,并且可以要求座位的间距也可以非常紧凑,最重要的是,当大家都到B区块就餐之后,垃圾回收机器人,就会在A区块进行工作了,这个时候,会将所有还残留在A区块的碗筷全部进行回收。然后机器人只要等到B区块快被用完之后,再要求大家去A区块就餐就好了,垃圾机器人的工作就会很简单。

这个算法大大提高了垃圾回收的效率,并且也将之前复杂的内存分配算法进行了简化,因为既然内存回收是对整个半区进行回收,那么在内存分配的时候,就不用考虑内存碎片的问题,直接顺序分配内存就可以了,因为在进行垃圾收集的时候,可以自动将内存进行对齐的,真是一个神奇,但是这个神奇也是有代价的,就是一整块内存在同一时刻使用的只能是半块,这在当时计算价格还是以kb为单位的时候,不得不说是一个极大的浪费。

需要指出的是,上面三个算法,在1960年代就被发明了出来,后面的研究者主要是针对三个算法的优缺点进行改进来满足未来的要求

4、标记-整理算法(mark-compact)

标记-整理算法是标记-清除算法和复制算法的一个结合,将以上两种算法的优点进行结合,创造一种既不会产生内存碎片,又不会占用太多内存的算法肯定是一种方向。在1970年后,这个算法逐渐清晰

这个算法的思想很简单,在第一阶段,会手下进行标记,然后在标记完成后,不是进行清理,而是让存活对象向一端进行移动,在移动的过程中,清理掉可回收的对象,这个过程就是清理,一方面可以回收掉不再使用的对象,另一方面也可以避免内存碎片,让内存更加紧凑。机器人首先标记所有正在被使用的碗筷,然后让人拿着那些碗筷向南边移动,在移动的过程中,如果有一个位置上的碗筷没有被标记,则直接回收掉,然后让人占住这个位置。

5、分代收集算法

餐厅老板开了几年之后,发现用餐的一个规律,就是大部分用餐都非常快,并且在用餐完后,会很快的离开位置,只有少部分人或者团体会在用完餐后,继续在餐厅内(此时不能收掉别人的碗筷),于是餐厅老板就想到一个更好的方式来进行收集碗筷,他将餐厅分为了好几个区域,有快速用餐区A,B 2个,有慢速用餐区1个,如果有人就餐就先到快速用餐区A用餐,如果快速用餐区A快被用完了,就让大家移动到B用餐区进行用餐,大部分情况下,经过这一次移动,就会剩余很多空间,如果有人经过了很多次(比如8次)移动,还在就餐,就把那个人移动到慢速用餐区就餐;当然如果在就餐时,就向老板说没 用餐的速度就是慢,也会被放到慢速用餐区。

分代收集算法,其实不能算为一种算法,仅仅是根据对象的存活时间,来使用不同的收集算法,分代收集算法是使用上面除了引用计数算法其他3种算法的一种中和而已

java的虚拟机中一般把堆内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。

新生代一般采用复制和标记-清理算法,新生代分为Eden区,Survivor from和Survivor to三部分,新生代内存容量默认比例分别为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。java对新生代的垃圾收集称为minor gc,次数比较多,时间比较短

年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。java对年老代的垃圾收集称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。

二:GC收集器

GC在有了以上算法的基础上,还有7种垃圾收集器,分别如下:

1、Serial(串行)收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。

2、ParNew(并行)收集器

ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。

3、Parallel Scavenge(并行回收GC)收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

4、Serial Old(串行GC)收集器

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

5、Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

6、CMS(并发GC)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,主要是处理年老代的垃圾回收,减少应用的暂停时间,减少full gc发生的频率,整个收集过程大致分为4个步骤:

①.初始标记(CMS initial mark)
          ②.并发标记(CMS concurrenr mark)
          ③.重新标记(CMS remark)
          ④.并发清除(CMS concurrent sweep)

从上面可以看出,CMS并非没有暂停,而是用两次较短的暂停标记来代替串行标记引起的长暂停,第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记之后,暂停所有应用程序线程,重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)。第一次暂停会比较短,第二次暂停通常会比较长,并且remark这个阶段可以并行标记。

而并发标记,并发清除中的并发,是指一个或者多个垃圾收集线程和应用线程并发的执行,不会暂停引用线程的运行。CMS的年轻代,仍然使用ParNew来进行收集。CMS的主要优点是和应用线程并发执行收集,并且做到低停顿,但是还是要注意以下问题

1、在并发执行阶段,虽然不会导致应用线程的暂停,但是会占用CPU资源,有可能导致程序运行缓慢

2、CMS不会整理碎片,从上面可以看到CMS用的是标记-清除算法,不可避免的会引入内存碎片,为了防止碎片引起fullgc,需要设置-XX:+UseCMSCompactAtFullCollection,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效,也可以设置-XX:+CMSFullGCBeforeCompaction,此参数设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程。

3、从上面的步骤可以看到,在并发清除阶段,引用程序仍然在运行,伴随着程序的运行,自然仍然有新的对象被创建,因此需要预留足够的内存控件给应用程序使用,因此这种垃圾收集器不能像新生代的survivor区那样,几乎被占满之后,再进行收集,默认配置为68%,也就是说CMS收集器在老年代使用了68%的空间时就会被激活,可以通过参数XX:CMSInitiatingOccupancyFraction的值来设置触发百分比,要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。这个时候,可以设置参数小一点,让CMS执行的更加频繁,降低被占满的可能。

7、G1收集器

详细的请参考这两篇文章  jvm调优Garbage First介绍

最后以一幅图来结束垃圾收集算法和收集器

      

二:创建类的过程

三:读懂GC日志

java 中的gc问题

时间: 2024-09-30 11:19:24

java 中的gc问题的相关文章

Java中System.gc()和Runtime.getRuntime().gc()

(1) GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法. (2) 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址.大小以及使用情况.通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象.通过这种方式确定哪些对象是"可达的",哪些

图解Java中的GC(分代收集器)

前面在Java垃圾收集算法中讲过垃圾收集算法中的分代收集器,今天看了一个视频发现里面将的也很不错,所以决定再总结一下. 我们知道,在分代收集算法中堆空间被分为新生代和老年代.因为新生代中对象的存活率比较低,所以一般采用复制算法,老年代的存活率一般比较高,一般使用"标记-清理"或者"标记-整理"算法进行回收. 上面的这个图已经很清楚的将堆的分区展现出来了. 下面我们来看看具体的算法过程. 新创建的对象一般放在新生代的Enden区,如下图所示. 上面对象中,绿色代表的是

java中的gc log解读

gc log是java程序在出现内存问题时候最好的查看问题的有利日志.下面我们来一步一步分析gc log. 首先,默认java程序是不会开启gc log,我们可以在给jvm参数加上-XX:+PrintGCDetails (这个只是我们在本地调试使用,在生成环境下不可使用. 下面我们可以模拟一个程序,来查看日志. public class GcLog { static final int MB = 1024 * 1024; static void printGC() { byte[] b1, b2

java中的GC(gabage collection)如何工作

1. “引用记数(reference counting)”是一种简单但速度很慢的垃圾回收技术.每个对象都含有一个引用记数器,当有引用连接至对象时,引用计数加1.当引用离开作用域或被置 为null时,引用计数减1.虽然管理引用记数的开销不大,但需要在整个程序生命周期中持续地开销.垃圾回收器会在含有全部对象的列表上遍历,当发现某个 对象的引用计数为0时,就释放其占用的空间.这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况.对 垃圾回收器而言,定位这样

搜集整理java中GC的理解

Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间. 需要注意的是:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身: System.gc() Runtime.getRuntime().gc() 上面的方法调用时用于显式通知JVM可以进行一次垃圾回收,但真正垃圾回收机制具体在什么时间点开始发生动作这同样是不 可预料的 ===================================================

Java中GC的工作原理

转文: 一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率,才能提高整个应用程序的性能.本文将从GC的工作原理.GC的几个关键问题进行探讨,最后提出一些Java程序设计建议,如何从GC角度提高Java程序的性能. 一.GC的基本原理: GC是什么? 为什么要有GC呢? GC是垃圾收集的意思(Garbage Collection),内存处理是编程人员容易出现问题的地方,忘

浅谈Java中的System.gc()的工作原理

很多人把Java的“效率低下”归咎于不能自由管理内存,但我们也知道将内存管理封装起来的好处,这里就不赘述. Java中的内存分配是随着new一个新的对象来实现的,这个很简单,而且也还是有一些可以“改进”内存回收的机制的,其中最显眼的就是这个System.gc()函数. 乍一看这个函数似乎是可以进行垃圾回收的,可事实并不是那么简单.其实这个gc()函数的作用只是提醒虚拟机:程序员希望进行一次垃圾回收.但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的

2. Java中的垃圾收集 - GC参考手册

标记-清除(Mark and Sweep)是最经典的垃圾收集算法.将理论用于生产实践时, 会有很多需要优化调整的地点, 以适应具体环境.下面通过一个简单的例子, 让我们一步步记录下来, 看看如何才能保证JVM能安全持续地分配对象. 您应该已经阅读了前一章: 1. 垃圾收集简介 - GC参考手册 碎片整理(Fragmenting and Compacting) 每次执行清除(sweeping), JVM 都必须保证不可达对象占用的内存能被回收重用.但这(最终)有可能会产生内存碎片(类似于磁盘碎片)

JAVA中GC时finalize()方法是不是一定会被执行?

在回答上面问题之前,我们一定要了解JVM在进行垃圾回收时的机制,首先: 一.可达性算法  要知道对象什么时候死亡,我们需要先知道JVM的GC是如何判断对象是可以回收的.JAVA是通过可达性算法来来判断对象是否存活的.这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的. 在JAVA语言中,可以作为GC Roots的对象包括下面几种: * 虚拟机栈(