java 垃圾回收机制
GC即垃圾收集机制是指JVM用于释放那些不再使用的对象所占用的内存。
Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。
垃圾回收重点关注的是堆和方法区部分的内存
JVM因为需要执行GC的执行,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成
jvm内存
jvm启动时进行一系列的工作,其中一项就是开辟一块运行时内存。而这一块内存中又分为了五大区域,分别是程序计数器、虚拟机栈、本地方法栈、方法区、直接内存。
-
程序计数器
记录程序运行的下一条指令的地址。
-
VM Stack
用于存储局部变量表、操作数栈、动态链接、方法出口。
方法开始调用时,会创建栈帧并入栈,方法执行结束时会出栈。每个线程都有自己的栈。
本地方法栈
-
堆
堆是用于存放对象实例的地方。
堆是线程共享的,这是多线程时同步机制的原因。
堆是GC管理的主要区域。
-
方法区
方法区也为所以线程所共享,用于存放已加载的类信息、静态变量、常量和即时编译器编译后的代码。
垃圾回收算法
1.引用计数法
在这种方法中,堆中的每个对象实例都有一个引用计数。
开始:
当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为1。
计数加1:
当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数加1(classA a2=a1; 则a2引用的对象实例的计数器加 1)。
计数减1:
- 但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减1。(a1/a2超过生命周期或者a2=a3;)
- 当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减1。(对象实例中有其他的对象作为成员变量或在方法中出现其他类对象)
- 任何引用计数为0的对象实例可以被当作垃圾收集。
引用计数收集器可以很快的执行,并且交织在程序运行中,对程序需要不被长时间打断的实时环境比较有利,但其很难解决对象之间相互循环引用的问题。
2.根搜索算法
程序把所有引用关系看作一张图,从一个节点GC ROOT 开始,寻找对应的引用节点,搜索所走过的路径称为引用链,找到这个节点后,继续寻找这个节点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即该对象不可达,垃圾收集器将回收其所占的内存。
-
在java语言中,可作为GC Root的对象包括以下几种对象:
a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
b.方法区中的类静态属性引用的对象。
c.方法区中的常量引用的对象。
d.本地方法栈中JNI本地方法的引用对象(Native对象)。
java方法区被称为永久代,java虚拟机规范也没有对该部分内存的垃圾收集做规定,但是方法区中的废弃常量和无用的类还是需要回收以保证永久代不会发生内存溢出。
-
判断废弃常量的方法:
如果常量池中的某个常量没有被任何引用所引用,则该常量是废弃常量。
-
判断无用的类:
(1).该类的所有实例都已经被回收,即java堆中不存在该类的实例对象。
(2).加载该类的类加载器已经被回收。
(3).该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射机制访问该类的方法。
3.标记-清除算法
算法分为“标记”和“清除”两个阶段:该算法首先从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收。
优点:
不需要进行对象的移动,仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效
缺点:
直接回收不存活的对象,并没有对还存活的对象进行整理,因此会导致内存碎片。空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
4.复制算法
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。所以复制算法的使用场景,必须是对象的存活率非常低才行,比如新生代。而且最重要的是,造成了50%内存的浪费。
5.标记整理算法
标记-整理算法采用 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。
标记-整理 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。
复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。所以JVM为了优化内存的回收,使用了分代回收的方式,对于新生代内存的回收主要采用复制算法。而对于老年代的回收,大多采用标记-整理算法。
6.分代收集算法
- 新生代:
绝大多数最新被创建的对象都会被分配到这里,新生代使用复制算法和标记-清除算法。
新生代分为Eden,Survivor from和Survivor to三部分,占新生代内存容量默认比例为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和原来非空白的Survivor进行标记-清理回收。(也就是说对象在刚刚被创建之后,是保存在Eden的,Eden 回收后存活的保存在其中一个非空的Survivor,当这个Survivor满了,就对它回收,将存活的保存在另一个空的Survivor,并把这个原来非空的Survivor一次性清空,那些长期存活的对象会经由Survivor转存到老年代空间。)
例外:在Survivor 空间不足的情况下,对于一些需要分配一块比较大的连续内存空间的对象直接进入到老年代。
对象从这个区域消失的过程我们称之为”minor GC“。
- 老年代:
对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。这个区域分配的空间要比新生代多,默认的新生代、老年代所占空间比例为 1 : 2 。因为老年代有相对大的空间,所以发生在老年代的GC次数要比新生代少得多。
老年代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在老年代中使用标记-整理算法回收。回收次数相对比较少,每次回收的时间也比较长。
老年代中存在一个 card table。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,需要查询 card table 来决定是否可以被回收,而不用查询整个老年代。
对象从老年代中消失的过程,我们称之为”major GC“(或者”full GC“)
-
永久代(也称之为 方法区):
用于保存类常量以及字符串常量,是被各个线程共享的内存区域。这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC,永久代使用标记-整理算法进行垃圾回收在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:1) 所有实例被回收 2) 加载该类的ClassLoader 被回收 3) Class 对象无法通过任何途径访问(包括反射)
发生在这个区域上的GC事件也会被算为major GC。
引用的种类
- 强引用
就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类引用。 只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用
用来描述一些还有用,但并非必需的对象。 SoftReference类表示软引用,对于被软引用关联的对象,在系统将要发生内存溢出时,会把这些对象列入回收范围后,进行二次回收。 - 弱引用
用来描述非必需对象的,但是它的强度比软引用更弱一些,WeakReference类表示弱引用,对于被弱引用关联的对象,只能生存到下一次垃圾回收发生之前。 - 虚引用
是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。PhantomReference类表示虚引用,虚引用不对关联的对象的生存时间构成影响,也无法取得对象实例,它唯一的作用是在对象被GC回收是收到一条系统通知。
垃圾收集器
1、串行垃圾回收器
串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。
通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
2、并行垃圾回收器
并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候
3、并发标记扫描垃圾回收器
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
- 当标记的引用对象在tenured区域;
- 在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
4、G1垃圾回收器
G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器
- Serial收集器:
采用复制算法;
新生代收集器;
单线程的收集器;
但它的单线程的意义不仅仅说明它会只使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束
- ParNew 收集器:
采用复制算法;
新生代收集器;
Serial收集器的多线程版本;
并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
并发:指用户线程与垃圾收集线程同时执行(不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上
- Parallel Scavenge
使用复制算法;
新生代收集器;
并行的多线程收集器。
吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
Serial Old 收集器:
使用标记整理算法;
Serial收集器的老年代版本;
单线程收集器;Parallel Old 收集器:
标记整理算法;
Paraller Seavenge收集器的老年代版本;
使用多线程;
- CMS收集器:
标记清除算法;
老年代收集器;
并发标记并发清除;
- G1收集器:
它是一款面向服务器应用的垃圾收集器
1.并行与并发:利用多CPU缩短STOP-The-World停顿的时间
2.分代收集
3.空间整合:不会产生内存碎片
4.可预测的停顿
运作方式:初始标记,并发标记,最终标记,筛选回收
在 JDK1.7之前,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;
从 JDK1.7 开始 HotSpot 开始移除永久代。其中符号引用(Symbols)被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。
在 JDK1.8 中,永久代已完全被元空间(Meatspace)所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
一些Java编码的建议
根据GC的工作原理,我们可以通过一些技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。以下就是一些程序设计的几点建议。
最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域后,自动设置为 null.我们在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组,队列,树,图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC 回收它们一般效率较低。如果程序允许,尽早将不用的引用对象赋为null.这样可以加速GC的工作。
尽量少用finalize函数。finalize函数是Java提供给程序员一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。
如果需要使用经常使用的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。
注意集合数据类型,包括数组,树,图,链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象,造成内存浪费。
当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。使用增量式GC可以缩短Java程序的暂停时间。