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

一、如何判断对象是否还在存活

引用计数法:

主流的Java虚拟机没有使用这种方法管理内存, 因为它很难解决循环依赖

可达性分析:

通过一系列的称为”GC  Roots“的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链, 当一个对象到GC Roots没有与任何引用链相连时, 则证明该对象是不可用的。

作为GC Roots的对象包括以下几种:虚拟机栈中引用的对象、 方法区中类静态属性引用的对象、方法区中常量引用的对象以及本地方法栈中JNI引用的对象。

二、引用:

定义:

如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址, 就称这块内存代表着一个引用。

分类:

强引用:

代码之中普遍存在的, 如Object obj = new Object(), 只要强引用还在, GC就永远不会回收该内容。

软引用:

描述有用但非必须的对象。 对于软引用关联着的对象, 在系统将要抛出内存异常之前, 会将这些对象列进回收范围进行二次回收。 如果这次回收还没有足够的内存, 才会抛出异常。(SoftReference)

弱引用:

弱引用也用来描述非必须的对象。被若引用关联的对象只能活到下次垃圾回收发生之前。 当垃圾收集器工作时, 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象。

虚引用:

又称为幽灵引用或者幻影引用。 一个对象是否有虚引用的存在, 丝毫不会影响对象的生存时间, 也不能通过虚引用获得对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。

三、对象标记之后就会回收吗

可达性分析之后, 没有在任何GC Roots引用链上的对象, 就会被第一次标记, 标记之后还有一次筛选的过程;

筛选的条件是: 此对象是否有必要执行finalize方法,有必要执行的条件是:该对象覆盖了finalize方法, 并且没有虚拟机调用过, 也就是说任何一个对象的finalize方法都只会被系统执行一次。

如果有必要执行finalize方法, 该对象则将会被放置一个成为F_Queue的队列之中, 并在稍后由一个有虚拟机自动建立的、低优先级的Finalizer线程去触发该方法。但不会等待finalize执行结束。

finalize方法是对象逃脱死亡命运的最后一次机会。稍后GC将对F_Queue中的对象进行第二次小规模的标记, 如果能与引用链上任何一个对象建立关系, 对象就不会被再次标记, 从而活下来。

注:如果没有必要执行finalize对象, 是不是就会立即被GC 回收呢

四、方法区的回收

在堆中,尤其是新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间, 而永久代的垃圾收集效率远低于此。

永久代中的垃圾收集主要回收两部分内容: 废弃常量和无用的类

废弃常量: 当前系统中没有任何一个对象引用该常量。

无用的类: 该类所有的实例都已被回收(Java堆中不存在该类的任何实例)、加载该类的ClassLoader被回收、该类对应的java.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法。

五、垃圾收集算法

1、标记-清除算法:

首先是标记出所有需要回收的对象, 在标记完成后回收所有被标记的对象。

缺点:

效率问题: 标记和清楚两个过程的效率都不高

空间问题: 标记清除之后会造成大量不连续的内存碎片, 空间碎片太多导致需要分配较大对象时, 无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

2、复制算法:

描述:将可用内存按容量划分为两块, 每次只是用其中的一块, 当这一块内存用完了, 就将还存活的对象复制到另外一块内存, 然后再把已使用过的内存空间一次性的清理掉。

优点:不用考虑内存碎片

缺点:内存缩小为原来的一半

应用:目前的商业虚拟机都采用这种收集算法来回收新生代,具体如下:

将内存分为一块较大的Eden空间和两块较小的Survivior空间, 每次使用Eden和其中一个Survivior空间。

当回收时, 将Eden区和Survivior中还存活着的对象一次性的复制到另外一块Survivior空间上, 最后清理Eden区和另一块Survivior区。

Hotspot 虚拟机默认Eden和Survivior的大小比例是8:1

当另外一块Survivior空间没有足够的空间存放上一次新生代存活的对象时, 这些对象将直接通过分配担保机制进入老年代。

3、标记-整理算法:

标记之后, 让所有存活的对象都向一端移动, 然后直接清理掉端边界以外的内存。

4、分代收集算法:

当前虚拟机的垃圾收集都采用”分代收集“算法。一般是把Java堆分为新生代和老年代;

新生代: 每次垃圾收集时都会有大批对象死去, 只有少量存活, 该区域采用复制算法。

老年代:对象存活率较高, 而且没有额外空间对她进行分配担保, 就必须使用”标记-清理“或者”标记-整理“算法进行回收。

六、HotSpot 的算法

1、枚举根节点:

GC停顿:

可达性分析要确保在一个一致性的快照中进行, 确保分析过程中引用关系不再变化。 GC进行时, 必须停顿所有的Java执行线程。 Stop the World

如何枚举根节点:

GC Roots主要在全局性的引用, 如常量、类静态属性, 与执行上下文中, 如果逐个检查, 必然消耗大量的时间。

使用OopMap: HotSpot使用一组OopMap来记录对象的引用, 加快GC Roots的枚举。

2、安全点:

背景:          如果每个指令都生成对应的OopMap, 那会需要大量的额外空间, 这样GC的成本将会变得非常高。

解决办法:    只在特定位置记录对象的引用情况, 这些特点的位置我们称之为安全点。

安全点的选定条件:是否具有让程序长时间执行的指令 (原因)

如何保证GC时, 让所有线程都跑到最近的安全点上再停顿下来:

主动式中断:

GC需要中断线程的时候, 不直接对线程操作, 而是简单的设置一个标志, 而是在执行到安全点时轮训该标志, 如果标志为真就自己中断挂起。

抢先式中断:

GC发生时, 首先把所有的线程全部中断, 如果发现有线程中断的地方不在安全点上, 就恢复线程, 让他”跑“到安全点上。

现在几乎没有虚拟机采用这种方式来暂停线程以响应GC事件

3、安全区域:

背景: 安全点机制保证了程序执行时, 在不太长时间就会遇到可进入GC的安全点, 如果程序没有执行呢, 比如出于sleep或者blocked状态,

这时候线程无法响应jvm的中断请求, Jvm也显然不太可能等待线程被重新分配CPU时间。

安全区域:在一段代码之中, 引用关系不会发生变化, 在这个区域中任意地方开始GC都是安全的

实现: 当线程执行到安全区域后, 首先会标示自己进入了安全区域, 那样, 当在这段时间内发生GC时, 就不用管这样的线程了,当线程要离开

该区域时, 要检查系统是否已经完成了根节点枚举, 如果没完成, 它就必须等待直到收到可以安全离开安全区域的信号。

七,垃圾收集器

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/f69341f4702a42ee614df0d8f973eea9.html