初识JVM垃圾回收

来源:互联网 发布:php 数组转换字符串 编辑:程序博客网 时间:2024/05/16 07:10

这段时间有些悠闲,所以可以安心的更新一波知识,今天简单的记录一下JVM中的垃圾回收和内存分配策略

前面我们讲到Java堆、方法区是共享的,而程序计数器、虚拟机栈、本地方法栈都是线程私有的,所以基本上是不存在垃圾回收的,所以接下来讨论的东西都是基于Java堆和方法区

对象存活判定算法

判断对象是否存活一般有引用计数算法、可达性分析算法

1、引用计数算法(python似乎使用这种算法判断的)

给对象中添加一个引用计数器,每当有一个地方引用它时,,计数器值就加1,当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

但是因为引用计数算法存在漏洞:即两个对象互相引用时,GC收集器就无法回收它们。所以一般主流的JVM都选择了后者:可达性分析算法

2、可达性分析算法

这里写图片描述

基本思想

通过一系列称为“GC Roots”对象作为起始点,从这些节点开始向下搜索,搜索走过的路劲称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。图中的object5,object6,object7就是可回收的对象

在Java语言中,可作为GC Roots的对象包括如下几种:
1、虚拟机栈中引用的对象
2、方法区中类静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中JNI(即Native方法)引用的对象

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

JDK1.2后,如今Java对引用的概念扩充为:强引用(不会被回收)、软引用(第二次会回收)、弱引用(第一次就会被回收)、虚引用(仅仅只是一个对对象的被回收的一个系统通知)

垃圾若回收至少会被2次标记

第一次:

  • 没有必要执行回收的:对象没有覆盖finalize方法或者finalize方法已经被JVM调用过了
  • 有必要执行回收的:对象将会被放置在一个叫做F-Queue队列中,并且JVM会自建一个低优先级的finalizer线程区 执行 finalize方法。(可能会因为一些原因执行失败)

第二次:

  • 这是对象最后的一次机会,若对象想实现自救,不想被回收,则可重新与引用链上的任何对象建立关联,并可移出队列。

注意: 任何一个对象的finalize方法都只会被系统调用一次。

自救举例:

