Java GC

什么是GC:

  Garbage Collection简称为GC,垃圾回收机制

  GC可以自动管理内存和垃圾清扫机制,释放内存中的资源和垃圾

  GC可以有效的防止内存泄漏,有效的使用空闲的内存

内存泄漏:
 程序中间动态分配了内存,但在程序结束后没有释放这部分内存,从而造成这部分内存不可用的情况
 在不涉及复杂数据结构的一般情况下,Java的内存泄漏表现为一个内存对象的生命周期超过了程序需要它的时间长度,也被称为“对象游离”

内存泄漏可以分为4类:
 常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏
 偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或者操作过程下才会发生
   常发性和偶发性是相对的,对于特定的环境,偶发性也许就变成了常发性的,所以测试环境和测试方法对检测内存泄漏至关重要
一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块而且仅有一块内存发生泄漏;比如,在类的构造器中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次
 隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存;
  严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存,但是对于一个服务器程序,需要运行几天,几周,甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存

内存泄漏和内存溢出的区别:
  相同点:都会导致应用程序出现问题,性能下降或挂起
  不同点:内存泄漏是导致内存溢出的原因之一,内存泄漏积累起来将导致内存溢出
    内存泄漏可以通过完善代码来避免,内存溢出可以通过调整配置来减少发生频率,但无法彻底避免

GC的特点:

  GC只负责回收堆内存,不会回收任何物理资源

  程序无法精确控制垃圾回收动作的具体发生时间,垃圾回收只有在内存不足或程序处于空闲时间才会发生回收动作

  在GC回收任何对象之前,都要先调用它的finalize()方法,finalize()可以使一些垃圾对象重新复活,finalize()方法结束后,垃圾回收动作才会发生

  调用GC并不能保证GC实际执行

  GC回收的是无任何引用的对象占据的内存空间而不是对象本身

finalize():
  java允许使用finalize()方法在GC回收对象之前作必要的清理工作
  这个方法是由GC确定这个对象没有被引用时对这个对象调用
  它是在Object类中定义的,因此所有的类都继承它;子类覆盖finalize()方法来整理系统资源或者执行其他清理工作

对象引用的类型:

对象的引用可以分为强引用、软引用、弱引用、虚引用

  强引用:可以理解为普通的引用,即我们在创建对象时,指向某个对象的引用

  软引用:由java.lang.reg.SoftReference实现,使用软引用的对象,当系统中内存足够,程序运行稳定时,垃圾回收器不会考虑回收该对象,而且程序也可以使用该对象,但是,当系统内存不足,垃圾回收器准备回收对象时,回收期可能将该对象进行回收

  弱引用:由java.lang.reg.WeakReference实现,弱引用和软引用差不多,内存空间充足,垃圾回收动作不会触发时,该对象可以被程序使用,但垃圾回收动作触发时,该引用指向的对象就肯被回收,并且弱引用的引用级别比软引用要低,就是说如果系统存在软引用和弱引用,垃圾回收器将首先回收弱引用指向的对象

  虚引用:由java.lang.reg.phantomReference实现,虚引用比较玄幻,就是虚引用类似于没有 ,当一个对象有虚引用指向他时,该对象和没有引用差不多,所以虚引用的引用级别最低,而且虚引用不能单独使用,必须与引用队列ReferenceQueue联合使用

  引用级别:强引用>软引用>弱引用>虚引用

对象在内存中的状态:

  可达状态:如果一个对象在创建之后,有一个或者是多个引用指向该对象,那么这个对象就处于可达状态

  可恢复状态:程序中,如果一个对象没有任何引用指向它,那么该对象处于可恢复状态,处于可恢复状态下的对象,GC在准备回收垃圾时,调用finalize()方法,在finalize()中,系统有可能重新让一个或多个引用指向该对象,那么这个对象就由可恢复状态转变为可达状态

  不可达状态:GC被触发调用finalize()方法时,处于内存中的可恢复状态的对象没有重新获取引用,那么该对象就处于不可达状态

 GC实现垃圾回收的基本原理:

  java内存管理实际就是对象的管理,其中包括对象的分配和释放

  对于程序员来说,分配对象使用new关键字,释放对象时只是将对象赋值为null,让程序员不能够再访问到这个对象,该对象就处于“不可达状态”,GC负责回收所有“不可达”对象的内存空间

  对于GC来说,当程序员创建对象时,GC就开始监控这个对象地址、大小以及使用情况。通常GC采用有向图的方式记录并管理堆中的所有对象,通过这种方式确定哪些对象是“可达的”,哪些对象是“不可达的”。当GC确定一些对象是“不可达”时,GC就有责任回收这些内存空间,但是为了GC能够在不同的平台上实现,java规范对GC的很多行为都没有严格的规定,例如采用什么类型的回收算法,什么时候进行回收等重要的问题都没有明确的规定,因此不同的JVM实现着不同的算法,这也给程序开发带来了很多不确定性,但是任何一种垃圾回收算法一般都要做两件事情:发现无用的不可达对象和回收被不可达对象占用的内存空间,使该空间可被程序再次使用

