JAVA虚拟机的垃圾收集

java对象什么时候变成垃圾?


java对象的实列都存放于jvm的堆区中,但是堆的内存空间是有限的。在空间不足的时候,便会发抛出内存溢出的异常(OutOfMemoryError),通常在遇到这种情况的时候,我们可以选择把堆空间的内存设置到更大,方法:

第一种 环境myeclipse修改
在菜单window->preferecces.在JDK对话框中输入-Xms512m -Xmx1024m这个参数就可以了

但这终究不是个办法,空间总是有限的,这时候jvm的垃圾回收器就该出马了。

他会做些什么?

对,就是把其中一些对象当作垃圾清除掉。什么?你要把我当作垃圾清除,凭什么?jvm说会说,因为我们判定你已经死亡了啊。

怎样判定一个对象死亡?

  1. 一个不太成熟的方法:引用计数

大家可能会问,这是什么东西?和jvm运行时数据区域里面的程序计数器有什么联系吗(什么?你不知道程序计数器这个东东,赶紧去看看)。下面且听我慢慢道来。

在很久之前,引用计数是作为判断一个对象是否存活的算法存在的。具体实现是:

为每一个java对象都添加一个引用计数器,当有一个地方引用这个对象的时候,就会是引用计数器的值增加一,当引用失效的时候计数器的值就减一。当对象的引用计数器为零的时候,jvm就认为他凉了,内存不够时,就得把他当垃圾干掉了。但是这种方法存在一个缺陷就是,无法解决对象之间相互循环引用的问题

也许你会问,什么叫做循环引用。我之前也不知道啊,所以搜了下,那就一起看看吧。

class A {
     Object instance = null;
     static final int 1_M = 1024*1024;
     static byte[]  bigSize =  new byte[2*1_M];  //占用内存用
     
    void main() { 
         A a = new A();
         B b = new B();
         a.instanc = b;
         b.instance = a;
         a = null;
         b = null;
         System.gc;//手动回收
    }
}

事实证明(这个事实需要大家手动式验证一下)我们这次手动的gc并没有导致对象a和对象b被回收啊,说明java的垃圾回收并没有使用引用计数这种算法策略了已经。那是使用了什么呢? 对,就是一种叫做根搜索的算法(到目前为止,我们已经多次提到了 引用这个词,如果还不是很理解的小伙伴,需要补补咯)。

在根搜索算法中有一个重要的点就是 “GC ROOT” 对象,稍后介绍哪些可以作为GC ROOT 对象。根搜索算法的思路就是,以GC ROO对象为起点,从这个起点开始往下搜索,搜索所走过的路线叫做引用链,当一个对象到GC ROOT 对象诶有什么引用链的时候,证明这个对象不可用,就会被纳入GC观察区了(这个名词楼主yy的,并非官方)。

说了这么多概念,来个小列子考考大家了:

在这里插入图片描述
以上图片中对象 5、6、7是否可用?

差点忘了很重要的一点,什么样的对象才有资格作为很牛逼的GC ROOT呢?

  1. 虚拟机栈(这个概念不懂又得八百字补补了)中的引用对象
  2. 方法区中的静态属性引用对象
  3. 方法区中的常量引用对象
  4. 本地方法栈中JNI的引用对象(这个楼主也不懂,还望哪位小伙伴指导)

好好记住上面几个区域里面的引用对象,可是决定着很多人的生死呢

生存还是死亡?


上面说完了jvm判断一个对象生死的算法。在跟搜索算法中,当GC ROOT不可达的时候,我们只说这个对象不可用,并不一定意味着他就死亡。我们可以称这些不可达的对象正处于缓刑阶段,所以还是有机会自救的。这到底是个什么情况?

在一个对象真正死亡之前,他往往需要两次标记。当一个对象与GC ROOT不可达的时候,会被进行一次标记,并且会进行一次筛选。筛选的条件是此对象是否有必要执行finalize()(一般用于资源回收,在执行GC方法之前执行)方法。当对象没有覆盖finalize() 方法或者finalize()方法已经被虚拟机调用过,则jvm会认为 “没有必要执行”。

如果 “有必要执行 ” 会怎么样呢?

如果 某个对象 被认为 有必要执行finalize() 方法,对象会被放入一个 F-Queue队列中。并且在稍后会被虚拟机建立的线程(Finalizer)去执行。这里的执行是指会有这个专门的线程执行 finalize()方法。

处于缓刑的对象的自我救赎:

对象第一次标记并被放入F-Queue队列里面,并且会执行finalize()方法。这时候对象的自我救赎机会来了。对象在finalize()方法中将自己与引用链上的任何一个对象建立连接,如果连接建立成功,将会被移出F-Queue。如果没有连接成功,那就真的离gg不远了。

接下来就来段自我救赎的代码演示了:

package json;

public class SaveAndGC {
	public static SaveAndGC gg = null;

