原文地址:
Java语言出来之前,大家都在拼命的写C或者C++的程序,而此时存在一个很大的矛盾,C++等语言创建对象要不断的去开辟空间,不用的时候有需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的allocated,然后不停的~析构。于是,有人就提出,能不能写一段程序在实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?
1960年
那究竟GC为我们做了什么操作呢?
1、 2、 3、 |
这时候有人就会疑惑了,既然GC已经为我们解决了这个矛盾,我们还需要学习GC么?当然当然是肯定的,那究竟什么时候我们还需要用到的呢?
1、 2、 3、 |
我们知道,GC主要处理的是对象的回收操作,那么什么时候会触发一个对象的回收的呢?
1、
2、
3、
4、
5、
其实,我们最容易想到的就是当对象没有引用的时候会将这个对象标记为可回收对象,那么现在就有一个问题,是不是这个对象被赋值为null以后就一定被标记为可回收对象了呢?我们来看一个例子:
package import public } package public } |
程序的运行结果回事什么样子的呢?
我们来看这段代码
1、 2、 3、 4、 5、 6、 7、 |
1、
2、
3、
4、
5、
6、
7、
我们来看一下对应的运行结果
GC的日志,让我们看的更清晰一点,我们很清晰的看出,最后一句打印的不是null,并且子啊之前,还出现了逃逸的字样。说明这个对象逃逸了,在垃圾回收之前逃逸了,我们再来看这个pojo的写法,就会发现,我们重写了方法finalize,而这个方法就相当于C++中的析构方法,在GC回收之前,会先调用一次这个方法,而这个方法又将this指针指向他自己,因此得以成功逃逸!可见,并不是这个对象被赋值为null之后就一定被标记为可回收,有可能会发生逃逸!
下面我们来看一下几种垃圾收集算法
1、
当我们的代码出现下面的情形时,该算法将无法适应
a)
b)
2、
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
目前java中可作为GC Root的对象有
1、
2、
3、
4、
说了这么多,其实我们可以看到,所有的垃圾回收机制都是和引用相关的,那我们来具体的来看一下引用的分类,到底有哪些类型的引用?每种引用都是做什么的呢?
Java中存在四种引用,每种引用如下:
1、
只要引用存在,垃圾回收器永远不会回收
Object obj = new Object();
//可直接通过obj取得对应的对象
而这样
2、
非必须引用,内存溢出之前进行回收,可以通过以下代码实现
Object obj = new Object();
SoftReference
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
3、
第二次垃圾回收时回收,可以通过如下代码实现
Object obj = new Object();
WeakReference
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器
4、
Object obj = new Object();
PhantomReference
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
虚引用主要用于检测对象是否已经从内存中删除。
在上文中已经提到了,我们的对象在内存中会被划分为5块区域,而每块数据的回收比例是不同的,根据IBM的统计,数据如下图所示:
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
Java堆中的数据基本上是朝生夕死的,我们用完之后要马上回收的,而Java栈和本地方法栈中的数据,因为有后进先出的原则,当我取下面的数据之前,必须要把栈顶的元素出栈,因此回收率可认为是100%;而程序计数器我们前面也已经提到,主要用户记录线程执行的行号等一些信息,这块区域也是被认为是唯一一块不会内存溢出的区域。在SunHostSpot的虚拟机中,对于程序计数器是不回收的,而方法区的数据因为回收率非常小,而成本又比较高,一般认为是“性价比”非常差的,所以Sun自己的虚拟机HotSpot中是不回收的!但是在现在高性能分布式J2EE的系统中,我们大量用到了反射、动态代理、CGLIB、JSP和OSGI等,这些类频繁的调用自定义类加载器,都需要动态的加载和卸载了,以保证永久带不会溢出,他们通过自定义的类加载器进行了各项操作,因此在实际的应用开发中,类也是被经常加载和卸载的,方法区也是会被回收的!但是方法区的回收条件非常苛刻,只有同时满足以下三个条件才会被回收!
1、所有实例被回收
2、加载该类的ClassLoader被回收
3、Class对象无法通过任何途径访问(包括反射)
好了,我们现在切入正题,Java1.2之前主要通过引用计数器来标记是否需要垃圾回收,而1.2之后都使用根搜索算法来收集垃圾,而收集后的垃圾是通过什么算法来回收的呢?
1、
2、
3、
我们来逐一过一下
1、
![JVM垃圾回收机制学习笔记](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!
2、
![JVM垃圾回收机制学习笔记](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
s0 s1等空间。
3、
![JVM垃圾回收机制学习笔记](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
我们知道,JVM为了优化内存的回收,进行了分代回收的方式,对于新生代内存的回收(minor GC)主要采用复制算法,下图展示了minor GC的执行过程。
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
JVM可使用很多种垃圾回收器进行垃圾回收,下图展示了不同生代不通垃圾回收器,其中两个回收器之间有连线表示这两个回收器可以同时使用。
![JVM垃圾回收机制学习笔记](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
![JVM垃圾回收机制学习笔记](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
1、
看名字我们都可以看的出来,这个属于串行收集器。其运行示意图如下
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
收集器是历史最悠久的一个回收器,JDK1.3之前广泛使用这个收集器,目前也是ClientVM下
串行回收方式适合低端机器,是Client模式下的默认收集器,对CPU和内存的消耗不高,适合用户交互比较少,后台任务较多的系统。
Serial收集器默认新旧生代的回收器搭配为Serial+ SerialOld
2、
ParNew收集器其实就是多线程版本的Serial收集器,其运行示意图如下
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
Stop The World的问题,他是多CPU模式下的首选回收器(该回收器在单CPU的环境下回收效率远远低于Serial收集器,所以一定要注意场景哦),也是Server模式下的默认收集器。
3、
ParallelScavenge又被称为是吞吐量优先的收集器,器运行示意图如下
![JVM垃圾回收机制学习笔记](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
所提到的吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。在当今网络告诉发达的今天,良好的响应速度是提升用户体验的一个重要指标,多核并行云计算的发展要求程序尽可能的使用CPU和内存资源,尽快的计算出最终结果,因此在交互不多的云端,比较适合使用该回收器。
4、
ParallelOld是老生代并行收集器的一种,使用标记整理算法、是老生代吞吐量优先的一个收集器。这个收集器是JDK1.6之后刚引入的一款收集器,我们看之前那个图之间的关联关系可以看到,早期没有ParallelOld之前,吞吐量优先的收集器老生代只能使用串行回收收集器,大大的拖累了吞吐量优先的性能,自从JDK1.6之后,才能真正做到较高效率的吞吐量优先。其运行示意图如下
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
SerialOld是旧生代Client模式下的默认收集器,单线程执行;在JDK1.6之前也是ParallelScvenge回收新生代模式下旧生代的默认收集器,同时也是并发收集器CMS回收失败后的备用收集器。其运行示意图如下
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
CMS又称响应时间优先(最短回收停顿)的回收器,使用并发模式回收垃圾,使用标记-清除算法,CMS对CPU是非常敏感的,它的回收线程数=(CPU+3)/4,因此当CPU是2核的实惠,回收线程将占用的CPU资源的50%,而当CPU核心数为4时仅占用25%。他的运行示意图如下
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
模式主要分为4个过程
并发执行,而在这个过程中,随着内存引用关系的变化,可能会发生原来标记的对象被释放,进而引发新的垃圾,因此可能会产生一系列的浮动垃圾,不能被回收。
CMS
在进行Concurrent Marking
CMS产生浮动垃圾的情况请见如下示意图
![JVM垃圾回收机制学习笔记](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
在运行回收过后,c就变成了浮动垃圾。
由于CMS会产生浮动垃圾,当回收过后,浮动垃圾如果产生过多,同时因为使用标记-清除算法会产生碎片,可能会导致回收过后的连续空间仍然不能容纳新生代移动过来或者新创建的大资源,因此会导致CMS回收失败,进而触发另外一次FULL GC,而这时候则采用SerialOld进行二次回收。
同时CMS因为可能产生浮动垃圾,而CMS在执行回收的同时新生代也有可能在进行回收操作,为了保证旧生代能够存放新生代转移过来的数据,CMS在旧生代内存到达全部容量的68%就触发了CMS的回收!
7、
我们再来看垃圾回收器的总图,刚才我们可以看到,我在图上标记了一个?,其实这是一个新的垃圾回收器,既可以回收新生代也可以回收旧生代,SunHotSpot 1.6u14以上EarlyAccess版本加入了这个回收器,sun公司预期SunHotSpot1.7发布正式版,他是商用高性能垃圾回收器,通过重新划分内存区域,整合优化CMS,同时注重吞吐量和响应时间,但是杯具的是被oracle收购之后这个收集器属于商用收费收集器,因此目前基本上没有人使用,我们在这里也就不多介绍,更多信息可以参考oracle新版本JDK说明。
下面我们再来看下JVM的一些内存分配与回收策略
1、
代码示例 package public // // // } 运行结果 ![]() |
从运行结果我们可以很清晰的看到,eden有8MB的存储控件(通过参数配置),前6MB的数据优先分配到eden区域,当下一个2MB存放时,因空间已满,触发一次GC,但是这部分数据因为没有回收(引用还在,当赋值为null后则不会转移),数据会被复制到s0区域,但是s0区域不够存储,因此直接放入老生代区域,新的2MB数据存放在eden区域
2、
代码示例 package public // // // // // // } 运行结果 ![]() |
我们看到,没有触发GC日志,而数据是直接进入老生代的
3、
代码示例: package public } 运行结果 ![]() |
从代码中我们可以看到,当testCase1划分为0.25MB数据,进行多次大对象创建之后,testCase1应该在GC执行之后被复制到s0区域(s0足以容纳testCase1),但是我们设置了对象的年龄为1,即超过1岁便进入老生代,因此GC执行2次后testCase1直接被复制到了老生代,而默认进入老生代的年龄为15。我们通过profilter的监控工具可以很清楚的看到对象的年龄,如图所示
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
4、
代码示例 package public // // // } 运行结果 ![]() |
我们看到,当创建后testCase3,testCase2被移动到s0区域,当被释放后,继续创建testCase3,按理说testCase2应该移动到s1区域,但是因为超过了s1区域的1/2,因此直接进入老生代
5、
担保GC就是担保minorGC能够满足当前的存储空间,而无需触发老生代的回收,由于大部分对象都是朝生夕死的,因此,在实际开发中这种很起效,但是也有可能会发生担保失败的情况,当担保失败的时候会触发FullGC,但是失败毕竟是少数,因此这种一般是很划算的。
代码示例 package public } 1、 ![]() |
![点击查看原始大小图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
我们可以很清楚的看到,当无担保的时候,触发了一次FullGC
当我们注释掉对应的代码
// // // |
的时候,就会引发担保失败,如下图所示
默认情况是是开启担保的,无需设置参数。
我学习到的东西:
1 首先是GC