JVM垃圾回收

来源:互联网 发布:spring框架编程 编辑:程序博客网 时间:2024/06/07 14:15

垃圾回收的主体

java的垃圾回收主要针对的是 堆上的内存空间(堆上的对象)(最主要的) 以及 方法区中的常量池和无用的类的回收(在经常动态加载类的场景中需要进行回收处理)

堆上的空间回收率比较高,一般能回收70-90%的空间,而方法区中的回收效率很低,常量池的回收比较简单,但是一个无用类要被回收则必须满足下面的条件:
该类的所有实例对象已经被回收
加载该类的classloader已经被回收
该类对应的Class对象没有在任何地方被引用,该类的方法没有在任何地方被反射调用
以上的判定条件也是一个类被卸载的条件, 可知: 回收的类肯定是通过用户自定义类加载器加载的类,因为其他三种类型的类加载器肯定至少有JVM指向他们,因此不可能被回收
在大量使用反射、动态代理、CGlib等字节码框架, 动态生成JSP 以及OSGI(同一个类被不同的类加载器加载视为不同的类)这类频繁自定义ClassLoader的场景都需要JVM具备类卸载的功能,以保证方法区不会溢出。

如何判断一个对象已经死亡,可以被清除

1、古老的方式: 引用计数法
给对象添加一个引用计数器,当有一个引用指向它,+1, 当指向它的引用不指向它了或者失效,-1,实现简单, 但是无法解决循环引用的问题
循环引用: 类A和类B, 类A中包含一个成员变量B, 类B中包含一个成员变量A,当他们相互引用者对方的时候,引用计数都不为0,即使他们都变成了null,也无法删除他们。
2、目前使用的方法,可达性分析算法
在主流的商用程序语言,都是通过可达性分析判断对象是否存活的。
基本思想: 将所有的引用关系看成一张图,从一系列根节点GC root出发,向下搜索,所走过的路径称为引用链,当一个对象到GC root没有任何引用链相连,(就是指从GC root 不可达该对象)则该对象应该被回收。
可以作为GC root的对象:
<1> 方法区中类的静态成员变量
<2> 方法区中常量所指向的对象
<3> 虚拟机栈本地变量表中的引用的对象
<4> 本地方法中native方法 引用的对象
引用的几种类型:
<1>强引用 new 关键字出来的对象, 只要强引用还存在,对象就不能被垃圾回收
<2> 软引用, 有用但是非必须的对象,如果内存充足,就保留它,如果内存不足,就垃圾回收它
<3> 弱引用,非必须对象,只能生存到下一次垃圾回收之前
<4> 虚引用,也称幽灵引用,被回收,而且通过这个引用也无法访问对象,设置它的目的是在这个对象被收集器回收的时候能收到一个系统通知
注意的是: 一个对象宣告死亡,至少要经历两次标记过程,一个不可达的对象是有一个缓刑期的,同时进行判断finalize()方法是否被调用或者覆盖过(该方法只会被系统调用一次,可以在这个方法里进行自救,自救的方法就是将该对象被引用,把this赋值为某个变量,不过不建议使用这个方法,它的出现只是java刚出现时为了迎合C,C++程序员做的妥协),如果对象重写了该方法,而且该方法没有被执行过,则执行finalize()方法(只是放在一个执行队列里,但是不保证能执行到,因为该线程优先级低),如果在finalize()方法里面进行了自救,则第二次标记的时候,移出垃圾回收的集合,使其存活。

垃圾回收算法:

1、标记-清除算法
标记出所有需要回收的对象,标记完成后统一回收对象。
不足:效率不高,标记和清除的效率不高; 容易产生内存碎片
2、标记-整理算法 老年代里一般使用此种算法, 首先也是进行标记然后让所有存活的对象向一端移动,直接清理掉端边界以外的内存。
3、复制算法 将内存区域划分为大小相同的两块,每次使用其中的一半,当进行垃圾回收的时候,将存活的对象,复制到另外一半空闲的内存区,这样在两块内存区来回复制。 特别适合存活对象很少的状况,只需要少量复制就可以实现,因此很适合在新生代(每次垃圾收集新生代只有少量对象能够存活下来)使用
缺点:其一:浪费了内存空间,而且需要依赖于其他内存(老年代)进行分配担保
其二:如果对象的存活率比较高,那么需要进行较多的复制,效率变低
4、分代算法(不属于具体的回收算法,只能说是一种内存分配的策略,方便快速的垃圾回收) 根据对象的生存周期将内存分为几块,这样就可以在不同的内存块里面实行不同的垃圾回收策略,年轻代使用复制算法,年老代使用标记-整理或者标记-清除算法。

