首页 > 代码库 > JVM垃圾回收机制
JVM垃圾回收机制
范围:要回收哪些区域
在JVM五种内存模型中,有三个是不须要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。由于它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自己主动释放。所以仅仅有方法区和堆须要进行GC。
前提:如何推断对象已死
全部的垃圾收集算法都面临同一个问题。那就是找出应用程序不可到达的内存块。将其释放,这里面得不可到达主要是指应用程序已经没有内存块的引用了, 在JAVA中。某个对象相应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在全部线程栈的对象的引用)引用或者对象被还有一个可到达的对象引用。
引用计数算法
引用计数是最简单直接的一种方式。这样的方式在每个对象中添加一个引用的计数。这个计数代表当前程序有多少个引用引用了此对象。假设此对象的引用计数变为0。那么此对象就能够作为垃圾收集器的目标对象来收集。
长处:简单,直接,不须要暂停整个应用
缺点:1.须要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作。2.不能处理循环引用的问题
因此这样的方法是垃圾收集的早期策略,如今非常少使用。Sun的JVM并没有採用引用计数算法来进行垃圾回收。是基于根搜索算法的。
根搜索算法
通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有不论什么引用链相连时。则该对象不可达。该对象是不可使用的。垃圾收集器将回收其所占的内存。
在java语言中。可作为GCRoot的对象包含以下几种对象:
a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
b.方法区中的类静态属性引用的对象。
c.方法区中的常量引用的对象。
d.本地方法栈中JNI本地方法的引用对象。
推断没用的类:
(1).该类的全部实例都已经被回收。即java堆中不存在该类的实例对象。
(2).载入该类的类载入器已经被回收。
(3).该类所相应的java.lang.Class对象没有不论什么地方被引用,无法在不论什么地方通过反射机制訪问该类的方法。
四种引用
GC在收集一个对象的时候会推断是否有引用指向对象。在JAVA中的引用主要有四种:
⑴ 强引用(Strong Reference)
强引用是使用最普遍的引用。假设一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足。Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止。也不会靠任意回收具有强引用的对象来解决内存不足的问题。
⑵ 软引用(Soft Reference)
假设一个对象仅仅具有软引用,则内存空间足够,垃圾回收器就不会回收它;假设内存空间不足了,就会回收这些对象的内存。仅仅要垃圾回收器没有回收它,该对象就能够被程序使用。软引用可用来实现内存敏感的快速缓存。
以下举个样例,假如有一个应用须要读取大量的本地图片,假设每次读取图片都从硬盘读取,则会严重影响性能,可是假设全部载入到内存当中,又有可能造成内存溢出,此时使用软引用能够解决问题。
设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时。JVM会自己主动回收这些缓存图片对象所占用的空间。从而有效地避免了内存溢出的问题。
软引用能够和一个引用队列(ReferenceQueue)联合使用,假设软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用添加到与之关联的引用队列中。
(3)弱引用(Weak Reference)
弱引用与软引用的差别在于:仅仅具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中。一旦发现了仅仅具有弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。只是。由于垃圾回收器是一个优先级非常低的线程。因此不一定会非常快发现那些仅仅具有弱引用的对象。
弱引用能够和一个引用队列(ReferenceQueue)联合使用。假设弱引用所引用的对象被垃圾回收。Java虚拟机就会把这个弱引用添加到与之关联的引用队列中。
⑶ 虚引用(Phantom Reference)
“虚引用”顾名思义。就是形同虚设,与其它几种引用都不同,虚引用并不会决定对象的生命周期。假设一个对象仅持有虚引用,那么它就和没有不论什么引用一样。在不论什么时候都可能被垃圾回收器回收。
虚引用主要用于检測对象是否已经从内存中删除,跟踪对象被垃圾回收器回收的活动。
虚引用与软引用和弱引用的一个差别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,假设发现它还有虚引用,就会在回收对象的内存之前。把这个虚引用添加到与之关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue (); PhantomReference pr = new PhantomReference (object, queue);策略:JVM中的垃圾收集策略
标记-清除算法
标记清除收集器停止全部的工作。从根扫描每个活跃的对象,然后标记扫描过的对象,标记完成以后,清除那些没有被标记的对象。
长处:
1 解决循环引用的问题
2 不须要编译器的配合,从而就不运行额外的指令
缺点:
1. 每个活跃的对象都要进行扫描,收集暂停的时间比較长。
2.标记-清除算法採用从根集合进行扫描,对存活的对象对象标记,标记完成后,再扫描整个空间中未被标记的对象,进行回收。如上图所看到的。
标记-清除算法不须要进行对象的移动,而且仅对不存活的对象进行处理,在存活对象比較多的情况下极为高效。但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
复制算法
复制收集器将内存分为两块一样大小空间,某一个时刻。仅仅有一个空间处于活跃的状态。当活跃的空间满的时候,GC就会将活跃的对象拷贝到未使用的空间中去,原来不活跃的空间就变为了活跃的空间。
长处:
1 仅仅扫描能够到达的对象,不须要扫描全部的对象。从而降低了应用暂停的时间
缺点:
1.须要额外的空间消耗,某一个时刻,总是有一块内存处于未使用状态
2.复制对象须要一定的开销
复制算法採用从根集合扫描。并将存活对象拷贝到一块新的,没有使用过的空间中,这样的算法当空间存活的对象比較少时。极为高效。可是带来的成本是须要一块内存交换空间用于进行对象的移动。
标记-整理算法
标记整理收集器汲取了标记清除和复制收集器的长处,它分两个阶段运行,在第一个阶段,首先扫描全部活跃的对象,并标记全部活跃的对象,第二个阶段首先清除未标记的对象。然后将活跃的的对象拷贝到堆得底部
该算法极大的降低了内存碎片,而且不须要像复制算法一样须要两倍的空间。
标记-整理算法採用标记-清除算法一样的方式进行对象的标记,但在清除时不同。在回收不存活的对象占用的空间后。会将全部的存活对象往左端空暇空间移动,并更新相应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,可是却攻克了内存碎片的问题。
分代回收算法
垃圾分代回收算法(GenerationalCollecting)基于对对象生命周期分析后得出的垃圾回收算法。
由于我们前面有介绍,内存主要被分为三块。新生代、旧生代、持久代。三代的特点不同,造就了他们所用的GC算法不同,新生代适合那些生命周期较短。频繁创建及销毁的对象。旧生代适合生命周期相对较长的对象,持久代在Sun HotSpot中就是指方法区(有些JVM中根本就没有持久代这中说法)。首先介绍下新生代、旧生代、持久代的概念及特点。
Young(年轻代、新生代):JVM specification中的 Heap的一部份年轻代分三个区。
一个Eden区。两个Survivor区。大部分对象在Eden区中生成。
当Eden区满时。还存活的对象将被拷贝到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被拷贝到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的而且此时还存活的对象。将被复制旧生代。
须要注意。Survivor的两个区是对称的。没先后关系,所以同一个区中可能同一时候存在从Eden复制过来对象,和从前一个Survivor复制过来的对象。而拷贝到年老区的仅仅有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
新生代使用复制算法和标记-清除垃圾收集算法,新生代中98%的对象是朝生夕死的短生命周期对象。所以不须要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from(Survivor 0)和Survivor to(Survivor1)三部分,其占新生代内存容量默认比例分别为8:1:1。当中Survivor from和Survivor to总有一个区域是空白,仅仅有Eden和当中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,仅仅有10%的Survivor内存浪费,当新生代内存空间不足须要进行垃圾回收时,仍然存活的对象被拷贝到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收。两个Survivor区域是轮换的。
假设空白Survivor空间无法存放下仍然存活的对象时。使用内存分配担保机制。直接将新生代依旧存活的对象拷贝到年老代内存中,同一时候对于创建大对象时,假设新生代中无足够的连续内存时,也直接在年老代中分配内存空间。
Java虚拟机对新生代的垃圾回收称为Minor GC。次数比較频繁,每次回收时间也比較短。
使用java虚拟机-Xmn參数能够指定新生代内存大小。
Tenured(年老代、旧生代):JVMspecification中的 Heap的一部份年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
年老代中的对象一般都是长生命周期对象,对象的存活率比較高,因此在年老代中使用标记-整理垃圾回收算法。
Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对照较少,每次回收的时间也比較长。
java虚拟机-Xms參数能够指定最小内存大小,-Xmx參数能够指定最大内存大小,这两个參数分别减去Xmn參数指定的新生代内存大小。能够计算出年老代最小和最大内存容量。
Perm(持久代、永久代): JVM specification中的 Method area 用于存放静态文件,如今Java类、方法等。
持久代对垃圾回收没有显著影响。可是有些应用可能动态生成或者调用一些class,比如Hibernate等,在这样的时候须要设置一个比較大的持久代空间来存放这些运行过程中新增的类。
java虚拟机内存中的方法区在SunHotSpot虚拟机中被称为永久代。是被各个线程共享的内存区域,它用于存储已被虚拟机载入的类信息、常量、静态变量、即时编译后的代码等数据。
永久代垃圾回收比較少,效率也比較低,可是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。
永久代也使用标记-整理算法进行垃圾回收,java虚拟机參数-XX:PermSize和-XX:MaxPermSize能够设置永久代的初始大小和最大容量。
垃圾回收过程
上面我们看了JVM的内存分区管理,如今我们来看JVM的垃圾回收工作是如何运作的。
首先当启动J2EE应用server时。JVM随之启动,并将JDK的类和接口,应用server运行时须要的类和接口以及J2EE应用的类和接口定义文件也及编译后的Class文件或JAR包中的Class文件装载到JVM的永久存储区。
在伊甸园中创建JVM,应用server运行时必须的JAVA对象,创建J2EE应用启动时必须创建的JAVA对象;J2EE应用启动完成,可对外提供服务。
JVM在伊甸园区根据用户的每次请求创建相应的JAVA对象,当伊甸园的空间不足以用来创建新JAVA对象的时候,JVM的垃圾回收器运行对伊甸园区的垃圾回收工作,销毁那些不再被其它对象引用的JAVA对象(假设该对象仅仅被一个没有其它对象引用的对象引用的话。此对象也被归为没有存在的必要,依此类推)。并将那些被其它对象所引用的JAVA对象移动到幸存者0区。
假设幸存者0区有足够空间存放则直接放到幸存者0区;假设幸存者0区没有足够空间存放。则JVM的垃圾回收器运行对幸存者0区的垃圾回收工作,销毁那些不再被其它对象引用的JAVA对象。并将那些被其它对象所引用的JAVA对象移动到幸存者1区。
假设幸存者1区有足够空间存放则直接放到幸存者1区;假设幸存者1区没有足够空间存放,则JVM的垃圾回收器运行对幸存者1区的垃圾回收工作。销毁那些不再被其它对象引用的JAVA对象,并将那些被其它对象所引用的JAVA对象移动到养老区。
假设养老区有足够空间存放则直接放到养老区。假设养老区没有足够空间存放。则JVM的垃圾回收器运行对养老区区的垃圾回收工作,销毁那些不再被其它对象引用的JAVA对象,并保留那些被其它对象所引用的JAVA对象。
假设到最后养老区,幸存者1区,幸存者0区和伊甸园区都没有空间的话,则JVM会报告“JVM堆空间溢出(java.lang.OutOfMemoryError: Java heap space)”。也即是在堆空间没有空间来创建对象。
这就是JVM的内存分区管理。相比不分区来说;普通情况下。垃圾回收的速度要快非常多。由于在没有必要的时候不用扫描整片内存而节省了大量时间。
对象的空间分配和晋升
(1)对象优先在Eden上分配
(2)大对象直接进入老年代
虚拟机提供了-XX:PretenureSizeThreshold參数。大于这个參数值的对象将直接分配到老年代中。由于新生代採用的是标记-复制策略,在Eden中分配大对象将会导致Eden区和两个Survivor区之间大量的内存拷贝。
(3)长期存活的对象将进入老年代
对象在Survivor区中每熬过一次MinorGC,年龄就添加1岁,当它的年龄添加到一定程度(默觉得15岁)时。就会晋升到老年代中。
触发:何时開始GC
Minor GC(新生代回收)的触发条件比較简单,Eden空间不足就開始进行Minor GC回收新生代。
而Full GC(老年代回收,一般伴随一次MinorGC)则有几种触发条件:
(1)老年代空间不足
(2)PermSpace空间不足
(3)统计得到的MinorGC晋升到老年代的平均大小大于老年代的剩余空间
这里注意一点:PermSpace并不等同于方法区,仅仅只是是HotspotJVM用PermSpace来实现方法区而已,有些虚拟机没有PermSpace而用其它机制来实现方法区。
实现:JVM中的回收器类型
串行回收器(Serial Collector)
单线程运行回收操作。回收期间暂停全部应用线程的运行,client模式下的默认回收器
年轻代的回收算法(Minor Collection):把Eden区的存活对象移到To区,To区装不下直接移到年老代。把From区的移到To区。To区装不下直接移到年老代,From区里面年龄非常大的升级到年老代。 回收结束之后,Eden和From区都为空,此时把From和To的功能互换,From变To。To变From,每一轮回收之前To都是空的。设计的选型为复制。
年老代的回收算法(Full Collection):年老代的回收分为三个步骤,标记(Mark)、清除(Sweep)、合并(Compact)。标记阶段把全部存活的对象标记出来。清除阶段释放全部死亡的对象。合并阶段把全部活着的对象合并到年老代的前部分,把空暇的片段都留到后面。设计的选型为合并,降低内存的碎片。
并行回收器(Parallel Collector)
使用多个线程同一时候进行垃圾回收,多核环境里面能够充分的利用CPU资源,降低回收时间。添加JVM生产率,Server模式下的默认回收器。
与串行回收器同样。回收期间暂停全部应用线程的运行。
年轻代的回收算法(Minor Collection):使用多个线程回收垃圾,每个线程的算法与串行回收器同样。
年老代的回收算法(Full Collection):年老代依旧是单线程的,与串行回收器同样。
并行合并收集器(Parallel Compacting Collection)
年轻代和年老代的回收都是用多线程处理。与并行回收器相比,年老代的回收时间更短,从而降低了暂停时间间隔(Pause time)。
年轻代的回收算法(Minor Collection):与并行回收器(ParallelCollector)同样
年老代的回收算法(Full Collection) :年老代分为三个步骤,标记、统计、合并。这里用到分的思想。把年老代划分为非常多个固定大小的区(region)。标记阶段,把全部存活的对象划分为N组(应该与回收线程数同样),每个线程独立的负责自己那一组,标记存活对象的位置以及所在区(Region)的存活率信息,标记为并行的。
统计阶段,统计每个区(Region)的存活率,原则上靠前面的存活率较高,从前到后。找到值得合并的開始位置(绝大多数对象都存活的区不值得合并),统计阶段是串行的(单线程)。合并阶段。根据统计阶段的信息,多线程并行的把存活的对象从一个区(Region)拷贝到另外一个区(Region)。
并发标记清除回收器(Concurrent Mark-Sweep Collector)
又名低延时收集器(Low-latencyCollector),通过各种手段使得应用程序被挂起的时间最短。基本与应用程序并发地运行回收操作。没有合并和复制操作。
年轻代的回收算法(Minor Collection):与并行回收器(ParallelCollector)同样
年老代的回收算法(Full Collection) :分为四个步骤,初始标记(Initial Mark)、并发标记(ConcurrentMark)、再次标记(Remark)、以及并发清理(Concurrent Sweep)。
特别注意。没有合并操作。所以会有碎片。
初始化阶段: 暂停应用线程,找出全部存活的对象,耗时比較短。回收器使用单线程。
并发标记阶段: 回收器标记操作与应用并发运行。回收器使用单线程标记存活对象。
再次标记:并发标记阶段由于应用程序也在运行,这个过程中可能新增或者改动对象。
所以再次暂停应用线程。找出全部改动的对象,使用多线程标记。
并发清理:回收器清理操作与应用并发运行。回收器使用单线程清理死亡对象。
JVM垃圾回收机制