@@:垃圾回收机制详细

写在前面:说实话,真的受刺激了,那多的的钱,就没有了。好好的学习,这个才是硬道理。

This is a very personal and subjective opinion of mine, but I believe that a person well versed in GC tends to be a better Java developer. If you are interested in the GC process, that means you have experience in developing applications of certain size. If you have thought carefully about choosing the right GC algorithm, that means you completely understand the features of the application you have developed. Of course, this may not be common standards for a good developer. However, few would object when I say that understanding GC is a requirement for being a great Java developer. 不牛的程序员不一定会,会的也不一定牛。但是牛B的程序员一定会

转自:http://www.cubrid.org/blog/dev-platform/understanding-java-garbage-collection/


1.垃圾回收机制是什么?

比较简单的回答是:Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。,这里面有几个问题,GC是什么时间,对什么东西,做了什么事情,这个我们慢慢的说。先这么回答着有一个初步的概念。

2.要弄清楚GC机制,首先要对Java的内存模型有所了解,我们再看看Java的内存模型,有一个初步的了解。JVM内存结构由堆、栈、本地方法栈、方法区等部分组成,结构图如下所示:

JVM内存组成结构

1 堆)在thinking in java中有关于,java中的量都存在什么地方的一节,上面很明确的说的是基本上所有的java对象都存在堆中,意思也就是说,我们通过new 创建的对象全部都是放在堆中的,换句话说所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。这两个量应该不会陌生,在设置虚拟机的时候常常因为outofmemery而被调大(就知道调大,呵呵)。堆又被划分为新生代和旧生代,新生代又被进一步划分为Eden(伊甸园)和Survivor(幸存区),最后Survivor由FromSpace和ToSpace组成,可以说幸存区有两部分,第一幸存区,第二幸存区。结构图如下所示:

JVM内存结构之堆

其实图上面,最后的这块,叫做持久代( permanent generation )也被称为方法区(method area)。他用来保存类常量以及字符串常量。因此,这个区域不是用来永久的存储那些从老年代存活下来的对象。继续在Thinking in java 中上面说:虽然某些java数据存储在堆栈中----特别是对象的引用,但是java对象不在其中。可以知道java对象和java对象的引用没有存在一起,不过想一想还是有一定的道理的。

2)栈 每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果
3)本地方法栈 用于支持native方法的执行,存储了每个native方法调用的状态
4)方法区 存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(PermanetGeneration)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。

3 我们就来看看Gc的机制

1)从上面的定义,我们大致的了解到,GC是回收无任何引用的对象占据的内存空间。既然是无任何的引用,那就设计到了有引用,或者现在没有引用但是不能保证以后没有引用的对象,这个就有一个对象的状态的说法,我们看一个例子:

public class Person implements Serializable {  
     Person grilfriend;    //朋友  
     public Person() {}   
     public Person(String name) {  
         super();  
         this.name = name;  
     }  
 } 
public static void main(String[] args) {  
         Person p1 = new Person("Kevin");  
         Person p2 = new Person("Rain");  
         Person p3 = new Person("Sunny");  
            
         p1.friend = p2;  
         p3 = p2;  
         p2 = null;  
     }  

把上面Test.java中main方面里面的对象引用画成一个从main方法开始的对象引用图的话就是这样的(顶点是对象和引用,有向边是引用关系):

Java的内存回收机制

当程序运行起来之后,把它在内存中的状态看成是有向图后,可以分为三种:

1)可达状态:在一个对象创建后,有一个以上的引用变量引用它。在有向图中可以从起始顶点导航到该对象,那它就处于可达状态。(正常的公民)

2)可恢复状态:如果程序中某个对象不再有任何的引用变量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能再导航到该对象。在这个状态下,系统的垃圾回收机制准备回收该对象的所占用的内存,在回收之前,系统会调用finalize()方法进行资源清理,如果资源整理后重新让一个以上引用变量引用该对象,则这个对象会再次变为可达状态;否则就会进入不可达状态。(像是临刑的犯人的监狱,等待有人搭救)

3)不可达状态:当对象的所有关联都被切断,且系统调用finalize()方法进行资源清理后依旧没有使该对象变为可达状态,则这个对象将永久性失去引用并且变成不可达状态,系统才会真正的去回收该对象所占用的资源。(判处了死刑,即将回收)

上述三种状态的转换图如下:

Java的内存回收机制

2.Java对对象的4种引用

1)强引用 :创建一个对象并把这个对象直接赋给一个变量,eg :Person person = new Person(“sunny”); 不管系统资源有么的紧张,强引用的对象都绝对不会被回收,即使他以后不会再用到。

2)软引用 :通过SoftReference类实现,eg : SoftReference<Person> p = new SoftReference<Person>(new Person(“Rain”));,内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前要判断是否为null从而判断他是否已经被回收了。

3)弱引用 :通过WeakReference类实现,eg : WeakReference<Person> p = new WeakReference<Person>(new Person(“Rain”));不管内存是否足够,系统垃圾回收时必定会回收。

