垃圾收集器与内存分配策略

对象引用以及回收

如何判断对象已死

引用计数算法

在对象当中添加一个引用计数器,每当有一个地方引用它,计数器就加一,引用失效的时候,计数器减一,初始化计数器为1,当计数器为0的时候,对象就不可使用了。

缺点:无法解决循环引用的问题

比如a的外部引用为b,b的外部引用为a,接着将a和b赋为null,正常情况下就会将这两个对象回收,但是ab有外部相互的引用,计数器并不为0,所以并不会回收这两个对象

可见引用计数算法并没有有效的运行,JVM虚拟机使用的算法是基于可达性分析算法来判断是否可以回收对象。

可达性分析算法

这个算法的基本思路就是根据GC Roots的根对象来作为起始节点集,然后从这些节点开始根据引用向下搜索,搜索过程所走过的路径称之为“引用链”(Reference Chain)

若是一个对象到GC Roots若是没有引用连接,也就是说这个对象不能通过引用路径到达就证明这个对象是不可再用的。

一般情况下作为GC Roots的对象包含以下几种:

  1. 方法区中类静态属性引用的对象
  2. 方法区中常量引用的对象
  3. 本地方法栈中JNI引用的对象
  4. Java虚拟栈内部的引用
  5. 所有被同步锁持有的对象
  6. 反映Java虚拟机内部情况的JMXBean,JVNTI中注册的回调,本地代码缓存等。

引用之间的区别

  1. 强引用是传统的引用,也就是new一个对象,只要这种强引用关系还存在,垃圾回收器就不会回收这种对象。
  2. 软引用,只要是被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列入回收范围内进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常
  3. 弱引用,他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收发生为止。当垃圾回收器开始工作的时候,无论内存是否够用都会回收掉弱引用的对象
  4. 虚引用:也被叫做幽灵引用,为一个对象设置虚引用的目的仅仅是在该对象被回收的时候会接收到一个系统通知。

对象死亡之前的最后挣扎

在一个对象没有引用链连接的时候,会去调用Finalize()方法,但是从Java 9开始弃用该方法。

finalize()

finalize()可能会导致性能问题,安全问题,不确定的清理行为。

finalize可将有必要执行这个方法的对象放入一个队列当中,然后遍历该队列,若是过程当中可以与GC Roots任何一个对象产生关联,就可以被免于死亡的结局。

作用:在对象被回收前作清理操作,但是finalize()受JVM实现和垃圾回收的影响。

缺点:1. 不确定性:不知道什么时候被调用,可能会导致资源泄漏,以及不必要的延迟。

		2. 增加回收复杂性。
		2. 可以被重写,影响安全性。
try-with-resources

现在try-with-resources可以替换掉finalize(),这是Java 7引入的新特性,实现了AutoCloseable或Closeable接口的资源可以在try-with-resources语句当中进行自动关闭。

try(创建资源){

使用资源

}catch(Expection e){

}

要使用这样的语句必须要实现上述的两个接口之一。

Java.lang.ref.Cleaner类

在Java 9被引入,也可以作finalize的替换。

回收方法区

方法区的垃圾收集主要回收两部分内容:1.废弃的常量 2.不再使用的类型

不再使用的判断方法如下:

  1. 该类的所有实例对象都被回收
  2. 加载该类的加载器被回收
  3. 该类对象的Class对象没有在任何地方被引用。

垃圾收集算法

分代收集理论

当前大多数虚拟机均遵循分代收集的理论,它们基于这两个假说之上:1.弱分代假说:绝大多数对象都是朝生夕灭的2.强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

这两个假说共同奠定了垃圾收集器的一致原则:收集器将Java堆划分为两个不同的区域,然后将回收对象依据其年龄分配到不同的区域当中存储,也就是新生代和老年代。

当每次要去进行垃圾回收的时候只需要去遍历新生代,以很低的频率去检查老年代即可。

针对他们发展出了“标记-复制算法” “标记-清除算法” “标记-整理算法”。

基于新生代的GC,若是他被一个老年代所引用,那么就出现了跨代引用,这个时候就可以将新生代当中有跨代引用的放在一个区域称之为记忆集,这个结构将老年代分为多份,进行GC的时候只需要去将发生了跨代引用的老年代放入GC Roots进行扫描即可。

标记-清除算法

这是将所有需要回收的对象进行标记,然后在标记结束后统一回收掉所有被标记的对象,这种方法在对象数量大的时候会导致效率低下的特点,并且容易出现空间碎片化问题,要是一个大对象需要大空间进行分配,此刻因为碎片空间化的影响并无法分配内存从而提前引发下一次GC。

标记-复制算法

