我所知道的-Java垃圾回收机制
本文章你能知道的内容:
- Java内存模型
- Java堆
- 对象什么时候回收
- 垃圾回收算法
- 垃圾回收器
Java内存模型
要了解Java垃圾回收机制,首先知道Java内存模型是怎么样的。如下图
Java内存模型
- 程序计数器
- Java 虚拟机栈
- 局部变量表:是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编译期可知的各种基本数据类型、对象引用(reference)和 returnAddress 类型(它指向了一条字节码指令的地址)。
- 操作数栈:又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了。
- 动态连接:每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
- 方法返回地址:当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。
- 本地方法栈:该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。
- 方法区
方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 - Java 堆:一般new的对象实例和数组都是在堆中的,而GC主要回收的内存也是这块堆内存,后面单独领出来来讲。
- 直接内存
不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域(Fresco图片加载库,就是运行这块内存超越虚拟机内存使用)。
Java 堆
Java Heap 是 Java 虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。几乎所有的对象实例和数组都在这类分配内存。
Java 堆内存由垃圾回收器的自动内存管理系统管理。
堆内存分为两大部分:新生代和老年代,默认比例为1:2,可修改。
老年代主要存放应用程序中生命周期长的存活对象。
新生代又分为三个部分:一个Eden区和两个Survivor区,默认比例为8:1:1,可修改。
Eden区存放新生的对象。
Survivor存放每次垃圾回收后存活的对象。
堆内存
为什么?
- 为什么要分新生代和老年代?
- 新生代为什么分一个Eden区和两个Survivor区?
带着这些疑问,我们就要先了解堆相关的垃圾回收算法和采用的垃圾回收器。在讲算法前先了解怎么判断对象是否回收。
对象什么时候回收?
判断对象是否回收主要有以下两种算法
-
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法- 优点:简单,高效,Objective-c用的就是这种算法。
- 缺点:很难处理循环引用,比如图中相互引用的两个对象则无法释放。但是也有解决办法,想知道自行搜索。
-
可达性分析算法(根搜索算法)
为了解决上面的循环引用问题,Java采用了一种新的算法:可达性分析算法。
从GC Roots(每种具体实现对GC Roots有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。
可达性分析算法
Java定义的GC Roots对象:
虚拟机栈(帧栈中的本地变量表)中引用的对象。
方法区中静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI引用的对象。
如果出现循环引用了,只要没有被GC Roots引用了就会被回收,完美解决!
垃圾回收算法
知道了什么时候回收对象,那我们再看具体垃圾回收器有哪些垃圾回收算法。
-
标记清除法
标记-清除法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
标记清除法- 优点:是简单,容易实现。
- 缺点:容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
-
复制算法
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
复制算法- 优点:实现简单,运行高效且不容易产生内存碎片,适用于存活对象很少。回收对象多
- 缺点:内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半,如果存活对象很多,那么Copying算法的效率将会大大降低。
-
标记整理算法
该算法标记阶段和标记清除法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
标记整理算法- 优点:不会出现内存碎片问题,适用于存活对象多,回收对象少的情况使用
- 缺点:整理时间长,容易导致卡顿。
-
分代回收算法
分代回收算法其实不算一种新的算法,而是根据复制算法和标记整理算法的使用场景综合使用。
复制算法:适用于存活对象很少。回收对象多
标记整理算法: 适用用于存活对象多,回收对象少
分代算法就是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Old Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收所以采用标记整理法。而新生代的特点是每次垃圾回收时都有大量的对象需要被回收所以采用复制算法(改良的复制算法,不是按1:1分配)。
分代回收算法- Eden空间和两块Survivor空间的工作流程
// 分配了一个又一个对象
放到Eden区
// 不好,Eden区满了,只能GC(新生代GC:Minor GC)了
把Eden区的存活对象copy到Survivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor A区的存活对象copy到Survivor B区,然后清空Eden区和Survivor A区
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor B区的存活对象copy到Survivor A区,然后清空Eden区和Survivor B区
// ...
// 有的对象来回在Survivor A区或者B区呆了比如15次,就被分配到老年代Old区
// 有的对象太大,超过了Eden区,直接被分配在Old区
// 有的存活对象,放不下Survivor区,也被分配到Old区
// ...
// 在某次Minor GC的过程中突然发现:
// 不好,老年代Old区也满了,这是一次大GC(老年代GC:Major GC)
Old区慢慢的整理一番,空间又够了
// 继续Minor GC
// ...
// ...
在回答上面提出的问题?
为什么要分新生代和老年代?
综合使用算法,最优采用分代算法所以分为新生代和老年代两块区域。具体为什么1:2?应该是根据实践测试得出的结果,也可以调整。
-
回答第二个问题为什么新生代内存需要有两个Survivor区,这里讲的比较详细
-
什么时候触发GC?
GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。
GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发GC操作来释放内存。
GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。
GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。
垃圾回收器
在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下图(图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用)
垃圾回收器
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- CMS收集器(应用非常广泛)
- G1收集器(面向服务端应用)