4)虚引用 :不能单独使用,主要是用于追踪对象被垃圾回收的状态。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现,eg :

 
 
  1. package test;  
  2.     
  3.  import java.lang.ref.PhantomReference;  
  4.  import java.lang.ref.ReferenceQueue;  
  5.     
  6.     
  7.  public class Test{  
  8.     
  9.      public static void main(String[] args) {  
  10.          //创建一个对象  
  11.          Person person = new Person("Sunny");      
  12.          //创建一个引用队列      
  13.          ReferenceQueue<Person> rq = new ReferenceQueue<Person>();  
  14.          //创建一个虚引用,让此虚引用引用到person对象  
  15.          PhantomReference<Person> pr = new PhantomReference<Person>(person, rq);  
  16.          //切断person引用变量和对象的引用  
  17.          person = null;  
  18.          //试图取出虚引用所引用的对象  
  19.          //发现程序并不能通过虚引用访问被引用对象,所以此处输出为null  
  20.          System.out.println(pr.get());  
  21.          //强制垃圾回收  
  22.          System.gc();  
  23.          System.runFinalization();  
  24.          //因为一旦虚引用中的对象被回收后,该虚引用就会进入引用队列中  
  25.          //所以用队列中最先进入队列中引用与pr进行比较,输出true  
  26.          System.out.println(rq.poll() == pr);  
  27.      }  
  28.  } 

运行结果:

Java的内存回收机制

---http://developer.51cto.com/art/201304/387381.htm 这个讲的比较清晰明白,我们知道了类的状态,以及java对象的集中引用的关系,那么我们接下来就是真正的去了解GC的机制。

回到上面所说的,就是什么时间,对什么东西,做了什么事情?

是什么时间那?系统自身决定,不可预测的时间/调用System.gc()的时候,这个就是比较大概的回答,如果想知道具体的触发的条件,就需要对GC的过程或者说算法比较的熟悉。我们接着看这位大牛给我们说的Gc。

首先得了解 minor Gc 和full GC 这两个的区分起来还是比较简单的。

新生代(Young generation): 绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为”minor GC“。

老年代(Old generation): 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,我们称之为”major GC“(或者”full GC“)。看下面这个图表。

 图1 : GC 空间 & 数据流

有些人可能会问:
如果老年代的对象需要引用一个新生代的对象,会发生什么呢? 这里说一下,新生代主要是新创建的对象。
为了解决这个问题,老年代中存在一个”card table“,他是一个512 byte大小的块。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询card table来决定是否可以被收集,而不用查询整个老年代。这个card table由一个write barrier来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,但GC的整体时间被显著的减少。


图 2: Card Table 结构

 新生代的构成