使用可达性分析来进行垃圾回收遇到的问题

1、可达性分析从GC roots节点查找引用链,需要到方法区查找静态变量或者常量,还要再本地变量表中查找,现在的程序有的方法区就有数百M,如果逐个进行查找 GC root,则势必消耗很多时间
HotSpot的解决方案:准确式GC, 使用一组称为OopMapde 数据结构来存储哪些地方存放着对象的引用,当执行系统停顿下来之后,查找这个数据结构,因此能快速的查找到GC root节点。
2. 可达性分析要求在分析的时候在一个能确保一致性的快照中进行,即此时系统的其他线程应该暂停,不能出现在分析的过程中对象的引用关系还在不断的变化的情况。这种情况称为GC停顿,GC时必须停顿所有的java执行线程,枚举根节点时也要进行停顿的。
HotSpot的解决方案: 安全点(在特定的位置区域记录了OopMapde 数据结构)和安全区域(一段代码片断内引用关系不会发生变化),意味着只能在这些安全点和安全区域才能停顿下来进行GC

垃圾收集器

垃圾收集器是内存回收的具体实现,收集算法可以视为一种回收的方法论。
HotSpot的垃圾收集器
这里写图片描述
在介绍下面的收集器时,先介绍一下本文中并行和并发的理解:
并行和并发是在并发编程中的概念,并行是指多个线程在交替进行执行,CPU时间切片给不同的线程执行,但是在同一时间只有一个线程在执行; 并发是指在多核CPU情况下多线程交替执行,因为多个CPU,所有在同一时间上有可能多个线程在同时执行,这取决于核的个数。
但是在本文垃圾回收中,结合语境,
并行的定义理解如下: 多条垃圾回收线程并行交替工作,但是用户线程已经被停顿,处于等待状态
并发的定义如下: 用户线程和垃圾回收线程同时并行执行,多条垃圾回收线程交替工作的时候并没有停顿用户线程。
1、Serial(串行GC)收集器(新生代)
单个垃圾回收线程执行,使用复制算法,垃圾回收时停顿其他用户线程
2、ParNew(并行GC)收集器(新生代)
多个垃圾回收线程执行,使用复制算法,垃圾回收时停顿其他用户线程,是串行GC的多线程版本。
3、Parallel Scavenge(并行回收GC)收集器 (吞吐量优先收集器)
多个垃圾回收线程执行,使用复制算法,垃圾回收时停顿其他用户线程, 与ParNew(并行GC)收集器功能一样,但是区别如下:
<1>此回收器的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),高吞吐量可以高效的利用CPu时间,尽快的完成程序的运算任务,适合在后台运算而不需要太多与用户交互的任务。
<2>该收集器提供了两个参数用于精确控制吞吐量,一个是最大垃圾收集停顿时间,一个是吞吐量大小;另外还提供了自适应调节策略,JVM会监控当前系统的内存性能进行动态参数的调整(如新生代大小,eden与survivor区比例,晋升老年代对象年龄大小等)来提供合适的停顿时间或者最大吞吐量,这也是与ParNew的一大不同
4、Serial Old(串行GC)收集器 (老年代)
线程,标记-整理算法,停顿用户线程
5、Parallel Old(并行GC)收集器(老年代)
线程,标记-整理算法,停顿用户线程
6、CMS(并发GC)收集器(最短回收停顿时间收集器)并发收集、低停顿
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器低停顿意味着良好的响应速度,良好的用户体验,适合运行在 在互联网站或者B/S系统的服务器上,重视服务的响应的速度,希望系统停顿时间最短,需要给用户良好的体验的场景。
CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:
①.初始标记(CMS initial mark)
②.并发标记(CMS concurrenr mark)
③.重新标记(CMS remark)
④.并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。但是耗时最长的并发标记和并发清除工作垃圾回收器线程可以和用户线程一起工作,所以总体看来可以视为并发执行的
但是CMS缺点: 1、对CPU资源非常敏感,会与用户竞争,占用一部分CPU资源2、无法处理浮动垃圾,在并发清除的时候又产生了新的垃圾,只能等待下次回收3、使用标记-清除算法,会产生垃圾碎片,可以设置N次full gc之后来一次带压缩的full gc
7、G1收集器(新生代、老年代) Garbage First
是垃圾收集器的前沿成果之一,是面向服务器端应用的垃圾收集器关注点也是在降低停顿时间上面HotSpot对它的期望是可以替换掉CMS收集器
需要注意的是,使用G1收集器意味着java的内存布局和以前不大相同了,它将整个堆划分为多个大小相等的独立区Region域,虽然还有新生代和老年代的概念,但是不再是物理隔离了
特点如下:
<1> 并行和并发,缩短停顿时间
<2> 分代收集,运行在新生代和老年代,自己单独就可以完成对整个堆的管理
<3> 空间整合,G1从整体上看使用标记-整理算法(多个region之间), 从局部看使用复制算法(单个region内部)
<4> 可预测的停顿,相对于CMS的一大优势,G1能建立可预测的停顿时间模型,能让使用者指定垃圾收集时间不能超过N毫秒
之所以能建立可预测停顿时间模型,是因为它可以避免在整个堆空间进行全区域垃圾回收,它的后台维护一个优先列表(记录每一个region区垃圾回收的价值大小),优先回收region垃圾回收价值大的区域,保证了在有限时间内获得较高的垃圾收集效率
以上垃圾收集器的内容参考了http://blog.csdn.net/java2000_wl/article/details/8030172