	@Override
	protected void finalize() throws Throwable {
       super.finalize();
       Syso("finalize start---");
       //开始自救表演
       SaveAndGC.gg = this;
  }

public void main() throws InterruptedException {
    gg = new SaveAndGC();
    //干掉引用链,称为缓刑者
    gg = null;
    //触发GC操作‘
    System.gc(); //会执行finalize()方法,并完成自我救赎
    Thread.sleep(500); //考个知识点:wait()和sleep()的区别
    if(gg  != null) {
      Syso(“自救成功!!”)
    }else {
      Syso(“自救失败!!”)
   }

    gg = null;
    //触发GC操作‘
    System.gc(); //会执行finalize()方法,并完成自我救赎
    Thread.sleep(500); //考个知识点:wait()和sleep()的区别
    if(gg  != null) {
       Syso(“自救成功!!”)
    }else {
       Syso(“自救失败!!”)
   }
   }
}

上面进行了两次GC操作,第一次 自救成功 第二次 自救失败。大家可以结合前面的知识分析原因。

垃圾收集算法


前面的知识应该说都是铺垫,从这里开始,才是真正的好戏

  • 标记清除算法

这个算法是最基础的算法,后续算法都是基于此来改造的。

核心分为两步:

  1. 标记。正如前面提到的标记一样,这里就是标记哪些对象为可以回收的对象
  2. 清除。移除掉那些被标记的对象

缺点:

  1. 效率太低
  2. 空间浪费,由于对象的分布问题,会导致很多碎片化的空间,在分配较大内存对象的时候很难找到合适的连续内存空间

效果图如下所示:
在这里插入图片描述

  • 复制算法
    通过对标记清除算法的了解,我们知道标记清除在效率和空间问题上存在一定的缺陷。于是,大牛们又想出了一种叫做复制的垃圾回收算法。

将内存按容量分为大小相等的两份,每次分配内存只使用其中的一份,当一份用完的时候,就将这份上面还存活着的对象一次性复制到另一份上面,然后把已经使用过的那一份内存全部清理掉。

优点:
不用考虑内存碎片等复杂情况,只需要移动堆指针,按顺序分配内存即可,实现简单,运行高效。
缺点:
将内存缩小到原来的一半

运行效果图如下:
在这里插入图片描述

在现在的商用虚拟机中,常常会将内存分为新生代内存区域和老年待内存区域,复制算法就是被大量应用在新生代内存区域中。下面会详细介绍这两种内存区域:

复制算法常常被用在新生代区域,但是由于新生代对象具有朝生夕死的特性,所以在新生代中,使用复制算法的时候,区域不用按照1:1的比例来划分。在新生代区域中,内存被划分为一块Eden区域和两块Survivor区域。Eden和其中的一块Survivor区域被用来作为存放对象的区域,还有一个Survivor区域被用来作为拷贝区域使用。


同时,在新生代区域中。Eden和Survivor区域是按照8:1的比例划分的。列入新生代被分配到10M内存,其中Eden占有8M,每个Survivor占有1M。

新生代
如上图所示:其中Edene、Survivor_01被用来存储新产生的对象,Survivor_02被用来作为拷贝区域。
现在假设一种情况:

  • 整个新生代内存区域为100M
  • 所以:Edene = 80M,Survivor_01 = 10M,Survivor_02 = 10M

所以现在新生代可以容纳的最大新生对象空间为90M(暂时不考虑某一个对象空间大于90M的情况),拷贝区域可以容纳的对象空间为10M。假设现在进行一次新生代垃圾回收(垃圾回收分为新生代和老年待垃圾回收),死亡的对象空间为20M,所以剩下的存活对象为70M,如果要进行基于复制算法的垃圾回收,Survivor_02的10M内存空间显然是不够的。这时候就需呀老年代进行空间的分配担保了。

这里稍微补充一点关于新生代和老年代的知识:
Java堆中是JVM管理的最大一块内存空间。主要存放对象实例。
在JAVA中堆被分为两块区域:新生代(young)、老年代(old)。
堆大小=新生代+老年代;(新生代占堆空间的1/3、老年代占堆空间2/3)
默认创建的对象都是先放在新生代,当gc收集发生之后,若该对象没有没回收,并且达到了老年代的年龄,就被转移到老年代

结合前面的知识,我们来介绍另一种专门应用于老年代的垃圾收集算法:标记整理算法。

如下图所示:核心在于让存活的对象向一端游走
在这里插入图片描述

分代收集算法

核心便是:将内存分为新生代和老年代,然后在新生代中使用复制算法,在老年代中使用标记整理算法。

相信如果是第一次接触jvm相关知识的小伙伴,一定对这篇笔记还有很多疑惑。不用担心,我将在下一篇笔记中为大家带来一个对象的出生过程,详细讲解在最初对象是怎样在内存中划分的。

好了,关于垃圾回收的部分就介绍到这里,如有错误还望多多指教。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值