垃圾回收需要完成三件事
(1)那些内存需要回收。
(2)什么时候回收。
(3)如何回收。
上篇文章深入理解JVM学习笔记-Java内存区域与内存溢出异常中介绍了Java内存运行时区域的的各个部分,其中程序计数器、虚拟机栈、本地方法栈3三个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈的操作,每个栈帧中分配多少内存基本上是类结构确定下来时已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内不需要过多的考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了,而Java堆和方法区不一样,这部分内存分配和回收都是动态,垃圾收集器所关注的就是这部分内存。
如何判断对象已死?
引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当解除引用时,计数器值就减1,引用计数器为0的对象就可以认为对象已死,可以进行内存回收。但是引用计数算反很难解决对象之间的互相循环引用问题。
可达性分析算法:以GC Roots对象为起始点进行搜索,如果有对象不可达,那么该对象就是垃圾对象,即使两个对象互相有引用关系,只要GC Roots是不可达的,那么这两个对象是可回收对象。
GC Roots对象包括以下几种:
(1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
(2)方法区中类静态属性引用的对象。
(3)方法区中常量引用的对象。
(4)本地方法栈中JNI(一般说的是Native方法)引用的对象。
Java四种引用
强引用(Strong Reference):就是指在程序代码之中普遍存在的,类似“Object Object = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用(Soft Reference):是用来描述一些还有用但并非必须的独享,对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象进行垃圾回收。如果回收之后还没有足够的内存,才会抛出内存溢出异常。
弱引用(Weak Reference):也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器进行垃圾时,无论内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用(Phantom Reference):是最弱的引用关系,一个对象是否有虚引用的存在,完全不对对其生存时间构成影响。
对象生存还是死亡
可达性分析算法中不可达的对象,也并非非死不可,要真正宣告对象死亡,至少要经理两次标记过程:如果对象在进行可达性分析后,发现没有与GC Roots相连接的引用链,那么对象将会进行第一次标记。第二次标记就是判断对象有没有实现finalize()方法,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此,如果再次使用该方法,对象的自救行动是不会成功的了(JVM不建议在finalize()拯救对象)。
回收方法区
方法区内存在虚拟机中的永久代,Java虚拟机规范说过可以不要求虚拟机在方法区实现垃圾收集,而且方法区垃圾收集性价比一般比较低(永久代)。永久代垃圾收集主要回收两部分内容:废弃常量和无用类,回收废弃常量与回收Java堆中对象类似,判断常量是否是废弃常量比较简单,而要判定一个类是否是无用的类条件相对苛刻,类需要满足下面三个条件才能算无用的类:
(1)该类所有实例都已被回收,也就是Java堆中不存在该类的任何实例。
(2)加载该类ClassLoader已经被回收。
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足以上三个条件的无用类进行回收,这里说的仅仅是”可以“,而并不是和对象一样,不使用了就必然回收,是否对类进行回收,部分虚拟机提供了参数可以进行控制。所以对类的回收整体是比较难的。使方法区发生类导致的内存溢出基本思路:在运行时产生大量的类去填满方法区,也就是在运行时动态产生很多的类,直到方法区内存溢出。所以频繁动态产生很多类时,需要注意方法区内存溢出。
垃圾收集算法
因为垃圾算法的实现设计大量程序细节,各个平台的虚拟机操作内存的方法各不相同,因此在本文中我们主要讲讲垃圾收集算法,并不涉及具体实现。
标记清除算法
上图为标记-清除算法的示意图。算法分为标记和清除两个阶段,首先需要标记可回收的内存,然后再对可回收的内存进行回收。这样做的2个不足分别是:
(1)效率问题:标记和清除两个过程的效率都不高;
(2)空间问题:标记-清除之后产生大量不连续的空间碎片。
为了解决效率问题,复制算法出现了(适合新生代),为了解决内存碎片问题,标记-整理算法出现了(适合老年代)。
复制算法
上图是复制算法实现图。它将内存分为大小相等的2块,每次只是使用其中一块。当一块内存用完之后,就将存活的对象复制到另外一块内存区域并将本块内存清理。这样做的大大降低了内存空间使用率。我们的HotSpot的年轻代就是使用复制算法,只不过它的比例不是1:1,而是8:1。
标记-整理算法
如图是标记-整理算法示意图。相比标记-清除算法,不同的是整理过程。将存活对象移到一端,然后清除可回收对象。这样做的明显好处就是产生了连续的空间。
分代收集算法
基于上面的几种收集算法,当前商业虚拟机基本采用的都是分代收集。结合了复制和标记-整理的优势。一般做法是将Java堆分为新生代和老年代。由于新生代会不断产生新生对象,因此采用了复制算法;而年老代的对象存活率较高,因此采用了标记-整理算法。在新生代中,我们可以看到新生代=Eden+S0+S1;他们设计的默认比例是8:1:1;这个参数是可以通过虚拟机参数进行调整的。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,下图是基于java虚拟机的HotSpot虚拟机垃圾收集器。一共是7种收集器,我们分别进行介绍。
Serial/Serial Old
Serial最基本,发展历史最悠久的收集器。具体原理让我们看一张图最明了
上图是Serial和Serial Old 两种垃圾收集器的运行示意图。其中Serial 和Serial Old的区别就是一个是运行在年轻代一个是运行在年老代。从图中我们可以看到,他是一个单线程模式的垃圾收集器。这里不仅仅是说该收集器是使用单线程或者一个CPU去完成垃圾收集,更重要的是它在进行垃圾收集的时候必须暂停用户线程,直到收集完成,也就是Stop the World。
ParNew
ParNew收集器其实是Serial收集器的多线程版本。它也是运行在年轻代中,如图:
如图,在新生代中采用的是多线程模式进行垃圾收集同时也需要暂停用户线程直到垃圾收集完毕。这种模式和Serial相比,CPU数量越多的情况下优势更加明显。如果CPU数量很少,比如2个,那么这种收集效率可能比Serial更低,因为它存在线程交互的开销。
Parallel Scavenge/Parallel Old
Parallel Scavenge也是一个新生代,多线程收集器。那么这种收集器和之前的ParNew有什么区别呢?区别还是有的,不然怎么会出来这种收集器呢?其实Parallel Scavenge收集器的特点是它关注一个可控的吞吐量。那么什么是吞吐量,我这边也不做任何讲述,直接上一个公式大家就知道了。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);
是不是很明白?垃圾收集时间越短,吞吐量就是越大。这里就有一个问题:垃圾收集时间越短,一般来讲收集的垃圾量就是越少,也就是回收的内存量越小,那么总内存一定的情况下,我们在一定时间内回收的次数就是越多。这就需要我们控制好回收时间来制约回收次数了。
同时,这里也有一个Parallel Old收集器,顾名思义是Parallel Scavenge收集器的年老代版本。
如下图:
CMS
CMS(Concurrent Mark Sweep),从名字我们知道这是一款基于并发使用标记清除算法的垃圾收集器。他是一款以获得最短回收停顿时间为目标的收集器。让我们赶紧来看看他的实现吧。上图
如上图,它分为以下步骤:
(1)初始标记:仅仅标记GC Roots能直接关联到的对象,时间很短,阻塞用户线程
(2)并发标记:标记可回收对象,和用户线程并行。
(3)重新标记:标记在并发阶段因用户线程继续运行产生的可回收对象,修正并发标记,此时是阻塞用户线程。
(4)并发清理:使用标记-清除算法将垃圾进行清理。
这种收集算法存在3个缺点:
(1)对CPU资源敏感。一般并发执行的程序对CPU数量都是比较敏感的
(2)无法处理浮动垃圾。在并发清理阶段用户线程还在执行,这时产生的垃圾无法清理。
(3)由于标记-清除算法产生大量的空间碎片。
G1
G1,Garbage-First是当今收集器技术发展的最前沿成果之一,它是一款面向服务端的垃圾收集器。
实现思路:将整个java堆划分为多个大小相等的独立区域(Region),G1跟踪各个Region里面的垃圾堆积和价值大小,在后台维护一个优先列表,每次进行优先回收。那么Region不是孤立的。那么如何避免全堆扫描呢?
G1使用Remembered Set。每一个Region对应一个Remembered Set,在虚拟机对Reference类型的数据进行写操作的时候,会检查Reference引用的对象是否处于不同的Region中,如是则记录到Remembered Set中。
G1收集器的运作大致分为以下几个步骤:
(1)初始标记:标记GC Roots能直接关联到的对象,耗时短
(2)并发标记:找出存活的对象,耗时长
(3)最终标记:修正并发标记
(4)筛选回收:根据用户所期望的GC停顿时间来制定回收计划
以上就是垃圾收集器体系以及运行原理,Java自动内存管理可以归结为自动化解决了两个问题,给对象分配内存以及回收分配给对象的内存,下面我们来一起探讨内存分配策略。
内存分配策略
内存分配策略主要有:
(1)对象优先在新生代Eden分配,当Eden区没有足够空间进行分配时,虚拟机发起一次Minor GC(指发生在新生代的垃圾收集动作)。
(2)大对象直接进入老年代。
(3)长期存活的对象直接进入老年代,在新生代Eden区多次GC未被回收的对象认为是长期存活的对象。
(4)动态对象年龄判定,多年年龄主要是指对象经过多少次GC,根据设置的对象年龄让对象进入老年代,但是对象年龄可以动态设置,以让对象进入老年代。
(5)空间分配担保:在进行GC前,虚拟机先检查老年代最大连续可用连续空间是否大于新生代所有对象总空间,以确保Minor GC是安全的。空间分配担保主要是保证进行GC后,有足够的空间可以存放已有对象。
以上就是JVM垃圾收集器与内存分配策略相关内容,部分内容直接从小腊月虚拟机相关文章直接拷贝而来(节省打字时间)。
JVM学习资料
《深入理解Java虚拟机》
Java虚拟机原理图解系列文章
小腊月虚拟机相关文章