它是按照容量大小分为相等的两块然后每次只去使用一半,当一半使用完后,就将还存活的对象复制到另一块儿上面,然后将以前那块直接清楚掉。这个算法的缺点就在于可用内存直接缩短到了一半。一项研究表明,一般情况下新生代的对象有98%熬不过第一轮回收。因此不需要1:1来分配空间,对该算法进行改进,分成了一块较大内存Eden和小空间Survivor,发生垃圾搜索的时候,就将Eden和Survivor空间当中存活的对象放到另外一个Survivor空间上,然后直接清除掉之前的Eden和Survivor空间。一般情况下会将两块区域内存的比值设置为8:1。但是每次情况下都不能保证Survivor空间可以放下存活的对象。

此时就可将该对象放入老年代进行分配担保。

标记-整理算法

并不是直接进行清理,而是将存活的对象移到内存空间的另外一端,然后直接清除掉边界以外的内存。

有些算法还进行和稀泥的方式,先进行标记-清除算法,当空间碎片大到一定程度的时候再使用标记-整理算法来获得规整的内存空间。

HotSpot算法细节

在HotSpot当中,会使用一组OopMap的数据结构来记录对象内什么遍历量上是什么类记录下来,并不需要从GC Roots进行全局搜索。

在OopMap的帮助下,HotSpot可以快速完成GC Roots枚举,但是导致OopMap内容变化的指令非常多,那可能会需要大量的额外空间,但是实际情况下HotSpot并不会为每句指令都生成OopMap,只会在特殊位置记录信息,这些位置被称为安全点。

对于安全点来说需要去考虑一个问题,如何在垃圾收集发生的时候,将所有线程跑到安全点,这有两种方法,一个是抢断式中断,一个是主动式中断。抢断式中断不需要线程的执行代码去主动配合,当垃圾收集发生的时候,系统会直接停止所有线程,没有到安全点的线程继续重启跑到安全点停止。现在几乎没有虚拟机使用这种方法。

主动式中断会去设置一个标志位,每个线程在执行的过程当中会不断去轮询这个标志点,一旦发现中断标志为真的时候就会到自己最近的安全点进行中断挂起,轮询标志的位置和安全点是重合的。

但是这是所有线程都在执行的情况,要是线程不执行呢,也就是sleep状态或者是Blcoked状态,无法相应中断命令,这块就引入安全区域来解决,安全区域是指一段代码片段当中,引用关系不会发生改变,这个区域当中任何时刻进行垃圾回收都是安全的。当线程要离开安全区域的时候就会去检查虚拟机是否完成的GC,要是完成了就继续执行下去,没有完成的话就一直等待直到可以离开。

记忆集

记忆集是记录从非收集区域指向收集区域的指针的集合的抽象数据结构。说白了就是老年代对于新生代的引用关系的集合。最简单的实现可以是这些发生引用关系的对象数组来完成这个数据结构。

最常用的记忆集是卡表,也就是精确到哪片区域有跨代指针。卡表最简单的形式可以是一个字节数组。一个卡页的内存当中并不止包括一个对象,要是这个卡页有跨代指针,就会被标记为1,称这个元素变脏了,没有的话标记为0,在GC的时候可以轻易的得出哪些卡页中包含变脏的元素,放入GC Roots扫描即可。

写屏障

在多线程环境下,使用分代垃圾收集器来确保内存的原子性和可见性

通过写屏障来维护卡表状态,在对象赋值更新,引用关系改变的那一刻去更新卡表。

  1. 字段写操作,JVM执行到一个字段写操作的时候触发写屏障操作
  2. JVM在字节码层面或是JIT层面会在字段写操作前后插入特定的写屏障代码,是由JVM自动生成的
  3. 执行写屏障逻辑:
    1. 记录卡表的更新,标记哪些区域有跨代引用
    2. 维护跨代引用
    3. 增量更新,即在对象引用更新的过程当中立刻反映给垃圾处理器,无需等待整个对象的移动
  4. 完成字段写操作。

并发的可达性分析

可达性分析算法基础理论上都基于一个能保障一致性快照当中进行分析,这意味着冻结全线程的运行。

从GC Roots往下遍历对象,这一停顿的时间会与Java堆容量直接产生正比例关系,堆越大,查询的对象越多,停顿时间就越长。

要是能减少这段停止的时间,这收益也就是系统性的。

可使用三色图进行分析。

白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。

灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

这里会产生对象误删的情况:

  1. 赋值器插入一或多的从黑色到白色对象的新引用。
  2. 赋值器删除了灰色到白色的直接或者间接引用。

解决方法是在结束并发扫描后,再对原灰色对象进行扫描,无论引用关系删除与否。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

下水道程序员

你的鼓励将是我奋斗的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值