JVM 内存分配和垃圾回收(GC)机制

一  判断对象是否存活

垃圾收集器在对堆进行回收前,第一件事情就是要肯定这些对象之中哪些还“活着”,哪些已经"死去”,即不能再被任何途径使用的对象。php

1.1 引用计数法 (Reference Counting

给对象加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效的时候,计数器值减1;任什么时候刻计数器为0的对象就是不可能再被使用的。html

引用计数法的实现简单,判断效率也很高,可是主流的java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的缘由是它很难解决对象之间的循环引用的问题。java

举个例子:算法

对象objA和对象objB都有字段instance,赋值令objA.instance = objB 以及objB.instance = objA,除此以外,这两个对象再无任何引用,可是他们互相引用着对方,致使它们的引用计数都不为0,因而引用计数算法没法通知GC收集器回收他们。缓存

 1.2 可达性分析算法(根搜索算法 GC Roots Tracing

在主流的商用程序语言中的主流实现中,都是经过可达性分析来断定对象是否存活的。jvm

这个算法的基本思想是经过一系列称为“GC Roots”的对象做为起始点,若是从GC roots到这个对象不可达,即一个对象到GC Roots 没有任何引用链相连,则证实此对象是不可用的。post

在java语言中,可做为GC Roots 的对象包括如下几种:性能

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 本地方法栈中JNI(Java Native Interface 通常说的native方法)引用的对象
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象

 1.3 引用

java中将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次逐渐减弱。学习

强引用:ui

  强引用是指程序代码之中广泛存在的,相似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用:

  软引用是用来描述一些还有用但非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围之中进行第二次回收。

弱引用:

  被弱引用关联的对象只能生存到下一次垃圾收集发生以前。

虚引用:

  为一个对象设置虚引用的目的是能在这个对象被垃圾收集机制回收时收到一个系统通知。

 

二 垃圾收集算法

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

  先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象。
  缺点:1 回收了被标记的对象后,因为未通过整理,因此致使不少内存碎片,空间碎片太多可能会致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集机制。
                  2 效率问题,标记和清除两个过程效率都不高 。

      图解:绿色是被标记为可回收的,当回收后,未使用的内存空间很是零碎,产生内存碎片

2.2 复制算法(Copying

  将可用的内存按容量划分为大小相等的两块(from,to),每次只是用其中一块(总有一块是空的【to区域】)。当这块的内存用完了,就将还存活着的对象复制到另一块上面,而后把已使用过的内存空间一次清理完。  
  通常不须要按照1:1的比例来划份内存空间,而是将一块内存分为一块较大的Eden空间和两块较小的Survivor空间(From Survivor 和 To Survivor),每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。
  HotSpot虚拟机默认Eden和Survivor大小的比例是8:1,也就是每次新生代中可用的内存空间为整个新生代容量的90%,只有10%的内存时被浪费的。
  缺点:浪费内存空间,若是对象存活率较高时要执行较多的复制操做,效率下降。
       优势:不产生内存碎片
 
    图解:有一块内存区域是空的,通常是to区域。保留区域每次回收后都由于复制的时候让他们变为连续的地址空间,全部不产生内存碎片。
 

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

  复制收集算法在对象存活率较高的时候就要进行较多的复制操做,效率将会变低。
  标记-整理算法的“标记”过程和标记-清除算法一致,只是后面并非直接对可回收对象进行整理,而是让全部存活的对象都向一段移动,而后直接清理掉端边界意外的内存。
 
    图解:因为标记后继续整理,能够很明显的看出未使用的地址空间都是连续的,不会产生内存碎片。
 

2.4 分代收集算法(Generational Collection

  当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并无什么新的思想,只是根据对象存活周期的不一样将内存划分为几块。通常是把java堆分为新生代和老年代。
  在新生代,每次垃圾收集时都会发现有大批对象死去,只有少许存活,那就使用  复制算法,只须要付出少许存活对象的复制成本就能够完成收集。
  而老年代中由于对象存活率较高,没有额外空间进行分配担保,就必须使用 标记 - 清除  或者 标记 - 整理 算法来进行回收。
 

补充:分代划份内存介绍

    整个JVM内存总共划分为三代:年轻代(Young Generation)、年老代(Old Generation)、(JDK 1.7 没了)持久代(Permanent Generation)

    一、年轻代:全部新生成的对象首先都放在年轻代内存中。年轻代的目标就是尽量快速的手机掉那些生命周期短的对象。年轻代内存分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中的一块Survior.当回收时,将Eden和Survior中还存活的对象一次性拷贝到另一块Survior空间上,最后清理Eden和刚才用过的Survior空间。

    二、年老代:在年轻代经历了N次GC后,仍然存活的对象,就会被放在老年代中。所以能够认为老年代存放的都是一些生命周期较长的对象。

    三、持久代:基本固定不变,用于存放静态文件,例如Java类和方法。持久代对GC没有显著的影响。持久代能够经过-XX:MaxPermSize=<N>进行设置。

 

三  内存分配和回收策略

3.1 Jvm怎么判断对象能够回收了?

  对象没有引用

  做用域发生未捕获异常

  程序在做用域正常执行完毕

   程序执行了System.exit()

  程序发生意外终止(被杀线程等)

在Java程序中不能显式的分配和注销缓存,由于这些事情JVM都帮咱们作了,那就是GC。

有些时候咱们能够将相关的对象设置成null 来试图显示的清除缓存,可是并非设置为null 就会必定被标记为可回收,有可能会发生逃逸。

将对象设置成null 至少没有什么坏处,可是使用System.gc() 便不可取了,使用System.gc() 时候并非立刻执行GC操做,而是会等待一段时间,甚至不执行,并且System.gc() 若是被执行,会触发Full GC ,这很是影响性能。

3.2 JVM GC何时执行?

eden区空间不够存放新对象的时候,执行Minor GC。升到老年代的对象大于老年代剩余空间的时候执行Full GC,或者小于的时候被HandlePromotionFailure 参数强制Full GC 。调优主要是减小 Full GC 的触发次数,能够经过 NewRatio 控制新生代转老年代的比例,经过MaxTenuringThreshold 设置对象进入老年代的年龄阀值(后面会介绍到)。

3.3 JVM分别对新生代和老年代采用不一样的垃圾回收机制

在 Java 中,堆被划分红两个不一样的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young )又被划分为

三个区域:Eden、From Survivor、To Survivor。

   这样划分的目的是为了使 JVM 可以更好的管理堆内存中的对象,包括内存的分配以及回收。

   堆的内存模型大体为:

     

    从图中能够看出: 堆大小 =新生代 + 老年代。其中,堆的大小能够经过参数 –Xms、-Xmx 来指定。

 

新生代(Young generation):绝大多数最新被建立的对象都会被分配到这里,因为大部分在建立后很快变得不可达,不少对象被建立在新生代,而后“消失”。对象从这个区域“消失”的过程咱们称之为:Minor GC 。

老年代(Old generation):对象没有变得不可达,而且重新生代周期中存活了下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正因为其相对大的空间,发生在老年代的GC次数要比新生代少得多。对象从老年代中消失的过程,称之为:Major GC 或者 Full GC。

(JDK7后 没了)持久代(Permanent generation)也称之为 方法区(Method area):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件很是严苛,必须符合如下三种条件才会被回收:

一、全部实例被回收

二、加载该类的ClassLoader 被回收

三、Class 对象没法经过任何途径访问(包括反射)

可能咱们会有疑问:

若是老年代的对象须要引用新生代的对象,会发生什么呢?

为了解决这个问题,老年代中存在一个 card table ,它是一个512byte大小的块。全部老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只须要查询 card table 来决定是否能够被回收,而不用查询整个老年代。这个 card table 由一个write barrier 来管理。write barrier给GC带来了很大的性能提高,虽然由此可能带来一些开销,但彻底是值得的。

 

新生代空间的构成与逻辑

为了更好的理解GC,咱们来学习新生代的构成,它用来保存那些第一次被建立的对象,它被分红三个空间:

· 一个伊甸园空间(Eden)

· 两个幸存者空间(Fron Survivor、To Survivor)

默认新生代空间的分配:Eden : Fron : To = 8 : 1 : 1

 

老年代空间的构成与逻辑

老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代空间绝大部分都是朝闻道,夕死矣。这里的对象几乎都是从Survivor 空间中熬过来的,它们毫不会轻易的狗带。所以,Full GC(Major GC)发生的次数不会有Minor GC 那么频繁,而且作一次Major GC 的时间比Minor GC 要更长(约10倍)。

 

Java 中的堆也是 GC收集垃圾的主要区域。GC 分为两种:Minor GC、FullGC ( 或称为 Major GC )。

Minor GC 是发生在新生代中的垃圾收集动做,所采用的是复制算法。新生代几乎是全部 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象一般不需长久存活,具备朝生夕灭的性质。当一个对象被断定为 "死亡" 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在通过一次 Minor GC后,如果对象还存活,而且可以被另一块 Survivor 区域所容纳(上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另一块 Survivor 区域 ( 即 to 区域 ) 中,而后清理所使用过的 Eden以及 Survivor 区域 ( 即from 区域 ),而且将这些对象的年龄设置为1,之后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,能够经过参数 -XX:MaxTenuringThreshold 来设定),这些对象就会成为老年代。但这也不是必定的,对于一些较大的对象 (即须要分配一块较大的连续内存空间 ) 则是直接进入到老年代

 

Full GC 是发生在老年代的垃圾收集动做,所采用的是标记-清除算法。现实的生活中,老年代的人一般会比新生代的人"早死"。堆内存中的老年代(Old)不一样于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 "死掉" 了的。所以,Full GC发生的次数不会有 Minor GC 那么频繁,而且作一次 Full GC 要比进行一次 Minor GC 的时间更长。

 

另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 (即不连续的内存空间 ),此后须要为较大的对象分配内存空间时,若没法找到足够的连续的内存空间,就会提早触发一次 GC 的收集动做。

 

 

3.4 JVM参数选项

    下面只列举其中的几个经常使用和容易掌握的配置选项

-Xms

初始堆大小。如:-Xms256m

-Xmx

最大堆大小。如:-Xmx512m

-Xmn

新生代大小。一般为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% 

-Xss

JDK1.5+ 每一个线程堆栈大小为 1M,通常来讲若是栈不是很深的话, 1M 是绝对够用了的。

-XX:NewRatio

新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3

-XX:SurvivorRatio

新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 

-XX:PermSize

永久代(方法区)的初始大小

-XX:MaxPermSize

永久代(方法区)的最大值

-XX:+PrintGCDetails

打印 GC 信息

 

 

 

 

 

3.5 jvm调优问题--full gc太过频繁该如何处理?

1 full gc频繁说明old区很快满了。

2 若是是一次full gc后,剩余对象很少。那么说明你eden区设置过小,致使短生命周期的对象进入了old区。

3 若是一次full gc后,old区回收率不大,那么说明old区过小。

 

 

参考

《深刻理解java虚拟机》

http://hllvm.group.iteye.com/group/topic/38223#post-248757

http://www.iteye.com/topic/1119491

http://www.importnew.com/1993.html

相关文章
相关标签/搜索