GC日志

33.125: [GC [DefNew: 3324k->152k(3712k), 0.0025 secs]
33.125 代表着GC发生的时间,是从JVM启动到目前的时间
GC/Full Gc 说明了垃圾收集的停顿类型,是否发生了 stop-the-world(full gc)如果是System.gc() 这里显示full gc(Sysytem)
[DefNew、[Tenured等是GC发生的区域,与垃圾回收期相关,
3324k->152k(3712k) 垃圾回收前该区域已使用容量->垃圾回收后已使用容量(总容量)
0.0025 secs 这一次GC所占用的时间

内存与回收策略

小知识1: 新生代GC(Minor GC), java对象大多是朝生夕死的,所以Minor GC非常频繁,而且回收速度也很快
老年代GC(Major GC / Full GC)
小知识2: 为什么新生代要分配两个Survivor区域,因为新生代采用的是复制算法,另外一个Survivor区域是用来轮转备份的。
1、对象优先在Eden区分配
<1>先在Eden分配,够了就分配,不够执行下一步
<2>Eden不够发起一次Minor gc,回收垃圾对象,同时Minor gc时会将Eden区经历过一次回收存活下来的对象放到suvivor区–
<3>如果够了就分配,不够就通过分配担保机制分配到老年代
2、大对象直接进入老年代
大对象是指需要连续内存空间的对象,典型的是很长的字符串或者数组,不放在新生代,直接分配到老年代,原因是避免在Eden区和两个Survivor区发生大量的内存复制操作最害怕的是一些朝生夕死的大对象,不利于回收,所以写程序的时候应该避免创建朝生夕死的大对象。
3、长期存活的对象进入老年代
每一个对象都有一个存活年龄,当它Eden区经历过一次回收存活下来,该对象放到suvivor区,当在Survivor区域经历过一次Minor Gc,年龄就+1, 到达设定年龄,转移到老年代
4、动态年龄判断
JVM并不要求survivor区的对象一定要到达存活年龄才转移到老年代,如果survivor区相同年龄的对象达到survivor区的一半,年龄大于或者等于该对象年龄的就可以直接进入老年代
5、空间分配担保
发生Minor GC 之前,会进行内存检查,老年代的剩余内存是否大于新生代存在的对象内存之和(这是因为发生Minor GC之后,存活下来的对象要放到survivor区,如果Survivor去放不下,肯定要转移到老年代,老年代必须要保证能够放下这些存活的对象,因为不知道能存活多少,只能按照全部存活的情况下做判断)
如果大于,则进行Minor gc
如果小于,则进行判定在设置参数里是否允许担保失败,
如果不允许,则进行一次full gc,
如果允许,则进行判定是否空间大于平均值,
如果大于则进行Minor gc,
如果小于则进行一次full gc

0 0
原创粉丝点击