引言
垃圾收集(简称GC),在产生是,就需要考虑一下三个方法:
- 哪些内存需要垃圾回收
- 什么时候回收
- 如何回收
我们有必要对这些"自动化"的技术实施监控和调节
在内存运行时区域的各个部分,其中程序计数器,虚拟机栈帧,本地方法栈三个区域都是随线程生,随线程灭;栈中的栈帧随着方法的进入和退出有条不紊的执行的入栈和出栈的操作,以上的区域都具有确定性.java中的堆和方法区就不一样了.垃圾收集器所关注的就是这些内存
1判断对象是否已死
1.1 引用计数法
大概就是为每一个对象添加引用计数器,当有一个地方引用它时,我们就为计数器加1,当引用失效时,就为计数器减1.
但是,引用计数法无法解决对象的循环引用问题
obj1.instance=obj2
obj2.instance=obj1
//然后obj1,obj2置空
obj1=null;
obj2=null;
System.gc()
结果虚拟机没有因为他们相互引用就不回收他们(他们被回收了),说明java虚拟机并没有使用引用计数法
1.2 根搜索算法
现在主流的商用语言中,大部分都使用了根搜索算法,这时候我们就要引出"GC ROOT"了,我们以GC ROOT为起始点,依次向下搜索,走过了路径称作是引用链,当一个对象到达GC ROOT是不可达的时候,证明此对象是不可用的
图片.png
GC ROOT回收的对象包括如下几种:
- 虚拟机栈(栈中的本地变量表)中引用的对象
- 方法区中的类静态属性
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
1.3 引用
在JDK 1.2 之前,我们把引用称作是一个数据,里面存储的数据,是另一块内存的起始地址,这个就叫做引用,过于狭隘
在JDK 1.2 之后,强引用 软引用 弱引用 虚引用
- 强引用 Object obj=new Object(),只要强引用还在,垃圾回收机制就永远不会回收掉被引用的对象
- 软引用描述一些 还有用 但不是必须的对象,当发生内存溢出异常之前,会把这些对象列进回收范围并进行二次回收
- 弱引用的强度比软引用更低,当垃圾回收器开始工作之后,都会回收掉只被弱引用关联的对象
- 虚引用也被称作是幽灵引用或者幻影引用.唯一的目的就是在回收之前能够获得一个系统通知
1.4 对象的自我拯救
在这一节开始之前,我们要先明确两点
- 对象可以在被GC时自我拯救
- 这种自救的机会只有一次,因为对象的finalize()的方法只能执行一次
一个对象的死亡,需要两次标记过程,但已经知道是对象不可达的时候,这时候并不会立即死亡,而是做了第一次的标记.在第一次标记的同时,我们也要进行一次筛选.对象是否有必要执行finalize方法,当没有覆盖finalize 方法或者是已经执行过一次了,此时就视为"没有必要执行".
当一个对象被断定"有必要执行finalize方法的时候",我们会将这个对象放在一个名字为F-QUEUE的队列之中.然后会有虚拟机自动建立的,低优先级的Finalize线程去执行.执行不代表一定会执行完毕.此时就到达了第二次标记的时候.这个时候也是对象实现自我拯救的时机,这个时候只要F-QUEUE中的对象和引用链中的任意一个对象建立连接,就可以实现自我拯救
protected void finalize() throws Exception
{
super.finalize();
对象=this;//这个时候就实现了自我拯救
}
finalize()能够做的所有工作,都可以用其他的方式实现,并且可以做的更好,更及时
2 垃圾回收算法
2.1 标记-清除算法
根据题目就可以知道这个算法包括两部分内存,即标记和清除两部分,这种回收方式的缺点较多,标记和清除的效率都不是很高,空间问题,在回收一部分之后,产生大量的不连续的内存碎片.太多的内存碎片会导致无法找到足够的连续内存而不得不触发另一次垃圾回收动作
标记清除算法
2.2 复制算法
将内存划分为同样大的两块,每次只是用其中的一块,当一块的内存使用完了,我们将存活的对象复制到另一块上去,然后再把已使用的内存空间清理掉.但是这种操作会导致内存缩小为原来的一般
进一步优化,将内存区域划分成一块较大的Eden区域和两块较小的Survivor区域,每次执行标记清除算法,只是用其中的Eden区域和一块Survivor区域.当回收时,我们将存活的对象拷贝到另一块没有使用的Survivor区域上去,Eden和Survivor区域的分配比例是8:1:1,所有每次只有10%会被浪费.
但是就会有一个问题,如果经过一次GC存活的对象超过了10%,我们该怎么办,这个时候,我们就需要依靠老年代进行分配担保,分配担保机制会导致会使得对象直接进入老年代
2.3 标记-整理算法
针对于老年代的算法,不是整体的复制,而是将存活的对象向一边移动,然后清理掉边界以外的内存
图片.png
2.4 分代算法
将堆这个区域分为新生代和老年代,然后根据每个年代的特点采用最适合的收集算法.
对于新生代来说,每次垃圾回收都会有大量的对象死去,只有少量的存活,那么就选择复制算法
对于老年代,就使用标记-清理或者是标记整理算法
3.垃圾收集器
垃圾回收器就是内存回收的具体实现
3.1 serial收集器
最基本,历史最悠久的
单线程的收集器
在进行垃圾回收的时候,会停掉所有的工作去完成垃圾回收
STOP THE WORLD 造成了很大的恶劣影响
3.2 ParNew收集器
就是多线程版本的serial收集器,但是它可以和CMS通过操作,CMS是具有跨时代意义的,它首次实现了并发收集器,它可以让垃圾收集线程和用户线程共同工作
ParNew是
老年代收集器
3.3 Parallel Scavenge
新生代的收集器使用的是赋值算法,并行的多线程收集器.Parallel Scavenge的目标和其他的垃圾收集器不同,其他的收集器类似于CMS,它们的目的是能够减少用户的停顿,而Parallel Scavenge是为了达到一个可控制的吞吐量
停顿时间适合和用户进行交互的程序,主要就是为了提高用户的体验,高吞吐量则是可以高效率的利用CPU的时间,尽快的完成程序的任务,两者的侧重点不同.
3.4 Serial Old收集器
老年代版本,是一个单线程的收集器,使用的是"标记-清除"算法
3.5 Parallel Old收集器
Parallel Scavenge的老年代版本 使用多线程和"标记-整理"算法
3.6 CMS收集器(****)
目的:为了有更短的停顿时间,为了提高用户体验,因为现在java的应用程序都是运行在B/S的服务端上,应用程序十分重视用户体验
Current Mark Sweep是基于"标记-清除"算法的
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记和并发标记仍然会Stop the world.
初始标记就是标记一下GC ROOTs能够直接关联的对象,速度比较快.
重新标记,是为了纠正并发标记导致的标记变化
整个过程中最耗费时间的计时并发标记和并发清除,但是这个时候收集器的线程都可以和用户的线程一起工作
图片.png
并发收集和低停顿所以也可以叫并发低停顿收集器
缺点
CMS收集器对于CPU资源非常敏感.面向并发的程序都对CPU的资源比较敏感.它不会使得用户程序停止,但是他本身占用了一定的CPU资源,这样会导致应用程序变慢,总的吞吐量会变低CMS默认启动的回收线程数是CPU的数量+3然后再除以4 ,当CPU比较大时,也就是超过了4个,这个时候CMS最多的占有量就是25%,但是如果CPU比较少,这样的话CMS对于用户的影响就会比较大了.如果是两个CPU,这样就有可能让用户程序的执行速度降低了50%.为例解决这一个问题,虚拟机又提供了一种"增量式并发收集器",采用GC线程和用户线程交替着进行
CMS无法处理"浮动垃圾",因为GC线程和用户线程一起运行,这样就会导致用户线程源源不断的产生垃圾,这要的垃圾GC线程没办法在这一次处理它们,所有要留到下一次,这种垃圾被称作是"浮动垃圾".
我们也要为用户线程预留足够多的内存空间.
所以CMS不能再老年代都填满之后才进行收集,需要预留一部分空间
在老年代被占用了68%就会开始垃圾回收
如果预留的内存不够,那么的话就会出现"Current Mode Failure"
CMS是基于标记清除算法的,这样会产生大量的空间碎片
3.7 G1收集器
G1收集器是基于"标记-整理"算法的,所以不会参数空间碎片.
然后它可以精准的控制停顿.
对于原来的收集器都是对整个范围进行回收的,但是G1会将这些内存区域划分冲一个一个的Region,然后根据他们的垃圾堆积程度,在后台维护一个优先的列表,然后优先回收垃圾最多的区域
网友评论