logo头像

一路过来,不过游牧自己。。

深入理解JAVA中的垃圾回收机制和gc原理


我们说,在计算机中,内存是作为一种有限的资源存在,用一点少一点,那总有用尽的那一天,所以如何管理内存问题可是一名大学问。

一、什么是垃圾回收及其意义:

我们能想象到如果每次用的内存都需要自己回收,那就非常蛋疼,所幸Java语言一个显著的特点就是引入了垃圾回收机制,对应的就是一个内存的回收问题,自动回收,这样听起来很酷吧!垃圾回收机制,可以有效防止内存泄露问题,有效使用空闲内存!

内存泄漏:就是指该内存空间使用之后没有回收,简单点就是内存对象超过了对象的生命周期(后续不在使用),但是没有做回收处理,这样造成一个结果就是内存越用越少!(想象下手机需要不断删除垃圾,只不过Java将这个过程自动化了)

二、垃圾回收算法描述:

在JAVA中,怎样去实现这个过程呢,那就会牵涉到一定的算法,所谓的算法,就是解决问题的方式。回收垃圾要做的就是:1.发现没用的垃圾 2.回收没用的垃圾(释放内存空间),这样的算法有多种,JVM没有明确哪种可以,但是不外乎以下这些算法:

1.引用计数法:

1.1算法原理:

从题目中就可以看出,它采用的就是计数的方式,每一个对象实例都有一个变量值,创建的时候就是将计数器设置为1。任何其他对象被赋值为这个对象的引用时,那这个对象上的变量值就会+1,举个栗子:a=b,那对于b来说,他就增加了一个引用,所以,他的计数器就+1。但是,我们说这个对象一旦超出了他的生命周期或者被设置成一个新值得时候,这个对象的计数值就会-1,那么当其计数值为0时,就说明没有引用了,自己也超过了生命周期(创建时为1),那就会被回收掉!

1.2算法优缺点

计数器可以很快的执行,对长时间不需要打断的程序比较有利!
缺点:无法解决循环引用的问题,例子:父对象有一个子对象的引用,子对象有一个父对象引用,所以,我们可以看到的是:双方的计数值都不可能为0,所以怎么都回收不掉!

1
2
3
4
5
6
7
8
9
10
11
12
    public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();

object1.object = object2;
object2.object = object1;

object1 = null;
object2 = null;
}
}

上面这段程序就是这样, object1和object2都是为Null了,但是可以看到,他们还是相互引用,所以,计数器不可能为0,所以呢,是不能被回收的!

2.tracing算法(Tracing Collector) 或 标记-清除算法(mark and sweep):

2.1根搜索算法:

根搜索算法原理图如下:

根搜索算法原理图

简单讲下这张图,如图所示,他们的引用关系是一张图,OBJB引用OBJA,OBJC引用OBJA,还有若干引用OBJC,OBJF没有对象引用了,就是说它没有可达其他对象的路径,那就没用了,回收!如果OBJB生命周期已过,那就自动解除那条链接OBJA的线,对于OBJB来说,他就可以回收了!所有都是从GC Root开始寻找,如果可达,那就是还在用的,如果不可达,那就说明可以被回收了!
JAVA中哪些可以被作为GC Root对象呢:

  1. 虚拟机栈中引用的对象(本地变量表)
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中引用的对象(Native对象)

2.2tracing算法的示意图:

算法示意图

如图所示,整个过程是一个扫描的过程,在从根集合扫描之后,会对每一个可达对象做一个标记,然后最后扫描,扫描到没有标记的B,到最后B会被是回收掉,因为这个时候B是不可达的!这种方法回收了不存活的对象,但可以看到会有很多内存碎片!

3.compacting算法 或 标记-整理算法

我们在上面说了,这个tracing算法有个很不好的缺点,那就是内存碎片问题,内存回收了,但细小的碎片还是不能用,所以回收内存会变得没有意义,所以,我们要做的,就是要将这种进行调整:

对这样的碎片及时处理掉,这样就会更加高效,这样来说,算法消耗会更大,但是更加有意义,使内存真正空出来给别人用!在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

4.copying算法(Compacting Collector)

从算法名称可以看出的是,copy,就是和复制有关,下面看图分析:
copying图解
我们将内存堆分成了两部分,一个是对象面,一个是空闲面。我们在对象面为我们的对象分配内存,当我们的对象面满了的时候,我们就开始扫描对象面,一扫描到活动的对象,我们就copy到空闲面中,这样一趟扫描下来,我们就将不用的对象都剔除出去了,而活动的对象都存到了空闲面,这时候再将空闲面转变成为对象面,对象面转化为空闲面,可以有效的克服句柄的开销和解决堆碎片的垃圾回收!

5.generation算法(Generational Collector)

二话不说先插图:

这里说一个分代回收的概念:年轻代,年老代,持久代。在Java中不同对象的生命周期是不一样的,所以针对不同周期的对象采用不同的回收算法,提高回收率!

年轻代(Young Generation):

  1. 所有刚生成的对象都放在年轻代里面,其目标就是回收那些生命周期短的对象!
  2. 新生代内存按8:1:1的比例区分三个区,是eden:survivor0:survivor1(一般都是这样)。步骤描述是这样:首先对象在eden区生成,当回收的时候,首先将活动的对象赋值到survivor0区,然后清空eden区,这时候其实已经去掉一部分已经没用的对象。当survivor0区也满了的时候,就将survivor0和eden区存活的对象复制到survivor1区,然后清空survivor0和eden区,这时候survivor0是空的,这时候在将survivor1和survivor0交换,这样会保持survivor1空,如此一直重复。
  3. 这样一直往复的时候,就会出现一种情况,就是eden和survivor0区copy的对象在survivor1放不下的时候,就会将活的对象存到年老代,当年老代都Full的时候,就会进行一次扫描和清理
  4. 新生代发生的GC是叫做MinorGC,MinorGC发生的频率很高(不一定等Eden区满了才触发)。

年老代(Old Generation):

  1. 在年轻代经历了N次垃圾回收之后依旧存活的对象,就会被放到年老代中,就是说一些生命周期比较长最后会放到这里面!
  2. 与新生代相比,其内存也在大概两倍之多,触发的GC为Major GC即Full GC,相对来说,其发生的频率比较低,存活时间比较长!

持久代(Permanent Generation):

从字面了解就是比较持久的东西,不太会被GC的东西!用来存放静态文件,生命周期基本都是从程序开始到程序结束,如JAVA类和方法等。可以预料的是,持久代对垃圾回收没啥影响,但是有些应用可能动态生成或者调用一些Class,例如Hibernate等,在这种时候需要设置一个比较持久的空间来存储这个新增的类!

(总的理解就是刚产生的放年轻代,一段时间GC不掉的就放年老代)

三、GC(垃圾收集器)

 新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge


 老年代收集器使用的收集器:Serial Old、Parallel Old、CMS

A:Serial收集器(copying算法)

 新生代单线程收集器,标记和清理都是单线程,简单高效!

B:Serial Old(标记-整理算法)

 和前面是一样的,也是单线程,但是是在年老区,Serial的年老版本!

C:PraNew(停止-复制算法) 

 只不过是Serial的多线程版本,如果是在多核CPU下效果会非常明显!

D:Parallel Scavenge:(停止-复制算法)

 并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。

E:Parallel Old(停止-复制算法)

 Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先

F:CMS(Concurrent Mark Sweep)(标记-清理算法)

 高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择!

四、GC的执行机制

 对象的分代处理,使得不同生命周期的对象的回收区域和回收时间都不同,主要有两种类型:Scavenge GCFull GC

1.Scavenge GC

 一般情况下,新对象生成在Eden区申请的时候(有可能已满),就会触发 Scavenge GC,对Eden区进行GC,然后将存活是的对象转移到Survicor区,然后整理Survicor的两个区,这是在新生代中执行的,不影响年老代,且Eden区不会分配很大的空间,所以Eden区的GC会很频繁,所以需要速度快和效率高的算法,使Eden区能够尽快释放出来!

