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

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

哪些内存需要回收?

什么时候回收?

如何回收?

判断对象已死吗?
引用计数算法

在对象中添加一个计数器,引用加一,失效减一,当数值为0的时候不再使用;弊端是无法解决对象间的循环引用问题。

可达性分析算法

有一个GC Roots,通过GC Roots为起点,从这些节点向下搜索,搜索走过的路径成为引用链,当一个对象到GC Roost没有任何的引用链时,则称为改对象不可用。比如a b 相互引用,但是与GC Roots没连接,所以判定为可回收。

其中,可作为GC Roots的对象包括:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象s
  • 本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象
  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(NullPointException、OutOfMemoryError)
  • 所有被同步锁持有的对象
  • 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等
引用

强引用:形如 Object obj = new Object() 这种引用关系就是我们常说的强引用。无论什么情况,只要强引用关系存在,对象就永远不会被回收

软引用:用来描述一些有用但非必须的对象。此类对象只有在进行一次垃圾收集仍然没有足够内存时,才会在第二次垃圾收集时被回收。JDK1.2 之后提供了 SoftReference 类来实现软引用

弱引用:也是用来描述那些非必须对象,但它的强度比软引用更弱一些。被软引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。JDK1.2 之后提供了 WeakReference 类来实现软引用

虚引用:最弱的一种引用关系,一个对象是否存在虚引用,丝毫不会对其生存时间造成任何影响,也无法通过虚引用来取得一个对象实例。设置虚引用关联的唯一目的就是让这个对象被回收时能收到一个系统通知。JDK1.2 之后提供了 PhantomReference 类来实现软引用

无论是通过引用计数法还是可达性分析算法,判断对象的存活都和引用离不开关系。

生存还是死亡?

在可达性分析算法中不可达的对象,也不是必死无疑的。因为每个死亡的对象,要经历过两次标记。第一次是经过可达性算法的标记。然后进行一次筛选,筛选的条件是该对象是否有必要执行finalize()方法。当这个对象没有覆盖finalize()方法或者该方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

如果有必要执行finalize()方法,则会将对象放置一个队列中。finalize()方法是对象逃脱死亡的最后一个机会,稍后GC会对上述队列中的对象进行二次标记,若对象中的finalize()没有与引用链上的对象建立连接,那么将会被回收,若建立连接,将会被移除这个队列。

注意:finalize()方法只会被系统调用一次。

回收方法区

主要回收废弃常量以及无用的类

废弃常量与对象相似,只要不在被引用,就会被清理;

无用的类要满足一下三个条件***才有可能***回收:

1、该类所有实例都已经被回收;

2、加载该类的classloader已经被回收;

3、该类对应的java.lang.class对象没有被任何地方引用。

垃圾回收算法
标记-清除算法
复制算法

标记 - 复制算法大多用于新生代。实际上,新生代中的对象大多数都熬不过第一轮收集,因此不需要按 1:1 的比例来划分新生代的内存空间。Eden,Survivor的大小比例为8:1:1,当Survivor大小不够使用时,需要从老年代借用内存。

标记-整理算法

标记 - 复制算法不适合用在对象存活率高的区域,而且会浪费一半的空间,因此老年代一般不采用这种算法,取而代之的是有针对性的标记 - 整理算法。标记 - 整理算法的标记过程与标记 - 清除算法一样,但后续步骤不是直接清理可回收对象,而是让所有存活对象都向内存空间的一侧移动,然后直接清理掉边界以外的内存.是否移动回收后的存活对象是一项优缺点并存的风险决策,尤其是在老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新其引用将会是一个极为繁重的操作,必须暂停用户应用程序线程才能进行,像这样的停顿行为被称为“Stop the World”。但如果不考虑移动存活对象,又会影响内存分配和访问的效率,为此使用者必须小心权衡其中的得失。一种和稀泥式的解决方案就是让虚拟机平时采用标记 - 清除算法,直到内存空间碎片化程度大到影响对象分配时,再采用标记 - 整理算法收集一次,已获得规整的内存空间

分代收集算法

根据不同的生命周期采用不同的回收算法;新生代大都采用复制算法,老年代一般采用标记清除算法或者标记整理算法。

HotSpot算法实现
枚举根节点

从可达性分析中从GC Roots节点找引用为例,可作为GC Roots的节点主要是全局性的引用与执行上下文中,如果要逐个检查引用,必然消耗时间。
另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里的“一致性”的意思是指整个分析期间整个系统执行系统看起来就行被冻结在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性就无法得到保证。这点是导致GC进行时必须暂停所有Java执行线程的其中一个重要原因。
由于目前主流的Java虚拟机都是准确式GC,做一档执行系统停顿下来之后,并不需要一个不漏的检查执行上下文和全局的引用位置,虚拟机应当有办法得知哪些地方存放的是对象的引用。在HotSpot的实现中,是使用一组OopMap的数据结构来达到这个目的的。

安全点

在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举,但可能导致引用关系变化的指令非常多,如果为每一条指令都生成OopMap,那将会需要大量的额外空间,这样GC的空间成本会变的很高。
实际上,HotSpot也的确没有为每条指令生成OopMap,只是在特定的位置记录了这些信息,这些位置被称为安全点(SafePoint)。SafePoint的选定既不能太少,以致让GC等待时间太久,也不能设置的太频繁以至于增大运行时负荷。所以安全点的设置是以让程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。
对于SafePoint,另一个问题是如何在GC发生时让所有线程都跑到安全点在停顿下来。这里有两种方案:抢先式中断和主动式中断。抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程来响应GC。
而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的另外再加上创建对象需要分配的内存的地方。

安全区域

使用安全点似乎已经完美解决了如何进入GC的问题,但实际情况却并不一定,安全点机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。但是程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。
安全区域是指在一段代码片段之中,你用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。
当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区,那样当在这段时间里,JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。当线程要离开安全区域时,他要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止。

垃圾回收器

新生代收集器:Serial、ParNew、Parallel Scavenge

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

整堆收集器: G1

几个相关概念:

并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

具体详见《深入了解java虚拟机》

内存分配与回收策略

1、对象优先在Eden区进行分配,空间不足,则进行一个Minor GC;Minor GC期间会尝试放到Survivor空间去;若Minor GC后空间仍不足,则会通过分配担保机制提前转移到老年代去。

2、大对象直接进入老年代:大对象是指需要连续内存空间的Java对象。最常见的就是很长的字符串以及数组。

3、长期存活的对象将进入老年代:虚拟机给每个对象定义了一个年龄计数器。如果对象在Eden出生,并且经过第一次Minor GC仍然存活,并且能够被Survivor容纳,那么年龄增加一岁,当年纪增加到一定值的时候,默认为15,就会被晋升为老年代。

4、动态对象年龄判定:为了更好的适应不同程序内存状况,虚拟机并不是永远的要求对象的年龄必须要达到maxTenuringThreshold才能晋升到老年代,如果Survivor空间相同年龄的所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可直接进入老年代。

5、空间分配担保:在发生Minor之前,虚拟机会查询老年代的连续空间是否大于新生代所有对象总空间,若是大于,则Minor可以确保是安全的。若是不大于,则虚拟机会查看HandlePromotionFailure设置值是否允许失败。如果允许,那么会继续检查老年代的可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试一次Minor GC,尽管这个GC是有风险的,如果小于,或者HandlePromotionFailure设置不允许冒险,那么会进行一次Full GC.

​ 什么是冒险行为?新生代使用复制收集算法,但为了内存利用率,只是用其中一个Survivor空间作为轮换备份,因此当出现大量对象在Minor GC后仍然存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接放入老年代哦。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

shichangle01

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值