java 垃圾回收总结

以前看过很多次关于垃圾回收相关的文章,都只是看过就忘记了,没有好好的整理一下,发现写文章可以强化自己的记忆。

 

java与C,c++有很大的不同就是java语言开发者不需要关注内存信息,不会显式的直接操作内存,而是通过jvm虚拟机来实现。

java虚拟机运行的时候内存分配图如下图:

1

 

jvm虚拟机栈:一个是线程独有的,每次启动一个线程,就创建一个jvm虚拟机栈,线程退出的时候就销毁。这里面主要保存线程本地变量名和局部变量值。

本地方法栈: 调用本地jni方法的时候而创建的。这里分配的jvm之外的内存空间。方法调用结束之后销毁。

pc寄存器 : 这个保存线程当前执行的字节码指令

堆:主要保存创建的对象。

方法区:保存class相关的信息。主要是class的一个内存结构信息

常量池:方法区的一部分,主要保存class内存结构中常量值 例如String值,public static final 类型的值

 

我们这里说的垃圾回收,主要是java虚拟机对堆内存区域的回收。

 

1 首先的问题是:jvm如何知道那些对象需要回收 ?

目前有两种算法

  • 引用计数法

每个对象上都有一个引用计数,对象每被引用一次,引用计数器就+1,对象引用被释放,引用计数器-1,直到对象的引用计数为0,对象就标识可以回收

这个可以用数据算法中的图形表示,对象A-对象B-对象C 都有引用,所以不会被回收,对象B由于没有被引用,没有路径可以达到对象B,对象B的引用计数就就是0,对象B就会被回收。

 

 2

但是这个算法有明显的缺陷,对于循环引用的情况下,循环引用的对象就不会被回收。例如下图:对象A,对象B 循环引用,没有其他的对象引用A和B,则A和B 都不会被回收。

 3

  • root搜索算法

这种算法目前定义了几个root,也就是这几个对象是jvm虚拟机不会被回收的对象,所以这些对象引用的对象都是在使用中的对象,这些对象未使用的对象就是即将要被回收的对象。简单就是说:如果对象能够达到root,就不会被回收,如果对象不能够达到root,就会被回收。

如下图:对象D访问不到根对象,所以就会被回收

4

以下对象会被认为是root对象:

  • 被启动类(bootstrap加载器)加载的类和创建的对象
  • jvm运行时方法区类静态变量(static)引用的对象
  • jvm运行时方法去常量池引用的对象
  • jvm当前运行线程中的虚拟机栈变量表引用的对象
  • 本地方法栈中(jni)引用的对象

由于这种算法即使存在互相引用的对象,但如果这两个对象无法访问到根对象,还是会被回收。如下图:对象C和对象D互相引用,但是由于无法访问根,所以会被回收。

5

jvm在确定是否回收的对象的时候采用的是root搜索算法来实现。

在root搜索算法的里面,我们说的引用这里都指定的是强引用关系。所谓强引用关系,就是通过用new 方式创建的对象,并且显示关联的对象

1
Object obj = new  Object();

以上就是代表的是强引用关系,变量obj 强引用了 Object的一个对象。

java里面有四种应用关系,从强到弱分别为:

Strong Reference(强引用) –>Weak Reference (弱引用) -> Soft Reference(软引用) – > Phantom Reference(引用)

 

Strong Reference : 只有在引用对象root不可达的情况下才会标识为可回收,垃圾回收才可能进行回收

Weak Reference :即使在root算法中 其引用的对象root可达到,但是如果jvm堆内存 不够的时候,还是会被回收。

Soft Reference : 无论其引用的对象是否root可达,在响应内存需要时,由垃圾回收判断是否需要回收。

Phantom Reference :在回收器确定其指示对象可另外回收之后,被加入垃圾回收队列.

 

下面可以看一个测试

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
public  class  ReferenceTest {
 
     public  static  final  Map<Integer, Reference> map = new  HashMap<Integer, Reference>();
 
     public  static  void  main(String[] args) {
         for  ( int  i = 0 ; i < 1000 ; i++) {
             map.put(i, new  WeakReference( new  ReferenceObject(i)));
         }
 
         int  i = 0 ;
         for  (Reference r : map.values()) {
             if  (r.get() == null ) {
                 i++;
             }
         }
         System.out.println( "被回收的对象数:"  + i);
     }
 
     static  class  ReferenceObject {
         private  int     i;
 
         private  byte [] b;
 
         public  ReferenceObject( int  i) {
             this .i = i;
             b = new  byte [ 1024  * 10 ];
         }
     }
}

这里创建大约1000个 10K的 Weak Reference 对象,最后打印的结果是:被回收的对象数:767,这里ReferenceObject如果设置为1K的话,最后的打印结果是0

这个例子并不严谨,但是却说明了被Weak Reference的对象在一定的时候会被jvm回收,但是强引用就不会出现这种状态。



基本上 jvm内存回收有三种 基本算法

  • 标记-清除
标记清除的算法最简单,主要是标记出来需要回收的对象,然后然后把这些对象在内存的信息清除。如何标记需要回收的对象,在上一篇文章里面已经有说明。