为了更好地理解GC,我们现在来学习新生代,新生代是用来保存那些第一次被创建的对象,他可以被分为三个空间

  •  一个伊甸园空间(Eden 
  •  两个幸存者空间(Survivor )

一共有三个空间,其中包含两个幸存者空间。每个空间的执行顺序如下:

  1. 绝大多数刚刚被创建的对象会存放在伊甸园空间。
  2. 在伊甸园空间执行了第一次GC之后,存活的对象被移动到其中一个幸存者空间。
  3.   此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。
  4.  当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。(这个地方我们就能够得到答案,当伊甸园满了的时候,会发生minor gc,第一个幸存区满了之后那?)
  5. 在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。

如果你仔细观察这些步骤就会发现,其中一个幸存者空间必须保持是空的。如果两个幸存者空间都有数据,或者两个空间都是空的,那一定标志着你的系统出现了某种错误。
通过频繁的minor GC将数据移动到老年代的过程可以用下图来描述:


图 3: GC执行前后对比

需要注意的是HotSpot虚拟机使用了两种技术来加快内存分配。他们分别是是”bump-the-pointer“和“TLABs(Thread-Local Allocation Buffers)”。Bump-the-pointer技术跟踪在伊甸园空间创建的最后一个对象。这个对象会被放在伊甸园空间的顶部。如果之后再需要创建对象,只需要检查伊甸园空间是否有足够的剩余空间。如果有足够的空间,对象就会被创建在伊甸园空间,并且被放置在顶部。这样以来,每次创建新的对象时,只需要检查最后被创建的对象。这将极大地加快内存分配速度。但是,如果我们在多线程的情况下,事情将截然不同。如果想要以线程安全的方式以多线程在伊甸园空间存储对象,不可避免的需要加锁,而这将极大地的影响性能。TLABs 是HotSpot虚拟机针对这一问题的解决方案。该方案为每一个线程在伊甸园空间分配一块独享的空间,这样每个线程只访问他们自己的TLAB空间,再与bump-the-pointer技术结合可以在不加锁的情况下分配内存。

以上是针对新生代空间GC技术的简要介绍,你不需要刻意记住我刚刚提到的两种技术。不知道他们不会对你产生什么影响,但是请务必记住在对象刚刚被创建之后,是保存在伊甸园空间的。那些长期存活的对象会经由幸存者空间转存在老年代空间。老年代GC处理机制老年代空间的GC事件基本上是在空间已满时发生(放生的时间),执行的过程根据GC类型不同而不同,因此,了解不同的GC类型将有助于你理解本节的内容。

JDK7一共有5种GC类型:

  1. Serial GC
  2. Parallel GC
  3. Parallel Old GC (Parallel Compacting GC)
  4. Concurrent Mark & Sweep GC  (or “CMS”)
  5. Garbage First (G1) GC

其中,Serial GC不应该被用在服务器上。这种GC类型在单核CPU的桌面电脑时代就存在了。使用Serial GC会显著的降低应用的性能指标。现在,让我们共同学习每一种GC类型

1. Serial GC (-XX:+UseSerialGC)

新生代空间的GC方式我们在前面已经介绍过了,在老年代空间中的GC采取称之为”mark-sweep-compact“的算法。

  1. 算法的第一步是标记老年代中依然存活对象。(标记)
  2. 第二步,从头开始检查堆内存空间,并且只留下依然幸存的对象。(清理)

最后一步,从头开始,顺序地填满堆内存空间,并且将对内存空间分成两部分:一个保存着对象,另一个空着(压缩)。

2. Parallel GC (-XX:+UseParallelGC)


图 4: Serial GC 与 Parallel GC的区别

从上图中,你可以轻易地看出serial GC和parallel GC的区别,serial GC只使用一个线程执行GC,而parallel GC使用多个线程,因此parallel GC更高效。这种GC在内存充足以及多核的情况下会很有用,因此我们也称之为”throughput GC“。

3. Parallel Old GC(-XX:+UseParallelOldGC)

Parallel Old GC在JDK5之后出现。与parallel GC相比,唯一的区别在于针对老年代的GC算法。Parallel Old GC分为三步:标记-汇总-压缩(mark – summary – compaction)。汇总(summary)步骤与清理(sweep)的不同之处在于,其将依然幸存的对象分发到GC预先处理好的不同区域,算法相对清理来说略微复杂一点。

4. CMS GC (-XX:+UseConcMarkSweepGC)

图 5: Serial GC & CMS GC

就像你从上图看到的那样, CMS GC比我之前解释的各种算法都要复杂很多。第一步初始化标记(initial mark) 比较简单。这一步骤只是查找那些距离类加载器最近的幸存对象。因此,停顿的时间非常短暂。在之后的并行标记( concurrent mark )步骤,所有被幸存对象引用的对象会被确认是否已经被追踪和校验。这一步的不同之处在于,在标记的过程中,其他的线程依然在执行。在重新标记(remark)步骤,会再次检查那些在并行标记步骤中增加或者删除的与幸存对象引用的对象。最后,在并行交换( concurrent sweep )步骤,转交垃圾回收过程处理。垃圾回收工作会在其他线程的执行过程中展开。一旦采取了这种GC类型,由GC导致的暂停时间会极其短暂。CMS GC也被称为低延迟GC。它经常被用在那些对于响应时间要求十分苛刻的应用之上。当然,这种GC类型在拥有stop-the-world时间很短的优点的同时,也有如下缺点:

  •  它会比其他GC类型占用更多的内存和CPU
  •  默认情况下不支持压缩步骤

在使用这个GC类型之前你需要慎重考虑。如果因为内存碎片过多而导致压缩任务不得不执行,那么stop-the-world的时间要比其他任何GC类型都长,你需要考虑压缩任务的发生频率以及执行时间。5. G1 GC

最后,我们来学习垃圾回收优先(G1)GC类型。

图 6:  G1 GC的结构

 如果你想要理解G1,首先你要忘记你所学过的新生代和老年代的概念。正如你在上图所看到的,每个对象被分配到不同的格子,随后GC执行。当一个区域装满之后,对象被分配到另一个区域,并执行GC。这中间不再有从新生代移动到老年代的三个步骤。这个类型是为了替代CMS GC而被创建的,因为CMS GC在长时间持续运作时会产生很多问题。

G1最大的好处是性能,他比我们在上面讨论过的任何一种GC都要快。但是在JDK 6中,他还只是一个早期试用版本。在JDK7之后才由官方正式发布。就我个人看来,NHN在将JDK 7正式投入商用之前需要很长的一段测试期(至少一年)。因此你可能需要再等一段时间。并且,我也听过几次使用了JDK 6中的G1而导致Java虚拟机宕机的事件。请耐心的等到它更稳定吧。

现在我们什么时间,差不多能够说上来几个啦,上面就是对什么东西,没有引用的对象,不是很全。

  1. 引用计数算法: 
      这种算法的思路是如果某一个对象被别的对象引用,那么就把他们引用计数器加上1,这样当进行垃圾回收时如果判断该引用的数量为0,此时就代表没有进行任何对象对其进行引用,此时就进行回收

  2. .根搜索算法: 
       主流的语言Java,C#甚至古老的语言Lisp都是采用根搜索算法进行垃圾回收的,这个算法的思路就是通过命名一系列的“GC Root"作为起点,从这些起点开始向下搜索,这样可以形成一条引用链,该引用链之外的对象都将被回收,在java钟只有一下的对象才可以被作为GC Root. 

做了什么事情,直接需要把算法将明白了。这个需要一点点的理解。可以比较清晰的说的是:新生代做的是复制清理,老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理等。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值