Java垃圾回收手记

常用定义

java垃圾回收

    在空闲时间以不定时的方式进行垃圾回收,回收的是无任何引用的对象占据的内存空间而不是对象本身

触发主GC(Garbage Collector)的条件

    (1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
    (2)Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。

内存泄露

    程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。

内存溢出

    程序运行过程中无法申请到足够的内存而导致的一种错误。

为什么要有垃圾回收机制

    Java语言建立了垃圾收集机制,用以跟踪正在使用的对象和发现并回收不再使用(引用)的对象。该机制可以有效防范动态内存分配中可能发生的两个危险:因内存垃圾过多而引发的内存耗尽,以及不恰当的内存释放所造成的内存非法引用。最终达到自动释放内存空间,减轻编程的负担的目的。

垃圾回收算法

引用计数法(Reference Counting Collector)

    引用计数是垃圾收集的早期策略。在这种方法中,堆中每一个对象都有一个引用计数。当一个对象被创建了,并且指向该对象的引用被分配给一个变量,这个对象的引用计数被设置为1。比如新建一个对象A a=new A();然后a被分配给另外一个变量b,也就是b=a;那么对象a的引用计数+1。当任何其他变量被赋值为对这个对象的引用时,计数加1。当一个对象的引用超过生存期或者被设置一个新的值时,对象的引用计数减1,比如令b=c,则a的引用计数-1。任何引用计数为0的对象可以被当做垃圾收集。当一个对象被垃圾收集的时候,它引用的任何对象计数减1。在这种方法中,一个对象被垃圾收集后可能导致后续其他对象的垃圾收集行动。比如A a=new A();b=a;当b被垃圾回收以后,a的引用计数变为0,这样导致a也被垃圾回收。
优点
    引用计数收集器可以很快执行,交织在程序的运行之中。这个提醒对于程序不能被长时间打断的实时环境很有利。
缺点
    引用计数无法检测出循环(即两个或者更多的对象互相引用)。循环的例子如,父对象有一个子对象的引用,子对象又反过来引用父对象。这样对象用户都不可能计数为0,就算它们已经无法被执行程序的根对象触及。还有一个坏处就是,每次引用计数的增加或者减少都带来额外的开销。

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

    此算法是为了解决引用计数法带来的不足问题。垃圾回收器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。当扫描结束时,未被标记的对象就是无法触及的,从而可以被收集。

复制算法(Copying)

    此算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

标记-整理算法(Mark-Compact)

    此算法结合了“标记-清除”和“复制”两个算法的优点。但又为了解决赋值算法的缺陷,充分利用内存空间,提出了”标记-整理”算法。该算法标记阶段和”标记-清除”一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

分代收集算法(Generational Collection)

    分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
    目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
    而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。
    注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

这里写图片描述

三.典型的垃圾收集器

常用的垃圾回收器

目前的收集器主要有三种:串行收集器、并行收集器、并发收集器。

一. 串行收集器

    使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。可以使用-XX:+UseSerialGC打开。

二. 并行收集器

  1. 对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC.打开。并行收集器在J2SE5.0第六6更新上引入,在Java SE6.0中进行了增强–可以堆年老代进行并行收集。如果年老代不使用并发收集的话,是使用单线程进行垃圾回收,因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。
  2. 使用-XX:ParallelGCThreads=设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。
  3. 此收集器可以进行如下配置:
    • 最大垃圾回收暂停:指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis=指定。为毫秒.如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。
    • 吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=来设定,公式为1/(1+N)。例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。

三. 并发收集器

    可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。
1. 并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
2. 并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。
3. 在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。
4. 浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
5. Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。
6. 启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=指定还有多少剩余堆时开始执行并发收集

四. 小结

  • 串行处理器:
    –适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。
    –缺点:只能用于小型应用
  • 并行处理器:
    –适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。
    –缺点:应用响应时间可能较长
  • 并发处理器:
    –适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。

如何影响java垃圾回收

    通常我们在开发中无法控制JVM的垃圾回收机制,但是可以通过编程的手段来影响垃圾回收,目的是让对象符合垃圾回收条件。
1.将无用对象赋值为null
2.重新为引用变量赋值
例如:

 Person p = new Person("aaa");
 p = new Person("bbb");

    这样,new Person(“aaa”)这个对象就是垃圾了—-符合垃圾回收条件了。
3.让相互联系的对象称为“岛”对象

 Person p1 = new Person("aaa");
 Person p2 = new Person("bbb");
 Person p3 = new Person("ccc");
 p1=p2; p2=p3; p3=p1;
 p1=null; p2=null; p3=null;

    在没有对p1、p2、p3置null之前,它们之间是一种三角恋关系。分别置null,三角恋关系依然存在,但是三个变量不在使用它们了。三个Person对象就组成了一个孤岛,最后死在堆上—-被垃圾回收掉。
4.强制的垃圾回收System.gc()

System.gc()
Runtime.getRuntime().gc() 

    上面的方法用于显式的通知JVM可以进行一次垃圾回收,但真正垃圾回收机制具体在什么时间点开始进行垃圾回收是不可预料的;唯一能保证的是当你内存在极少的情况,垃圾回收器在程序抛出OutofMemaryException之前运行一次。

finalize()方法

    在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源,但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象心释放资源,这个方法就是finalize()。它的原型为:

protected void finalize() throws Throwable

finalize()方法的理解:
1.finalize()方法是Object中的方法。
2.finalize()方法会在对象被垃圾回收之前被垃圾回收器调用一次,这是Java语言的一种机制。
3.finalize()方法在任何对象上最多只会被垃圾回收器调用一次。
    在finalize()方法返回之后,对象消失,垃圾收集开始执行。原型中的throws Throwable表示它可以抛出任何类型的异常。之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况。假定你的对象(并非使用new方法)获得了一块“特殊”的内存区域,由于垃圾回收器只知道那些显示地经由new分配的内存空间,所以它不知道该如何释放这块“特殊”的内存区域,那么这个时候java允许在类中定义一个由finalize()方法。
finalize()方法使用陷阱:
1.垃圾回收器无法保证垃圾对象能被回收,因此,finalize()方法也无法保证运行。建议不要重写finalize()方法,即使重写,也不要在finalize()方法中写关键的代码。
2.finalize()方法中可以把自己传递个别的对象,这样就不是垃圾了,避免了被回收。但是当下次这个对象又符合垃圾回收的时候,finalize()方法不会被调用第二次了,而是直接被清理掉了。

开发中常用的减少GC开销的措施

(1)不要显式调用System.gc()

    此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。这里特别需要说明的是,在代码中显示的调用System.gc(),并不一定能够进行GC,这个我们可以通过finalize()方法进行验证,即主动调用System.gc(),并不一定每次都调用finalize()方法。finalize()方法的特征是在对象被回收之前, 首先调用finalize()方法。

(2)尽量减少临时对象的使用

    临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

(3)对象不用时最好显式置为Null

    一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

(4)尽量使用StringBuffer,而不用String来累加字符串

    由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer 是可变长的,它在原有基础上进行扩增,不会产生中间对象。

(5)能用基本类型如Int,Long,就不用Integer,Long对象

    基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。什么情况下需要使用Integer?

(6)尽量少用静态对象变量

    静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

(7)分散对象创建或删除的时间

    集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片, 从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC 的机会。


参考链接:
http://blog.csdn.net/zsuguangh/article/details/6429592
http://lavasoft.blog.51cto.com/62575/112126/

发布了79 篇原创文章 · 获赞 88 · 访问量 90万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览