垃圾回收算法:

引用计数法:(Reference Counting Collector )

  引用计数是垃圾收集器中的早期策略,这个方法中,堆中的每个对象实例都有一个引用计数,当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1;当任何其他变量被赋值为这个对象的引用时,计数器加1(a=b,则b的引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器-1;任何引用对象为0的对象实例可以被当作垃圾收集;当一个对象实例被垃圾收集时,他引用的任何对象实例的引用计数器-1

  优点:引用计数收集器可以很快的执行,交织在程序运行中,对程序需要不被长时间打扰的实时环境比较有利  

  缺点:无法检测出循环引用。如一个父对象有一个对子对象的引用,子对象反过来引用父对象,这样,他们的引用计数永远不可能为0

trancing算法或标记-清除算法:(mark and sweep)

   根搜索算法:

    程序将所有的引用关系看作是一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕后,剩余的节点则被认为是没有引用到的节点,即无用的节点

java中可以作为GC ROOT的对象有:
  虚拟机栈中引用的对象(本地变量表)
  方法区中静态属性引用的对象
  方法区中常量引用的对象
  本地方法栈中引用的对象(Native对象)

  标记-清除算法:

    采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存货对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片

 compacting算法或标记-整理算法:

  标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活对象占用的空间后,会将所有的存货对象往左端空闲空间移动,并更新对应的指针;标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是解决了内存碎片的问题,在基于compacting算法的收集器的实现中,一般增加句柄和句柄表

copying算法:

  copying算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收,它开始时把堆分为一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存空间没有空闲洞),这样空闲面成为了对象面,原来的对象面成为了空闲面,程序会在新的对象面中分配内存;一种典型的基于copying算法的垃圾回收是stop-and-copy算法,它将堆分为对象面和空闲区域面,在对象面和空闲区域面的切换过程中,程序暂停执行

 generation算法:

   分代的垃圾回收策略,是基于这样一个事实:不同对象的生命周期是不一样的,因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率

新生代:所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象

    新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。
    大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,
    当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,
    此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复
    
    当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

    新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

年老代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象
    
   内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高

持久代:用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类

强制垃圾回收:

  JVM在收到消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法进行加权,使GC容易执行,或者是提早发生,或回收较多而已

  强制垃圾回收有两种方式:

    1 调用System类的gc()静态方法:System.gc()

    2 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()

优化垃圾回收:

  1 尽早释放无用对象的引用:好的办法是使用临时变量的时候,让引用变量在退出活动域后自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露

  2 程序进行字符串处理时,尽量避免使用String,而应使用StringBuffer

    因为每一个String对象都会独立占用内存一块区域,如:

      String str = "aaa";    
      String str2 = "bbb";    
      String str3 = str + str2;    
      // 假如执行此次之后str , str2再不被调用,那么它们就会在内存中等待GC回收;    
      // 假如程序中存在过多的类似情况就会出现内存错误
  3 尽量少用静态变量
    因为静态变量是全局的,GC不会回收
  4 避免集中创建对象尤其是大对象,如果可以的话尽量使用流操作
    JVM会突然需要大量内存,这时会触发GC优化系统内存环境; 一个案例如下:
      // 使用jspsmartUpload作文件上传,运行过程中经常出现java.outofMemoryError的错误,    
      // 检查之后发现问题:组件里的代码    
      m_totalBytes = m_request.getContentLength();    
      m_binArray = new byte[m_totalBytes];    
      // totalBytes这个变量得到的数极大,导致该数组分配了很多内存空间,而且该数组不能及时释放。    
      // 解决办法只能换一种更合适的办法,至少是不会引发outofMemoryError的方式解决
  5 尽量运用对象池技术以提高系统性能
    生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略
  6 不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象
    可以适当的使用hashtable,vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃
  7 优化配置
    a.设置-Xms、-Xmx相等
    b.设置NewSize、MaxNewSize相等
    c.设置Heap size, PermGen space

 参考链接:

  http://blog.csdn.net/simple727/article/details/51296756

  http://blog.csdn.net/aliz13/article/details/70208295

  https://www.cnblogs.com/andy-zcx/p/5522836.html

原文地址:https://www.cnblogs.com/roxy/p/7852899.html