2_1

 

  • 标记-清除-压缩

这个算法是在标记-清除的算法之上进行一下压缩空间,重新移动对象的过程。因为标记清除算法会导致很多的留下来的内存空间碎片,随着碎片的增多,严重影响内存读写的性能,所以在标记-清除之后,会对内存的碎片进行整理。最简单的整理就是把对象压缩到一边,留出另一边的空间。由于压缩空间需要一定的时间,会影响垃圾收集的时间。

2_2

 

  • 标记-清除-复制

这个算法是吧内存分配为两个空间,一个空间(A)用来负责装载正常的对象信息,,另外一个内存空间(B)是垃圾回收用的。每次把空间A中存活的对象全部复制到空间B里面,在一次性的把空间A删除。这个算法在效率上比标记-清除-压缩高,但是需要两块空间,对内存要求比较大,内存的利用率比较低。适用于短生存期的对象,持续复制长生存期的对象则导致效率降低

2_3 

 

由于现在的处理器都是多核的,处理器的性能得到了极大的提升,所以在此基础上有产生了几种垃圾收集算法。主要包括两种算法

  • 并行标记清除

所谓并行,就是原来垃圾回收只是一个线程进行。现在创建多个垃圾回收线程。并行的进行标记和清除。比如把需要标记的对象平均分配到多个线程之后,当标记完成之后,多个线程进行清除。

 

  • 并发标记清除

所谓并发,就是应用程序和垃圾回收可以同时执行。在标记清除算法中,在标记对象和清除对象,以及压缩对象的情况下是需要暂停应用的。那么并行标记清除压缩算法则是在标记清除压缩算法的基础上,把标记清除压缩算法分为以下几个过程

初始标记->并发标记->重新标记->并发清除->重置

 

以上几种算法是垃圾回收的基本算法,jvm垃圾回收就是在以上几种算法为基础的,在以上几种算法的基础上,java垃圾回收器可以分为以下几种:

  • 串行收集器

用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器

单线程收集器。在目前多核服务器端运行的情况下,效率比较低。比较适合堆内存小的情况下使用。

2_5

  • 并行收集器

用多线程处理所有垃圾回收工作,利用多核处理器的优势。但是如果线程数量过多,导致线程之间频繁调度,也会影响性能。一半并行收集的线程是处理器的个数。

“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。

2_6

  • 并发收集器

并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器 会对整个应用进行简短的暂停(初始标记的过程),在收集中还会再暂停一次。第二次暂停会比第一次稍长(重新标记的过程),在此过程中多个线程同时进行垃圾回收工作。

并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。

在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。

浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。

Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。

并发收集器,在垃圾回收的时候采用并发标记清除算法的收集器

对响应时间要求高的,多CPU,大型应用。比如页面请求/web服务器。前端业务系统用的比较多。

2_7

 

我们以sun公司的hotspot虚拟机为例,hotspot采用的是分代回收算法,因为根据经验,一般80%的对象就是朝生夕灭,对象的生命期都很短。而根据对象的生命周期不同,可以划分出来几个区域,一部分区域的对象创建比较频繁,但是生命周期比较短,一部分区域对象生命周期比较长,还有一部分区域对象生命周期几乎和java虚拟机相同。

sunspot堆内存分为三个区域:

2_4

  • 新生代(Young)

新生代包括两个区:Eden区和Survivor区,其中Survivor区一般也分成两块,简称Survivor1 Space 和 Survivor2 Space (或者From Space 和 To Space)。新生代通常存活时间较短,因此基于标记清除复制算法来进行回收,扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和From或To之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到Survior,最后到旧生代。

可以采用串行处理和并行处理器

  • 老年代(Old)

在垃圾回收多次,如果对象仍然存活,并且新生代的空间不够,则对象会存放在老年代。

在老年代采用的是 标记清除压缩算法。因为老年代的对象一般存活时间比较长,每次标记清除之后,会有很多的零碎空间,这个就是所谓的浮动垃圾。当老年代的零碎空间不足以分配一个大的对象的时候,就会采用压缩算法。在压缩的时候,应用需要暂停。

可以采用串行处理,并行处理器,以及并发处理器。

  • 持久代(Permanent)

这部分空间主要存放java方法区的数据以及启动类加载器加载的对象。这一部分对象通常不会被回收。所以持久代空间在默认的情况下是不会被垃圾回收的。

 

由于把内存空间分为三块,一般把新生代的GC称为minor GC ,把老年代的GC成为 full GC,所谓full gc 会先出发一次minor gc,然后在进行老年代的GC。

具体的过程如下:

首先想eden区申请分配空间,如果空间够,就直接进行分配,否则进行一次Minor GC。minor GC 首先会对Eden区的对象进行标记,标记出来存活的对象。然后把存活的对象copy到From空间。如果From空间足够,则回收eden区可回收的对象。如果from内存空间不够,则把From空间存活的对象复制到To区,如果TO区的内存空间也不够的话,则把To区存活的对象复制到老年代。如果老年代空间也不够(或者达到触发老年年垃圾回收条件的话)则触发一次full GC。

简单方向就是Eden->From->To->Old,如下图所示。

2_8


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值