2.Full GC

 是对整个堆进行整理,包括Young、Tenured和Perm。Full GC是对整个堆进行回收的,所以比上一个慢,所以我们得减少他的次数。JVM调优就是对fuLL gc的一个调节!可能如下原因导致GC:

  1. 年老代(Tenured)被写满
  2. 持久代(Perm)被写满
  3. System.gc()被显示调用
  4. 上一次GC之后Heap的各域分配策略动态变化

上面说的年老代和持久带被写满,这应该很好理解,但是System.gc()是什么呢?

3.System.gc()方法

这是一个显示调用垃圾回收的方法,调用JVM不管应用那种回收算法,都可以很好的回收内存!
但是,执行这个方法并不是立即做垃圾回收,只是对垃圾回收几个算法做了加权,使垃圾回收操作更容易发生,或提早发生或回收较多而已,仅仅只是给JVM的一个请求建议而已!
采用命令行的方式也可以查看GC垃圾回收器的运行,主要命令是:
java -verbosegc classfile

4.finalize()方法

在JVM 垃圾回收器回收一个对象之前,一般会让程序调用适当的方法释放资源,但在 没有明确 释放资源的情况下,JAVA提供了缺省机制来终止对象并释放资源,这个方法就是finalize(),其原型函数为:

1
protected void finalize() throws Throwable

在finalize方法返回之后,对象消失,垃圾收集开始执行,throws Throwable表示他可以抛出任何异常!
为什么会用finalize()方法呢?因为在Java中也存在特殊的垃圾回收器不能处理的情况。假定你的对象(不使用new方法)获得了一个特殊的内存地址,但垃圾回收器只会回收那些new的对象空间,却拿这些特殊的无可奈何,这时候就要用到finalize()了!
那有哪些特殊的时侯呢?

  1. 在分配内存的时候,采取了类似C语言的做法,采用malloc函数来动态分布内存,这时候,就要用free()函数来分配,但是在JAVA中,就是释放的是NEW函数,这样就必须调用finalnize()函数来释放这些特殊的空间了!
  2. 或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。
    总而言之,就是释放那些其他做法所使用的空间,以及做一些清理工作。JAVA必须自己动手创建一个执行清理工作的普通方法,那就是object类中的这个finalize() 方法
    一般来说,在普通的清理工作的时候,为清除一个对象,那对象必须在希望释放的地点调用一个清除方法。在C++概念里,所有对象都应该被破坏,一般调用的清理函数会在“{}”这个末尾结束清理工作!若对象是用new创建的(类似于Java),那么当程序员调用C++的 delete命令时(Java没有这个命令),就会调用相应的析构函数。若程序员忘记了,那么永远不会调用析构函数,我们最终得到的将是一个内存”漏洞”,另外还包括对象的其他部分永远不会得到清除。
    所以我们可以预想到,JAVA中没有析构函数就是因为他的垃圾回收机制,Java不允许我们创建本地(局部)对象–无论如何都要使用new。但是其实深入学习之后,可以预想到,其实垃圾回收机制并不能完全满足析构函数所带来的垃圾回收那种需求!(finalize()函数是在垃圾回收器准备释放对象占用的存储空间的时候被调用的,绝对不能直接调用finalize(),所以应尽量避免用它)若希望执行除释放存储空间之外的其他某种形式的清除工作,仍然必须调用Java中的一个方法。它等价于C++的析构函数,只是没后者方便。
    来做个总结:在C++中所有的对象运用delete()一定会被销毁,而JAVA里的对象并非总会被垃圾回收器回收。简言之 1 对象可能不被垃圾回收,2 垃圾回收并不等于“析构”,3 垃圾回收只与内存有关。也就是说,并不是如果一个对象不再被使用,是不是要在finalize()中释放这个对象中含有的其它对象呢?不是的。因为无论对象是如何创建的,垃圾回收器都会负责释放那些对象占有的内存。

