java 复制对象_深入学习Java垃圾回收基础篇

9506963976250b39ea97439b1a8be1aa.png

001 什么是Java垃圾回收

讲一个简单的例子,当你在快餐店吃完饭之后,你需要把食物残渣盘子等投放到指定的回收区进行回收。而当你在KFC吃完之后你可以直接走人,然后服务员会将你的盘子之类的东西收回至指定区域。像这种手动回收就有点类似C++中的手动释放内存。而服务员的自动回收就像Java的垃圾回收。

Java虚拟机的自动内存管理,将原本需要开发人员手动回收的内存交给了垃圾回收器进行回收。既然是自动的肯定没有C++那般好用顺滑。而且还会带来一些实现相关的问题。

002 Java是如何辨别哪些是垃圾(可回收对象)

垃圾回收顾名思义就是把已经分配出去的内存回收回来,以便能够有足够的内存为后续对象分配。在堆里面存放了Java世界中几乎所有的Java对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些是正在使用的,哪些是不用了的。那么如何判断呢?

5d7b52645b16dbf83ab412b3cd0bb97a.png

错了,重来!如何判断哪些是Java中的垃圾(不用的、已经死亡的或者需要被回收的对象)

0001 引用计数法(reference counting)

引用计数法的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为0,则说明该对象已经死亡,便可以进行回收了。

String h = new String("hello world");

fd43842bb780871f12f0ea50e4addc0b.png

如上代码,先在内存中创建hello world字符串,这时有个引用就是h

h = null;

1b09ac02d460a5dca30b40356abc9fa9.png

当给h复制null时,堆中的hello world的引用个数会-1 变为 0,则会触发垃圾回收机制。回收内存。引用计数法需要截获所有的引用更新操作,并且相应地增减目标对象的引用计数器。

除了需要额外的空间来存储计数器,以及繁琐的更新操作,引用计数法还有一个重大的漏洞,那边是无法处理循环引用对象。

请看我手绘版引用计数法的循环依赖讲解:

cd65c0034a33a08391a99ef8aa8cd83f.png

6f7404b581a767135399443d548479f3.png

0002 可达性分析算法

92a2b8fc733ccd31d6973083dcc89331.png

目前Java虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列GC Roots作为初始的存活对象合集(live set),然后从该集合出发,探索所有能够被该集合引用到的对象,并将其加入该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。

那么问题来了,什么是GC Roots呢?这个我们可以理解为由堆外向堆内的引用,一般而言,GC Roots包括(但不限于)以下几种:

  • 虚拟机栈(桢栈中的本地变量表)。

  • 方法区中静态属性引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈JNI(即一般说的Native方法)引用的对象。

  • 已启动且未停止的Java线程

可达性分析算法可以解决引用计数法所不能解决的循环引用问题。举例来说,即便对象a和b相互引用,只要从GC Roots出发无法到达a或者b,那么可达性分析便不会将它们加入存活对象合集之中。

8b3ee112d55ee7f90c5224878c123bd6.png

虽然可达性分析的算法思路很出色,但是在实践中还是会存在很多问题。

002 Stop-the-world 以及安全点

可达性分析的算法有哪些问题呢?

首先我们知道Java的引用对象都存放在方法区和堆栈区中,当Jvm运行对象存活判定算法的时候,如果在当前环境下对象之间的引用还在发生变化,那么这个算法几乎是没办法执行的,所以我们需要使用Stop-the-world来维持秩序。

可达性分析工作必须在一个能够保持一致性的快照中执行,一致性是指整个分析期间整个执行系统看起来就像是冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断的变换的情况。该点不满足的话则无法保证分析的准确性。这点也是导致GC进行时必须停顿所有Java线程的一个重要原因。

那么在什么时间点执行STW最优呢?Java虚拟机通过安全点机制来确定什么时候执行STW。安全点的存在并不是让所有程序停下,而是让程序进入一个稳定的状态,在这个执行状态下,Java的堆栈不会发生变化,这样垃圾回收器就可以安全的执行可达性分析算法了。

003 常见的垃圾收集算法学习

当通过可达性分析算法标记之后我们就能够知道哪些对象是被回收的了。常见的垃圾收集算法有以下几种:

0001 标记-清除算法

标记清除算法分为两个阶段:标记和清除。首先标记需要回收的对象,在标记完成之后再统一的回收所有被标记的对象。

0574cb5e4e5128a0e0ee6762c4a60083.png

标记-清除算法有两个不足:1、效率问题,标记和清除两个效率都不高;2、空间问题,标记清除之后会产生大量的不连续的内存碎片,内存空间碎片太多就可能会导致以后程序在运行过程中需要分配较大的对象时,无法找到足够连续的内存空间,从而不得不提前触发另外一次垃圾收集动作。

4a0bb68b5181b84a0246d4dbb27ed33d.png

0002 复制算法(Copying)

为了解决效率问题就出现了复制算法,它将内存分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行回收,内存空间分配也不用考虑内存碎片问题。分配内存时只用移动堆顶指针,按顺序分配内存即可。实现简单,运行高效。但是复制算法也有很大的弊端,就是将整个内存分半,只能够使用一般的内存,代价太高。

0f1458bda99ac4586bf732134aa95944.png

对于复制算法一般会使用在对新生代对象的回收。Jvm将新生代对象内存划分为一块较大的Eden空间和两块较小的Survivor空间(空间大小可以通过JVM参数调节),每次使用Eden空间和一块Survivor空间。当回收时,将Eden和Survivor中还存活的对象一次性的复制到另外一个Survivor空间中,最后清除掉Eden空间和刚刚使用过的Survivor空间。Hospot虚拟机偶人Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费掉。IBM公司专门做过一个研究表明新生代中98%的对象都是创建使用之后就销毁了。当然,98%的对象可回收只是在一般场景下的数据,我们没办法保证每次回收都只用不多于10% 的对象存活,当Survivor空间不够使用的时候,需要依赖老年代进行分配担保。

0003标记整理算法(压缩算法)

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更重要的是如果不想浪费50%的空间,就需要额外的空间进行分配担保。

压缩算法把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。可以解决内存碎片化以及额外50%内存的问题。但代价是压缩算法的性能开销。

压缩算法的标记阶段和标记清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

3e5b5feec7439ca13ea767b214d539c9.png

总结

本篇文章主要讲解了Java虚拟机可以通过引用计数法和可达性分析算法来标记哪些对象是可以被回收的。在并发环境下通过Stop-the-world和安全点机制来准确的标记对象, 被标记过的对象可以通过标记清除算法、复制算法以及压缩算法来回收内存。也讲解了这些垃圾回收算法的优缺点适用对象。

e275956fb50cb1f9e9085f287ac8e0b4.png

文章中如有疑问请留言。如果该篇文章对你有用点个收藏-在看-点赞吧

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值