【JVM】JVM垃圾回收机制
垃圾回收GC
Java garbage collection is an automatic process to manage the runtime memory used by programs. By doing it automatic JVM relieves the programmer of the overhead of assigning and freeing up memory resources in a program.
Java 与 C语言相比的一个优势是,可以通过自己的JVM自动分配和回收内存空间。
何为GC
垃圾回收机制是由垃圾收集器Garbage Collection GC来实现的,GC是后台的守护进程。它的特别之处是它是一个低优先级进程,但是可以根据内存的使用情况动态的调整他的优先级。因此,它是在内存中低到一定限度时才会自动运行,从而实现对内存的回收。这就是垃圾回收的时间不确定的原因。
为何要这样设计:
因为GC也是进程,也要消耗CPU等资源,如果GC执行过于频繁会对java的程序的执行产生较大的影响(java解释器本来就不快),因此JVM的设计者们选着了不定期的gc。
GC有关的是 runtime data area 中的 heap(对象实例会存储在这里) 和 gabage collector方法。
程序运行期间,所有对象实例存储在运行时数据区域的heap中,当一个对象不再被引用(使用),它就需要被收回。在GC过程中,这些不再被使用的对象从heap中收回,这样就会有空间被循环利用。
GC为内存中不再使用的对象进行回收,GC中调用回收的方法–收集器garbage collector. 由于GC要消耗一些资源和时间,Java 在对对象的生命周期特征(eden or survivor)进行分析之后,采用了分代的方式进行对象的收集,以缩短GC对应用造成的暂停。
在垃圾回收器回收内存之前,还需要一些清理工作。
因为垃圾回收gc只能回收通过new关键字申请的内存(在堆上),但是堆上的内存并不完全是通过new申请分配的。还有一些本地方法(一般是调用的C方法)。这部分“特殊的内存”如果不手动释放,就会导致内存泄露,gc是无法回收这部分内存的。 所以需要在finalize中用本地方法(native method)如free操作等,再使用gc方法。显示的GC方法是system.gc()
GC(Garbage Collection)机制,是Java与C++/C的主要区别之一,Java开发者,一般不需要单独处理内存的回收,GC会负责内存的释放。
Java运行时区域中程序计数器、虚拟机栈、本地方法栈这3个区域随线程生命周期结束而结束,Java堆、方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
何为垃圾
Java中那些不可达的对象就会变成垃圾。那么什么叫做不可达?其实就是没有办法再引用到该对象了。主要有以下情况使对象变为垃圾:
1.对非线程的对象来说,所有的活动线程都不能访问该对象,那么该对象就会变为垃圾。
2.对线程对象来说,满足上面的条件,且线程未启动或者已停止。
例如:
(1)改变对象的引用,如置为null或者指向其他对象。
Object x=new Object();object1
Object y=new Object();object2
x=y;object1 变为垃圾
x=y=null;object2 变为垃圾
(2)超出作用域
if(i==0){ Object object1 = new Object(); }//括号结束后object1将无法被引用,变为垃圾
(3)类嵌套导致未完全释放
class A{ A a; }
A x= new A();//分配一个空间
x.a= new A();//又分配了一个空间
x=null;//将会产生两个垃圾
(4)线程中的垃圾
class A implements Runnable{
void run(){ …. }
}
public static void main(String... args){
A object1 = new A();
object1.start();
x=null;//等线程执行完后object1才被认定为垃圾
}
这样看,确实在代码执行过程中会产生很多垃圾,不过不用担心,java可以有效地处理他们。
JVM中将对象的引用分为了四种类型,不同的对象引用类型会造成GC采用不同的方法进行回收:
(1)强引用(Strong Reference):默认情况下,对象采用的均为强引用 (GC不会回收)
(2)软引用(Soft Reference):软引用是Java中提供的一种比较适合于缓存场景的应用 (只有在内存不够用的情况下才会被GC)
(3)弱引用(Weak Reference):在GC时一定会被GC回收
(4)虚引用(Phantom Reference):在GC时一定会被GC回收
对象是否死亡
判断对象是否死亡通常有引用计数算法、可达性分析算法
引用计数算法
该算法通常是给对象中添加一个引用计数器,每当有一个地方引用它是,计数器就会+1,引用失效,计数器值就会-1,如果计数器值为0,对象就不会被再使用,就可以回收该对象了。
该算法简单但速度很慢。缺陷是:不能解决循环引用的问题。
可达性分析算法
在主流的商用程序语言(Java、C#)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。
基本思路就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
GC Roots对象包括以下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈JNI(Java Native Interface,即一般说的Native方法)引用的对象
引用
判定对象是否存活都与”引用”有关,Java对引用概念进行了扩充。
强引用(Strong Reference)
软引用(Soft Reference)
弱引用(Weak Reference)
虚引用(Phantom Reference)
垃圾收集算法
JAVA虚拟机中是如何做的? java的做法很聪明,我们称之为自适应的垃圾回收器,或者是自适应的、分代的、停止-复制、标记-清扫式垃圾回收器。它会根据不同的环境和需要选择不同的处理方式。
标记算法
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。
此算法最致命的是无法处理循环引用的问题。
标记-清除算法(mark and sweep)
标记-清除:此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
优点:不会存在类似标记算法对象互相引用导致无法回收的问题。
缺点:因为被回收的堆空间不能保证一定是连续的,所以会产生许多空间碎片,可能导致大对象无法创建。
复制算法(停止-复制,stop and copy)
为了解决效率问题,“复制”(Copying)的收集算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
一般是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。
优点:因为大多数对象遵循“朝生夕灭”的原则,使用这种回收算法进行回收的时候每次只需要将存活的对象复制到另一块内存区域即可,所以回收效率绝对OK。
缺点:很明显,需要双倍内存,或者说,在内存一定情况下,只能使用一半内存。
缺点,效率低,需要的空间大。
优点,不会产生碎片。
标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行整理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
优点:这种算法解决了标记-清除算法里的内存碎片问题和复制算法里的空间问题。
缺点:这种算法虽然不会产生空间碎片也不会浪费内存,但是,在回收完之后对堆空间的整理过程是非常耗时的。
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”算法,一般把Java堆分为新生代和老生代,这样就可以根据各个年代的特点采用最合适的收集算法。
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年轻代、年老代、持久代,对不同生命周期的对象使用不同的算法进行回收。
从J2SE 1.2开始均采用此种垃圾回收算法,也是到目前为止相对快速且稳定的垃圾回收算法(JDK 7采用了所谓G-First的回收算法,有待研究)这里详细介绍分代算法:
为什么要分代
在Java程序中会产生大量的对象,其中有些对象是与业务信息相关的,比如Session对象,Socket连接,这些对象生命周期相对较长;但是有些对象生命周期很短,比如String对象,由于它的final特性,甚至有的只用一次就被回收。
如果在不进行分代的情况下,每次进行垃圾回收都要对整个堆空间进行扫描,已死对象即可被回收,但是对于依然存活的对象,这种遍历是没有任何意义的。因此采用分代的回收理念,按照生命周期进行划分,把不同生命周期的对象放在不同的代上,不同的代采用不同的垃圾回收算法进行回收,以便提高回收效率。
如何分代
VM中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息以及JVM自身所需的空间,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
年轻代
每当对象被创建之后会首先进入年轻代,年轻代的目标就是尽可能回收那些短生命周期的对象。年轻代分为三个区:一个Eden Space,两个相同大小的Survivor Space(也称为From Space和To Space)。大部分对象在Eden Space中生成(大对象会直接进入年老代),当Eden Space达到出发GC的时候被回收的对象会被放到一个Survivor Space里,当一个Survivor Space快满的时候会将依然存活的对象复制到两一个Survivor Space里,对象每“逃过”一次GC,对象的年龄就会长一岁,达到默认的年龄之后就会被放入年老代。
这两个Survivor Space总有一个是空的,可以看出,此空间采用了以上介绍的复制算法,不过,根据用户需要Survivor Space可以配置多余两个区域,这样可以增加对象在年轻代中存在的时间,降低被存放在年老代的可能,减少full gc的发生(为什么要减少full gc的发生会在后面介绍)。
年老代
主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured。一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
持久代
主要保存class、method、filed等对象,这部分的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到应用服务器的热部署时,有时候 会遇到java.lang.OutOfMemoryError: PermGen space 的错误,造成这个错误的很大原因就有可能是每次热部署后,旧的class没有被卸载掉,这样就造成了大量的class对象保存在了Perm中,这种情况下 一般重新启动应用服务器可以解决问题,或者通过-XX:MaxPermSize= 将持久代大小设大点。
heap组成
由于GC需要消耗一些资源和时间的,Java在对对象的生命周期特征进行分析后,采用了分代的方式来进行对象的收集,即按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停. heap 的组成有三区域世代:(可以理解随着时间,对象实例不断变换heap中的等级,有点像年级)
新生代 Young Generation
1. Eden Space 任何新进入运行时数据区域的实例都会存放在此
2. S0 Suvivor Space 存在时间较长,经过垃圾回收没有被清除的实例,就从Eden 搬到了S0
3. S1 Survivor Space 同理,存在时间更长的实例,就从S0 搬到了S1
旧生代 Old Generationtenured 同理,存在时间更长的实例,对象多次回收没被清除,就从S1 搬到了tenured。
永久代Permanent 存放运行时数据区的方法区。
如何回收
Java 不同的世代使用不同的 GC 算法。
Minor collection: 新生代 Young Generation 使用将 Eden 还有 Survivor 内的数据利用 semi-space 做复制收集(Copying collection), 并将原本 Survivor 内经过多次垃圾收集仍然存活的对象移动到 Tenured。
Major collection 则会进行 Minor collection,Tenured 世代则进行标记压缩收集。
To note that 这个搬运工作都是GC完成的,这也是garbage collector 的名字来源,而不是叫garbage cleaner. GC负责在heap中搬运实例,以及收回存储空间。
GC工作原理
JVM 分别对新生代和旧生代采用不同的垃圾回收机制
JVM的GC类型分为两种:
1) Scavenge GC (也称Minor GC)
对年轻代进行回收,包括Eden Space和两个Survivor Space。
2) Full GC
对整个堆空间(包括Young Gen、 Old Gen、 Perm Gen)进行扫描回收,比较耗时。
一般情况下,当新对象生成并在Eden申请空间失败时就会触发Minor GC,对Eden区域进行GC,清除已死对象,并把尚且存活的对象移到Survivor Space里,然后整理Surivor的两个区域,这种对年轻代的GC不会影响年老代,因为大部分对象是从Eden space开始的,同时 Eden Space不会分配太大,默认情况下,年轻代空间大小与年老代空间大小的比值在30%左右,所以EdenSpace的GC会频繁进行,因而这里需要速度快、效率高的算法尽可能把年轻代空间空闲出来供其他对象申请以复用。
JVM在进行Full GC的时候会,所有线程会跑到最近的安全点并挂起,等待GC完成之后再恢复运行,Sun官方将这件事称为“Stop The World”,Full GC发生过程中所有线程处于等待状态,程序无响应,所以JVM调优应运而生,其关键在于通过对程序代码、硬件、网络等软硬件环境综合分析运用JVM提供的诸多垃圾收集器进行共同协作来完成GC这件看似平常却又极为关键的事儿。
内存分配与回收策略回顾
对象优先在Eden分配
大对象直接进入老年代
长期存活的对象将进入老年代
动态对象年龄判定
空间分配担保
垃圾收集器
Serial收集器
ParNew收集器
Parallel Scavenge收集器
Serial Old 收集器
Parallel Old收集器
CMS收集器
G1收集器
一般收集器都需要“Stop The World”,垃圾收集器都在尽力减少中断时间,提高吞吐量。
G1垃圾收集器
参考:
《深入理解Java虚拟机:JVM高级特性与最佳实践》及其他文档
声明:图片来自源于网络。这是之前整理的word笔记,没有注明图片具体来源,抱歉。在此向贡献图片的人表示感谢。