Java GC 的那些事儿

发表于:2017-10-10 10:55

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:占小狼    来源:51Testing软件测试网采编

#
java
分享:
  前言
  与C语言不同,Java内存(堆内存)的分配与回收由JVM垃圾收集器自动完成,这个特性深受大家欢迎,能够帮助程序员更好的编写代码,本文以HotSpot虚拟机为例,说一说JavaGC的那些事。
  Java堆内存
  我们知道Java堆是被所有线程共享的一块内存区域,所有对象实例和数组都在堆上进行内存分配。为了进行高效的垃圾回收,虚拟机把堆内存划分成新生代(YoungGeneration)、老年代(OldGeneration)和永久代(PermanentGeneration)3个区域。
  新生代
  新生代由Eden与SurvivorSpace(S0,S1)构成,大小通过-Xmn参数指定,Eden与SurvivorSpace的内存大小比例默认为8:1,可以通过-XX:SurvivorRatio参数指定,比如新生代为10M时,Eden分配8M,S0和S1各分配1M。
  Eden:希腊语,意思为伊甸园,在圣经中,伊甸园含有乐园的意思,根据《旧约·创世纪》记载,上帝耶和华照自己的形像造了第一个男人亚当,再用亚当的一个肋骨创造了一个女人夏娃,并安置他们住在了伊甸园。
  大多数情况下,对象在Eden中分配,当Eden没有足够空间时,会触发一次MinorGC,虚拟机提供了-XX:+PrintGCDetails参数,告诉虚拟机在发生垃圾回收时打印内存回收日志。
  Survivor:意思为幸存者,是新生代和老年代的缓冲区域。
  当新生代发生GC(MinorGC)时,会将存活的对象移动到S0内存区域,并清空Eden区域,当再次发生MinorGC时,将Eden和S0中存活的对象移动到S1内存区域。
  存活对象会反复在S0和S1之间移动,当对象从Eden移动到Survivor或者在Survivor之间移动时,对象的GC年龄自动累加,当GC年龄超过默认阈值15时,会将该对象移动到老年代,可以通过参数-XX:MaxTenuringThreshold对GC年龄的阈值进行设置。
  老年代
  老年代的空间大小即-Xmx与-Xmn两个参数之差,用于存放经过几次MinorGC之后依旧存活的对象。当老年代的空间不足时,会触发MajorGC/FullGC,速度一般比MinorGC慢10倍以上。
  永久代
  在JDK8之前的HotSpot实现中,类的元数据如方法数据、方法信息(字节码,栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32位默认永久代的大小为64M,64位默认为85M,可以通过参数-XX:MaxPermSize进行设置,一旦类的元数据超过了永久代大小,就会抛出OOM异常。
  虚拟机团队在JDK8的HotSpot中,把永久代从Java堆中移除了,并把类的元数据直接保存在本地内存区域(堆外内存),称之为元空间。
  这样做有什么好处?
  有经验的同学会发现,对永久代的调优过程非常困难,永久代的大小很难确定,其中涉及到太多因素,如类的总数、常量池大小和方法数量等,而且永久代的数据可能会随着每一次FullGC而发生移动。
  而在JDK8中,类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间,可以避免永久代的内存溢出问题,不过需要监控内存的消耗情况,一旦发生内存泄漏,会占用大量的本地内存。
  ps:JDK7之前的HotSpot,字符串常量池的字符串被存储在永久代中,因此可能导致一系列的性能问题和内存溢出错误。在JDK8中,字符串常量池中只保存字符串的引用。
  如何判断对象是否存活
  GC动作发生之前,需要确定堆内存中哪些对象是存活的,一般有两种方法:引用计数法和可达性分析法。
  1、引用计数法
  在对象上添加一个引用计数器,每当有一个对象引用它时,计数器加1,当使用完该对象时,计数器减1,计数器值为0的对象表示不可能再被使用。
  引用计数法实现简单,判定高效,但不能解决对象之间相互引用的问题。
  public class GCtest{
  private Object instance=null;
  private static final int_10M=10*1<<20;
  //一个对象占10M,方便在GC日志中看出是否被回收
  private byte[]bigSize=newbyte[_10M];

  publicstaticvoidmain(String[]args){
  GCtestobjA=newGCtest();
  GCtestobjB=newGCtest();

  objA.instance=objB;
  objB.instance=objA;

  objA=null;
  objB=null;

  System.gc();
  }
  }
  通过添加-XX:+PrintGC参数,运行结果:
  [GC(System.gc())[PSYoungGen:26982K->1194K(75776K)]26982K->1202K(249344K),0.0010103secs]
  从GC日志中可以看出objA和objB虽然相互引用,但是它们所占的内存还是被垃圾收集器回收了。
  2、可达性分析法
  通过一系列称为“GCRoots”的对象作为起点,从这些节点开始向下搜索,搜索路径称为“引用链”,以下对象可作为GCRoots:
  本地变量表中引用的对象
  方法区中静态变量引用的对象
  方法区中常量引用的对象
  Native方法引用的对象
  当一个对象到GCRoots没有任何引用链时,意味着该对象可以被回收。
  在可达性分析法中,判定一个对象objA是否可回收,至少要经历两次标记过程:
  1、如果对象objA到GCRoots没有引用链,则进行第一次标记。
  2、如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法。finalize()方法是对象逃脱死亡的最后机会,GC会对队列中的对象进行第二次标记,如果objA在finalize()方法中与引用链上的任何一个对象建立联系,那么在第二次标记时,objA会被移出“即将回收”集合。
  看看具体实现
  publicclassFinalizerTest{
  publicstaticFinalizerTestobject;
  publicvoidisAlive(){
  System.out.println("I'malive");
  }

  @Override
  protectedvoidfinalize()throwsThrowable{
  super.finalize();
  System.out.println("methodfinalizeisrunning");
  object=this;
  }

  publicstaticvoidmain(String[]args)throwsException{
  object=newFinalizerTest();

  //第一次执行,finalize方法会自救
  object=null;
  System.gc();

  Thread.sleep(500);
  if(object!=null){
  object.isAlive();
  }else{
  System.out.println("I'mdead");
  }

  //第二次执行,finalize方法已经执行过
  object=null;
  System.gc();

  Thread.sleep(500);
  if(object!=null){
  object.isAlive();
  }else{
  System.out.println("I'mdead");
  }
  }
  }
  执行结果:
  methodfinalizeisrunningI'maliveI'mdead
  从执行结果可以看出:
  第一次发生GC时,finalize方法的确执行了,并且在被回收之前成功逃脱;
  第二次发生GC时,由于finalize方法只会被JVM调用一次,object被回收。
  当然了,在实际项目中应该尽量避免使用finalize方法。
精选软件测试好文,快来阅读吧~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号