五、GC与内存泄漏

我们说,GC是负责内存回收的,但是是不是GC之后就不会发生内存泄漏了呢?答案是:不是!他同样会出现内存泄漏问题!

  1. 静态集合类像HashMap,Vector的使用最容易出现内存泄漏,这些静态变量的生命周期是随着程序从一而终的,所以所有的Object的对象也不能释放。
    举个例子:
    1
    2
    3
    4
    5
    6
    7
    Static Vector v = new Vector();
    for (int i = 1; i<100; i++)
    {
    Object o = new Object();
    v.add(o);
    o = null;
    }

这个例子应该比较容易懂吧,代码栈中存在Vector 对象的引用v和 Object 对象的引用。在for循环中,不断生成对象o,然后不断添加到Vector中,但是这之后一直将o置空。问题是当o置空之后,如果发生GC(),是否可以被GC回收Z?我们说,GC()回收的都是不可达对象,所以当垃圾回收机一直顺着V往下追踪时,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。所以OBject对象仍然是可以被访问到的,GC无法被释放掉,如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。

  1. 各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
  2. 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

六、减少GC开销的措施

GC是个好东西,但如果不对GC进行设计和管理,也会出现内存驻留等一系列问题。为了避免这些影响,基本原则就是尽可能减少垃圾和GC过程中的开销,具体措施包括以下几个人方面:

  1. 不要显示调用System.gc()
    这个函数建议JVM主GC,虽然是建议,但很多情况下还是会触发GC,从而增加GC的频率,增加了资源消耗和间歇性停顿的次数。

  2. 尽量减少临时对象的使用
    这个是从源头上减少垃圾的产生,减少了GC。

  3. 对象不用时最好显示置为null
    一般来说,为Null的对象都会被作为垃圾回收处理,所以如果显示置为Null,这样有利于GC判定,从而提高了GC的效率!

  4. 尽量使用StringBuffer而不用String来累加字符串
    当用String累加字符串时,如Str5=Str1+Str2+Str3+Str4;这时候会创建很多对象,这样的过渡是没有意义的,只会增加更多垃圾。所以我们要用StringBuffer来累加字符串,可变长,在原有基础上扩增,不产生中间对象。

  5.  用基本类型如Int,Long,不用Integer,Long对象
    基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。减少对象的创建!

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Chair {    
  static boolean gcrun = false;
  static boolean f = false;
  static int created = 0;
  static int finalized = 0;
  int i;
  Chair() {
   i = ++created;
   if(created == 47)
    System.out.println("Created 47");
  }
  protected void finalize() {
   if(!gcrun) {
    gcrun = true;
    System.out.println("Beginning to finalize after " + created + " Chairs have been created");
   }
   if(i == 47) {
    System.out.println("Finalizing Chair #47, " +"Setting flag to stop Chair creation");
    f = true;
   }
   finalized++;
   if(finalized >= created)
    System.out.println("All " + finalized + " finalized");
  }
}

public class Garbage {
  public static void main(String[] args) {
  if(args.length == 0) {
    System.err.println("Usage: /n" + "java Garbage before/n or:/n" + "java Garbage after");
    return;
  }
  while(!Chair.f) {
    new Chair();
    new String("To take up space");
  }
  System.out.println("After all Chairs have been created:/n" + "total created = " + Chair.created +
  ", total finalized = " + Chair.finalized);
  if(args[0].equals("before")) {
    System.out.println("gc():");
    System.gc();
    System.out.println("runFinalization():");
    System.runFinalization();
  }
  System.out.println("bye!");
  if(args[0].equals("after"))
    System.runFinalizersOnExit(true);
  }
}

有兴趣的朋友可以自己丢到运行环境中去看看,这篇博文是边看另一篇博文边写的,原作者也写个很详细,我是转载者,但是其中很多我感觉以一种非常通俗易懂的方式去描述出来,如果有任何问题,可以联系我相互交流!

原文地址:深入理解Java垃圾回收机制

微信打赏

赞赏是不耍流氓的鼓励