jvm垃圾回收器介绍
上篇文章中我们讨论了jvm的内存区域,这篇文章我们来讨论针对的内存区域的垃圾回收机制。
其实针对垃圾回收我们通常考虑三个问题:1.哪些内存需要回收?2.什么时候回收?3.如何回收?下面我们针对这三个问题逐一解释
垃圾回收器回收哪些内存
通过对jvm的内存区域这篇文章的了解,我们知道程序计数器、Java虚拟机栈、本地方法栈都是属于线程私有的,并且都是随线程而生,随线程而死的;栈中的栈帧随着方法的进入和推出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会有JIT编译器进行一些优化,但是答题上可以认为是在编译期就是可知的),因此这几个区域的内存分配和回收都是具备确定性的,在这几个区域内就不需要过多的考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。但是,Java堆和方法区就不一样了,一个接口中的多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也是可能不一样的,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
特别要注意方法区的回收,Java虚拟机规范中确实说过可以不要求虚拟机的方法区进行垃圾回收,但是不是说方法区不能回收,方法区的回收主要包含两部分内容:废弃的常量和无用的类。回收废弃的常量与回收Java堆中的对象非常的类似。以常量池中的字面量回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说。就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这个时候发生内存回收,并且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
而无用类的判断条件就要苛刻的多需要同时满足以下三个条件才可以:
1.该类所有的实例都已经被回收,也就是java堆中不存在该类的认识实例
2.加载该类的ClassLoader已经被回收
3.改类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法。
虚拟机可以对满足上述三个条件的无用类进行回收,这里说的仅仅是可以,而并不是和对象一样,不适用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,当然还有其他一些参数可对类的加载和卸载进行控制。
如何判断jvm中对象是否存活
上面我们分析了垃圾回收器索要回收的内存区域,但是这些内存区域中哪些数据是可以回收的?当然是被jvm当作垃圾的数据也就是没有被用到的数据。哪如何判断一个数据是不是“垃圾”呢?下面我们来讲一下判断算法:
- 引用计数算法
所谓引用计数法就是给对象中添加一个引用计数器,每当一个地方引用它时,计数器的值就+1,引用失效时,计数器的值就减1;任何时刻计数器为0的对象就是没有被引用的对象就是可以被回收的。
客观的说,引用计数算法的实现简单,判定效率也很高,在大部分情况下都是一个不错的算法,很多著名的公司一些技术使用就是引用技术法。但是,在主流java虚拟机里面并没有选用引用计数器算法来管理内存,其中一个重要原因就是很难解决对象之间的相互循环引用的问题。
- 可达性分析算法
这个算法的基本思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走的路径称为引用链,当一个对象到GC Roots没有任何引用链时,则证明此对象时不可用的。
在主流的商用语言中使用的都是可达性分析算法,例如:java,C#。
在java语言中,可作为GC Roots的对象包括一下几种:
1.Java虚拟机栈(栈帧中的本地变量表)中引用对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(即一般说的native方法)引用的对象
注意,一个对象在通过可达性分析后确认没有引用链的情况下就一定会被回收吗?
答案是否定的。因为确认一个对象是否真的死亡需要进行两次标记。一个对象在进行可达性分析后发现没有和GC Roots相连接的引用链,那他将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖 finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果一个对象被判定有必要执行finalize()方法,那么这个对象将会放置到一个叫做F-Queue的队列中,并在稍后由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的执行就是虚拟机会触发这个方法,但并不承诺会等待他们执行结束,原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很有可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()是对象逃脱死亡的最后一次机会,稍后Gc会对F-Queue中的对象进行第二次的小规模标记,入宫对象在finalize()中成功拯救了自己(只要重新与引用链上的如何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成功变量),那么在第二次标记是它将被溢出“即将回收”的集合;如果对象这个时候仍然没有逃脱,哪基本上他就真的被回收了。
引用的介绍
JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据中存储的数值戴白哦的时另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很狭隘,无法覆盖这类对象:当内存空间还足够时,则保留在内存中,如果内存空间在进行垃圾收集后还是很紧张,则进行回收。所以有了后面堆引用概念的扩充,将引用分为:强引用,软引用,弱引用,虚引用四种。
- 强引用
指的是程序代码中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用
这类引用用来描述一些还在用但是并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收仍然没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
- 弱引用
用来描述非必须的对象,但是它的轻度比软引用更弱一些,被弱引用的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论当时内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2后,提供了weakReference类来实现弱引用
- 虚引用
也称为幽灵引用或者幻影引用。它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会堆器生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2后,提供了PhantomReference类来实现。
垃圾收集的算法
- 标记-清除算法
所谓标记清除算法顾名思义就是包含两个阶段,标记和清除。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程起始就是上面我们在可达性分析讨论时说的两次标记的过程。
标记清除算法是最基础的算法,其他的算法都是在此基础上对其不足进行改进而得到的。
缺点:
1.效率问题,标记和清除的效率都不高
2.空间问题,标记清除之后会产生大量部连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配比较大对象时,无法找到足够的连续的内存而不得不提前触发另一次垃圾收集动作。
- 复制算法
这种算法是为了解决效率问题,它将可用的内存按容量划分为大小相等的两块,每次只是用其中的一块。当一块内存用完了,就将还存活的对象复制到另一块上面,然后把使用过的内存空间一次性清空。这样使得每次都对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行搞笑。
缺点:将内存使用缩小为原来的一半,未免代价太高了
目前商业虚拟机都采用这种收集算法来回收新生代的内存。但是通过商业研究发现,新生代98%的的对象都是“朝生夕死”的,所以不用按1:1的比例来划分内存空间,而是将内存划分为一个较大的Eden空间和两个较小的Survivor空间,每次使用Eden空间和一个Survivor空间。当回收时,将Eden和Survivor中还存活的休想一次性复制到另一个Survivor上,最后清理掉Eden和刚才使用过的Survivor空间,HotSpot虚拟机默认Eden:Survivor=8:1,也就是说每次使用90%的空间,只有10%的空间浪费掉,但是我们无法保证每次回收Eden和Survivor空间都只有不多于10%的对象存活,当Survivor空间不足时,我们需要一来其他内存(这里指老年代)进行分配担保。
大致思路是,如果一个Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。下面在讲垃圾收集器执行规则时再详细讲。
- 标记-整理算法
复制收集算法再对象存活率较高的时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记-整理算法,标记过程仍然与“标记-清除”算法一样,但是后面的步骤不是直接对可回收对象进行清理,而是让所有皴获得对象都向一端移动,然后直接清理掉端边界以外的内存。
- 分代收集算法
当代商业虚拟机的垃圾收集都采用“分代收集“算法,这种算法没有什么特别的思想,只是根据对象存活周期的不通将内存划分为几块。一般是把java堆分为新生代合老年代,这样就可以根据各个年代的特点选择最适合的收集算法。在新生代,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中,因为对象存活率高、没有额外的内存对它进行担保,就必须使用“标记-清理”或者“标记-整理”算法进行回收。
垃圾收集器分类
这里讨论的收集器仅仅是JDK1.7之后HotSpot虚拟机所提供的,不同的虚拟机所使用的收集器是不同的,因为java虚拟机规范中对垃圾收集器该如何实现并没有任何规定。如下图:
如果两种收集器之间有连接线,说明他们可以配合使用。需要说明的是之所以会有这么多的收集器就是因为没有一个是万能的、放之四海而皆准、任何场景下都适用的完美收集器存在,换句话说就是这些收集器没有最好,只有最适合不同的场景。
- Serial收集器:新生代收集器
此收集器是最基本的、历史发展最悠久的收集器,再Jdk1.3之前是虚拟机新生代收集的唯一选择。它是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个cpu或者一条收集线程区完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。这一点在现在的实际应用中是很难忍受的。
它是使用复制算法的收集器。
但是它也有优点就是简单高效(与其他收集器的单线程相比)。对于限定单个cpu的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。
- ParNew收集器 :新生代收集器
ParNew收集器其实是Serial收集器的多线程版本,其他与Serial收集器相比并没有太多创新之处,很多配置参数和Serial是公用的,Serial的收集算法、stop the world、对象分配规则、回收策略等都是一样的。但是即便如此,它确实许多运行在Server模式下的虚拟机中首选的新生代收集器,其中最主要的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。
它是使用复制算法的收集器。
ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后得默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。同时可以使用-XX:ParallelGCThreads参数来限制垃圾收集得线程数,如果不适用此参数,默认开启得线程数和cpu数量相同。
- Parallel Scavenge收集器:新生代收集器
Parallel Scavenge收集器是新生代收集器,他也是用复制算法得收集器,又是并行得多线程收集器。
Parallel Scavenge收集器得特点是它关注得点与其他收集器不通。CMs等收集器得关注点是尽可能得缩短垃圾收集时用户线程得停顿时间,而Parallel Scavenge收集器得目标则是达到一个可控制得吞吐量。所谓吞吐量就是CPU用于运行用户代码得时间与CPU总消耗时间得比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。同时还提供过一个参数-XX:+UseAdaptiveSizePolicy需要特别注意,这是一个开关参数,打开后,就不需要手动指定新生代大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio)。晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态的设置这些参数以适应最适合的停顿时间或最大吞吐量,这种调整方式称为GC自适应调整策略。
- Serial Old收集器:老年代收集器
Serial Old收集器是Serial的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
- Parallel Old收集器:老年代收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记整理”算法。
- CMS收集器:老年代收集器
CMS收集器是一种以获取最短回收停顿时间为目标的并发收集器。它是基于“标记-清除”算法实现的。
运作过程分为四个步骤:
1.初始标记:仅仅是标记一下GC Roots能直接关联到的对象,速度很快
2.并发标记:并发标记阶段就是进行GC Roots Tracing的过程
3.重新标记:则是为了修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但是远比并发标记的时间短。
4.并发清除
其中初始标记和重新标记两个步骤仍然需要“Stop the world”。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以从整体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点:
1.CMS收集器对Cpu资源非常敏感。
2.Cms收集器无法处理浮动垃圾,可能出现“soncurrent Mode Failure”失败而导致另外一次FullGC的产生。
3.由于Cms使用“标记-清除”算法,所以会产生大量的空间碎片。
- G1收集器:两代都可以使用
暂缺
内存分配与回收策略
首先我们要清除,内存分配规则并不是百分之百固定的,其细节取决于当前使用的是哪种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。下面我们说一下几种最普遍的内存分配规则:
- 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够内存进行分配时,虚拟机将发起一次MinorGC。MinorGC后如果发现Eden中存活的对象不超过了Survivor的大小,则移动到Survivor,则这部分对象就直接放到老年代
- 大对象直接进入老年代
- 长期存活的对象直接进入老年代
- 动态对象年龄判定
- 空间分配担保
理解GC日志
暂略
下篇文章我们会介绍一下jvm的调优