package javaDemo;public class saveDemo {    public static saveDemo SAVE_HOOK=null;    public void isAlive(){        System.out.println("YES I am still alive");    }    protected void finalize() throws Throwable{        super.finalize();        System.out.println("finalize method executed");       saveDemo.SAVE_HOOK=this;    }    public static void main(String []args)throws Throwable{        SAVE_HOOK=new saveDemo();//        第一次自救        SAVE_HOOK=null;        System.gc();        Thread.sleep(500);//因为finalize方法是低优先级线程,所以暂停0.5秒等待它        if(SAVE_HOOK!=null)            SAVE_HOOK.isAlive();        else            System.out.println("no I'm dead :(");//        第二次调用finalize方法        SAVE_HOOK=null;        System.gc();        Thread.sleep(500);//因为finalize方法是低优先级线程,所以暂停0.5秒等待它        if(SAVE_HOOK!=null)            SAVE_HOOK.isAlive();        else            System.out.println("no I'm dead :(");    }}

这里写图片描述

由结果可以看出,finalize方法只会被同一对象调用一次,所以第二次自救失败。

回收方法区

方法区虽然是HotSpot虚拟机中的”永久代“,但是也是有垃圾存在的,

主要有:

  • 废弃的常量,常量池中没有任何地方引用的常量
  • 无用的类(Java堆中不存在该类的实例,加载该类的ClassLoader已被回收,该类所对应的java.lang.class对象没被引用,无法反射)

以上对象可以被回收,但”永久代“的垃圾回收效率很低,所以不一定会被回收掉,所以叫可以而不是必然被回收。

垃圾收集算法

1、标记-清除算法

利用可达性分析算法判断对象是否存活,不存活则标记,然后在第二次标记中被清除掉

最基础的算法,不足之处是:效率不高、产生大量内存碎片

2、复制算法

现在一般是将内存分为Eden:survivor:survivor=8:1:1,每次只使用Eden和一个survivor,当回收时,将还存活的对象一次性复制到另一个survivor,然后清理掉Eden和survivor空间,若复制到另一个survivor空间不够,则需要依赖其他内存(一般指老年代)分配担保。

复制算法一般用于新生代上

3、标记-整理算法

标记过程和”标记-清理“算法一样,但后续不是直接堆可回收对象进行整理,而是让所有存活对象都移向一端,然后直接清理掉端边界以外的内存。

标记-整理算法一般用于老年代上

4、分代收集算法

当前商业虚拟机都采用”分代收集“算法,一般将Java堆分为新生代和老年代,新生代每次都有大批对象死去,只有少量存活,可选用复制算法;老年代对象存活率高且没有额外空间对它进行分配担保,必须使用”标记-清理“算法或者“标记-整理”算法。

HotSpotJVM的算法实现

stop the world 事件:可达性分析必须在一个一致性的快照中进行:一致性是指整个分析期间整个执行系统看起来想被冻结在了某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。
OopMap数据结构: 在类加载完成时候,HotSpot就把对象内的偏移量上的数据计算出来,在JIT编译过程中也会在特地的位置记录下栈和寄存器中哪些位置的引用。
安全点: 安全点的选定基本上是以“是否具有让程序长时间执行的特征”为标准,长时间执行的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等这些功能的指令才会产生Safepoint。
如何让GC发生时所有线程(不包括JNI调用的线程)都“跑”到最近的安全点上再停顿下来,两种方案:
抢先式中断(GC发生时,所有线程全部中断,若中断的线程不在安全点上则恢复中断继续让它跑到安全点)、
主动式中断(设置一个简单的标志,让各个线程主动区轮询这个标志,发现标志为真则挂起,否则继续跑)
安全区: 安全点的扩展,在这个区域内GC安全,因为引用关系不改变

垃圾收集器

这里写图片描述
Serial收集器(新生代、单线程、复制算法)

Serial收集器是最古老的收集器,它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即stop the world。到现在为止,它依然是虚拟机运行在client模式下的默认新生代收集器,与其他收集器相比,对于限定在单个CPU的运行环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程收集效率。

Serial Old收集器(老年代、单线程、标记-整理算法)

是Serial收集器的老年代版本,它同样是一个单线程收集器,使用”标记-整理“算法。这个收集器的主要意义也是被Client模式下的虚拟机使用。在Server模式下,它主要还有两大用途:一个是在JDK1.5及以前的版本中与Parallel Scanvenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。

ParNew收集器(新生代、多线程、复制算法)

ParNew收集器是Serial收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会stop the world,只是相比较Serial收集器而言它会运行多条进程进行垃圾回收。

Parallel Old收集器(老年代、多线程、标记-整理算法)

是Parallel Scavenge收集器的老年代版本,采用多线程和”标记-整理”算法。”吞吐量优先“收集器,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

Parallel Scavenge收集器(新生代、多线程、复制算法)

Parallel是采用复制算法的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

CMS收集器(老年代、并发执行、标记-清除算法)

CMS(Concurrent Mark Swep)收集器是一个比较重要的回收器,现在应用非常广泛,我们重点来看一下,CMS一种获取最短回收停顿时间为目标的收集器,这使得它很适合用于和用户交互的业务。从名字(Mark Swep)就可以看出,CMS收集器是基于“标记-清除”算法实现的。
它的收集过程分为四个步骤:

初始标记(initial mark)
并发标记(concurrent mark)
重新标记(remark)
并发清除(concurrent sweep)

注意初始标记和重新标记还是会stop the world,但是在耗费时间更长的并发标记和并发清除两个阶段都可以和用户进程同时工作。

此外除了CMS的GC,其实其他针对old gen的回收器都会在对old gen回收的同时回收young gen。

G1收集器(老年代、并发与并行、分代收集、整体上看是标记-整理算法、局部是复制算法)

G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

并行与并发: G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。
分代收集: 和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆。
空间整合: G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。
可预测的非停顿: 这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。回收时,后台维护一个优先列表,每次根据允许的收集时间优先回收价值最大的Region。

在GC根节点枚举范围内加入Remembered Set 即可保证不对全堆扫描。

工作流程

初始标记标记一下GC Roots能直接关联到的对象
并发标记 从GC roots 到堆对象进行可达性分析算法
最终标记 修正用户程序继续运行而导致标记变动的那部分,最终整合进Remembered Set
筛选回收 首先堆各个Region回收价值成本排序,根据用户期望的GC停顿时间制定回收计划

虽然G1看起来有很多优点,实际上CMS还是主流。

内存分配和回收策略

Eden:survivor:survivor=8:1:1
新生代垃圾回收(Minor GC)
老年代垃圾回收(Marjoc GC/Full GC)

大对象(需要大量连续内存空间的Java对象,例如很长的字符串或者数组)直接设置进入老年代。

-

长期存活对象将进入老年代


若在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代了

-

空间分配担保:发生minor GC前,检查老年代最大可用连续空间是否大于新生代中所有对象总空间或者历次晋升到老年代对象的平均大小,如果大于,则比较安全继续minor GC,否则进行 Full GC。

小结

再次感谢《深入理解Java虚拟机(第2版)》,这本书实在是太棒了,内容生动,浅显易懂,十分适合了解JVM中的相关知识,努力汲取中ing…