范围:要回收哪些区域
在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应用服务器时,JVM随之启动,并将JDK的类和接口,应用服务器运行时需要的类和接口以及J2EE应用的类和接口定义文件也及编译后的Class文件或JAR包中的Class文件装载到JVM的永久存储区。在伊甸园中创建JVM,应用服务器运行时必须的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)。特别注意,没有合并操作,所以会有碎片。
初始化阶段: 暂停应用线程,找出所有存活的对象,耗时比较短,回收器使用单线程。
并发标记阶段: 回收器标记操作与应用并发运行,回收器使用单线程标记存活对象。
再次标记:并发标记阶段由于应用程序也在运行,这个过程中可能新增或者修改对象。所以再次暂停应用线程,找出所有修改的对象,使用多线程标记。
并发清理:回收器清理操作与应用并发运行,回收器使用单线程清理死亡对象。