前言
在介绍了对象的创建、定位以后,具体的使用则需要根据不同的业务逻辑来确定,这部分是比较自由的。但正如前文所说,Java程序员将内存的管理交托给了JVM。因此此章节将进一步介绍Java垃圾收集器及内存分配策略的相关知识。
Java语言出来之前,大家都在拼命的写C或者C++的程序,而此时存在一个很大的矛盾,C++等语言创建对象要不断的去开辟空间,不用的时候有需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的allocated,然后不停的~析构。于是,有人就提出,能不能写一段程序在实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?
1960年 基于MIT的Lisp首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时Java还没有出世呢!所以实际上GC并不是Java的专利,GC的历史远远大于Java的历史!
那究竟GC为我们做了什么操作呢?
1、 哪些内存需要回收?
2、 什么时候回收?
3、 如何回收?
这时候有人就会疑惑了,既然GC已经为我们解决了这个矛盾,我们还需要学习GC么?当然当然是肯定的,那究竟什么时候我们还需要用到的呢?
1、 排查内存溢出
2、 排查内存泄漏
3、 性能调优,排查并发瓶颈
因此这就是学习和了解GC机制的最主要原因。
一、哪些内存需要回收?
针对这个问题,我们先要明确一点,JVM重点回收的是内存共享的区域,也就是堆和方法区,因为程序计数器、虚拟机栈、本地方法栈这三个区域都是线程私有的,因而随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出有条不紊地执行出栈和入栈操作。每个栈帧分配多少内存基本上以及在类结构确定下来时,便已知晓。因此随着方法的结束,这些栈帧所分配的内存自然而然被回收了。
而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行期间才能知道创建哪些对象,这部分内存分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。
二、什么时候回收
我们知道,GC主要处理的是对象的回收操作,那么什么时候会触发一个对象的回收的呢?
1、 对象没有引用
2、 作用域发生未捕获异常
3、 程序在作用域正常执行完毕
4、 程序执行了System.exit()
5、 程序发生意外终止(被杀进程等)
那么,紧接而来的问题便是,如何确定对象已经“死去”以达到被回收的条件?
这里便需要提及的两种判定对象是否“死去”的方法:
1、 引用计数算法
在JDK1.2之前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!这种算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题
当我们的代码出现下面的情形时,该算法将无法适应
a) ObjA.obj = ObjB
b) ObjB.obj = ObjA
这样的代码会产生如下引用情形 objA指向objB,而objB又指向objA,这样当其他所有的引用都消失了之后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。
2、可达性分析算法
根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
目前java中可作为GC Root的对象有
1、 虚拟机栈中引用的对象(本地变量表)
2、 方法区中静态属性引用的对象
3、 方法区中常量引用的对象
4、 本地方法栈中引用的对象(Native对象)
说了这么多,其实我们可以看到,所有的垃圾回收机制都是和引用相关的,那我们来具体的来看一下引用的分类,到底有哪些类型的引用?每种引用都是做什么的呢?
Java中存在四种引用,每种引用如下:
1、 强引用
只要引用存在,垃圾回收器永远不会回收
Object obj = new Object();
//可直接通过obj取得对应的对象 如obj.equels(new Object());
而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。
2、 软引用
非必须引用,内存溢出之前进行回收,可以通过以下代码实现
Object obj = new Object();
SoftReference sf = new SoftReference(obj);
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
3、 弱引用
第二次垃圾回收时回收,可以通过如下代码实现
Object obj = new Object();
WeakReference wf = new WeakReference(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器
4、 虚引用(幽灵/幻影引用)
垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现
Object obj = new Object();
PhantomReference pf = new PhantomReference(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
虚引用主要用于检测对象是否已经从内存中删除。
在上文中已经提到了,我们的对象在内存中会被划分为5块区域,而每块数据的回收比例是不同的,根据IBM的统计,数据如下图所示:
我们知道,方法区主要存放类与类之间关系的数据,而这部分数据被加载到内存之后,基本上是不会发生变更的
Java堆中的数据基本上是朝生夕死的,我们用完之后要马上回收的,而Java栈和本地方法栈中的数据,因为有后进先出的原则,当我取下面的数据之前,必须要把栈顶的元素出栈,因此回收率可认为是100%;而程序计数器我们前面也已经提到,主要用户记录线程执行的行号等一些信息,这块区域也是被认为是唯一一块不会内存溢出的区域。在SunHostSpot的虚拟机中,对于程序计数器是不回收的,而方法区的数据因为回收率非常小,而成本又比较高,一般认为是“性价比”非常差的,所以Sun自己的虚拟机HotSpot中是不回收的!但是在现在高性能分布式J2EE的系统中,我们大量用到了反射、动态代理、CGLIB、JSP和OSGI等,这些类频繁的调用自定义类加载器,都需要动态的加载和卸载了,以保证永久带不会溢出,他们通过自定义的类加载器进行了各项操作,因此在实际的应用开发中,类也是被经常加载和卸载的,方法区也是会被回收的!但是方法区的回收条件非常苛刻,只有同时满足以下三个条件才会被回收!
1、所有实例被回收
2、加载该类的ClassLoader被回收
3、Class对象无法通过任何途径访问(包括反射)
三、如何回收?
好了,我们现在切入正题,Java1.2之前主要通过引用计数器来标记是否需要垃圾回收,而1.2之后都使用根搜索算法来收集垃圾,而收集后的垃圾是通过什么算法来回收的呢?
1、 标记-清除算法
2、 复制算法
3、 标记-整理算法
我们来逐一过一下
1、 标记-清除算法
标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!
2、 复制算法
复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。也就是我们前面提到的s0 s1等空间。
3、 标记-整理算法
标记 -整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
我们知道,JVM为了优化内存的回收,进行了分代回收的方式,对于新生代内存的回收(minor GC)主要采用复制算法,下图展示了minor GC的执行过程。
对于新生代和旧生代, JVM可使用很多种垃圾回收器进行垃圾回收,下图展示了不同生代不通垃圾回收器,其中两个回收器之间有连线表示这两个回收器可以同时使用。
而这些垃圾回收器又分为串行回收方式、并行回收方式合并发回收方式执行,分别运用于不同的场景。如下图所示
下面我们来逐一介绍一下每个垃圾回收器。
Serial收集器
收集器是历史最悠久的一个回收器,JDK1.3之前广泛使用这个收集器,目前也是ClientVM下 ServerVM 4核4GB以下机器的默认垃圾回收器。串行收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需要中断所有的用户线程,知道它回收结束为止,因此又号称“Stop The World” 的垃圾回收器。注意,JVM中文名称为java虚拟机,因此它就像一台虚拟的电脑一样在工作,而其中的每一个线程就被认为是JVM的一个处理器,因此大家看到图中的CPU0、CPU1实际为用户的线程,而不是真正机器的CPU,大家不要误解哦。
串行回收方式适合低端机器,是Client模式下的默认收集器,对CPU和内存的消耗不高,适合用户交互比较少,后台任务较多的系统。
Serial收集器默认新旧生代的回收器搭配为Serial+ SerialOld
1、单线程收集器
2、进行垃圾回收时必须暂停用户所有的工作线程。
Serial收集器对于运行在Client模式下的虚拟机来说是一个很棒的选择
ParNew收集器
ParNew收集器其实就是多线程版本的Serial收集器,其运行示意图如下
同样有 Stop The World的问题,他是多CPU模式下的首选回收器(该回收器在单CPU的环境下回收效率远远低于Serial收集器,所以一定要注意场景哦),也是Server模式下的默认收集器。
1、Serial的多线程版本
2、是运行在Server模式下的首选新生代收集器,只有它可以与CMS收集器配合工作。通过-XX:ParallelGCThreads参数来限制垃圾收集的线程数
Parallel Scavenge收集器
ParallelScavenge又被称为是吞吐量优先的收集器,器运行示意图如下
所提到的吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。在当今网络告诉发达的今天,良好的响应速度是提升用户体验的一个重要指标,多核并行云计算的发展要求程序尽可能的使用CPU和内存资源,尽快的计算出最终结果,因此在交互不多的云端,比较适合使用该回收器。
1、是并行的多线程收集器
2、目的是达到一个可控制的吞吐量(Throughput)
3、-XX:MaxGCPauseMillis :最大垃圾手机停顿时间
-XX:GCTimeRatio:设置吞吐量大小
-XX:+UseAdaptiveSizePolicy 打开后,虚拟机会根据当前系统的运行状况收集性能监控信息。
Serial Old 收集器
SerialOld是旧生代Client模式下的默认收集器,单线程执行;在JDK1.6之前也是ParallelScvenge回收新生代模式下旧生代的默认收集器,同时也是并发收集器CMS回收失败后的备用收集器。其运行示意图如下
Serial的老年代版本,采用“标记-整理”算法
Parallel Old 收集器
ParallelOld是老生代并行收集器的一种,使用标记整理算法、是老生代吞吐量优先的一个收集器。这个收集器是JDK1.6之后刚引入的一款收集器,我们看之前那个图之间的关联关系可以看到,早期没有ParallelOld之前,吞吐量优先的收集器老生代只能使用串行回收收集器,大大的拖累了吞吐量优先的性能,自从JDK1.6之后,才能真正做到较高效率的吞吐量优先。其运行示意图如下。
Parallel Scavenge收集器的老年代版本,采用“标记-整理”算法
1、在注重吞吐量以及CPU资源敏感结合,可以考虑Parallel Scavenge加Parallel Old收集器
CMS(Concurrent Mark Sweep)收集器
CMS又称响应时间优先(最短回收停顿)的回收器,使用并发模式回收垃圾,使用标记-清除算法,CMS对CPU是非常敏感的,它的回收线程数=(CPU+3)/4,因此当CPU是2核的实惠,回收线程将占用的CPU资源的50%,而当CPU核心数为4时仅占用25%。他的运行示意图如下
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。使用与B/S系统服务器上。是基于“标记-清除”算法实现的。
运作过程:
(1)初始标记
(2)并发标记
(3)重新标记
(4)并发清除
在初始标记的时候,需要中断所有用户线程,在并发标记阶段,用户线程和标记线程并发执行,而在这个过程中,随着内存引用关系的变化,可能会发生原来标记的对象被释放,进而引发新的垃圾,因此可能会产生一系列的浮动垃圾,不能被回收。
CMS 为了确保能够扫描到所有的对象,避免在Initial Marking 中还有未标识到的对象,采用的方法为找到标记了的对象,并将这些对象放入Stack 中,扫描时寻找此对象依赖的对象,如果依赖的对象的地址在其之前,则将此对象进行标记,并同时放入Stack 中,如依赖的对象地址在其之后,则仅标记该对象。
在进行Concurrent Marking 时minor GC 也可能会同时进行,这个时候很容易造成旧生代对象引用关系改变,CMS 为了应对这样的并发现象,提供了一个Mod Union Table 来进行记录,在这个Mod Union Table中记录每次minor GC 后修改了的Card 的信息。这也是ParallelScavenge不能和CMS一起使用的原因。
CMS产生浮动垃圾的情况请见如下示意图。
在运行回收过后,c就变成了浮动垃圾。
由于CMS会产生浮动垃圾,当回收过后,浮动垃圾如果产生过多,同时因为使用标记-清除算法会产生碎片,可能会导致回收过后的连续空间仍然不能容纳新生代移动过来或者新创建的大资源,因此会导致CMS回收失败,进而触发另外一次FULL GC,而这时候则采用SerialOld进行二次回收。
同时CMS因为可能产生浮动垃圾,而CMS在执行回收的同时新生代也有可能在进行回收操作,为了保证旧生代能够存放新生代转移过来的数据,CMS在旧生代内存到达全部容量的68%就触发了CMS的回收!
主要的三个缺点:
(1)CMS收集器对CPU资源非常敏感,因为是并行标记和并行清除的,多CPU性能存在影响。
(2)CMS收集器无法处理浮动垃圾,因为用户线程还在运行,运行过程中还会产生垃圾
(3)空间碎片,因为采用的是“标记-清除”算法
G1收集器
G1与CMS比较的两个改进的地方:
(1)使用“标记-整理”算法实现的收集器,不会产生空间碎片
(2)非常精准的控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。
(3)G1可以实现基本不牺牲吞吐量的情况下完成低停顿的内存回收。
(4)G1将新生代和老年代划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表。