GC

垃圾回收回收什么

Java的内存分配原理与C/C++不同。C/C++每次申请内存时都要malloc进行系统调用。而系统调用发生在内核空间,每次都要中断进行切换,这须要一定的开销.

而Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,降低了系统调用的次数。节省了一定的开销。节省开销的同一时候,我们也必须对其进行垃圾的检測和回收。

java一般内存申请有两种:静态内存和动态内存。非常easy理解,编译时就能够确定的内存就是静态内存,即内存是固定的。系统一次性分配,比方int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比方java对象的内存空间。

依据上面我们知道,java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭。栈中的栈帧随着方法的结束也会撤销。内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不须要管的。可是java堆和方法区则不一样。我们仅仅有在程序执行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。

一般我们所说的垃圾回收也是针对的这一部分。

垃圾回收主要是针对堆空间和方法区的。

GC策略

垃圾检測

垃圾检測存在两种策略。第一种是引用计数。另外一种是可达性分析。

  • 引用计数是给对象加入一个引用计数器,另外一个对象引用它。计数器自增,反之自减。但这种引用计数存在一定问题,若存在两个对象A,B互相引用,而没有外部对象引用它们。这时。这两个垃圾对象不能被检測出来。
  • 可达性分析是以根集对象(栈中对象、方法区常量对象等)作为起点。向下搜索。假设不能搜索到的对象就是垃圾对象,能够进行回收。假设採用Dijkstra算法,其时间复杂对为O(m),m为对象个数。

垃圾回收

垃圾回收依照基本回收策略分有以下回收策略

按策略分类

引用计数:

对于引用计数来说,回收引用数为0的对象就可以,但其存在致命缺陷即不能处理循环引用的问题

复制(copying):

此算法将内存分为两个区域,内存的分配每次仅仅使用当中一个区域(两次gc之间)。

回收时将标记为垃圾的对象清除,把标记为非垃圾的对象拷贝到还有一个区域中。

JVM中的垃圾回收_垃圾回收

此算法速度快,不会出现内存碎片,可并发实现。可是缺点也非常明显,即须要两倍的内存空间。

标记-清除(Mark-Sweep):

对标记为垃圾的对象进行清除,不整理。

JVM中的垃圾回收_java_02

此算法速度较 ​​标记整理​​ 方法快。不浪费额外的内存空间,能够并发实现。但会产生内存碎片,效率不高。

标记-整理(Mark-Compact):

对标记为垃圾的对象进行清除。清除后进行整理,按顺序存放。

JVM中的垃圾回收_垃圾收集_03

此算法避免了​​复制​​和​​标记清除​​的缺点,不浪费内存空间,同一时候不会产生内存碎片。但其速度是最慢的,且难以并发实现。

按系统线程分类

串行收集:

串行收集使用单线程处理全部垃圾回收工作,因为无需多线程交互。实现easy。并且效率比較高。可是。其局限性也比較明显。即无法使用多处理器的优势。所以此收集适合单处理器机器。

当然,此收集器也能够用在小数据量(100M左右)情况下的多处理器机器上。

并行收集:

并行收集使用多线程处理垃圾回收工作,因而速度快。效率高。并且理论上CPU数目越多。越能体现出并行收集器的优势。

并发收集:

相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时。须要暂停整个执行环境,而仅仅有垃圾回收程序在执行。因此。系统在垃圾回收时会有明显的暂停,并且暂停时间会因为堆越大而越长。

JVM中的实际GC

JVM使用分代收集算法。分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。

因此,不同生命周期的对象能够採取不同的收集方式,以便提高回收效率。

为什么要运用分代垃圾回收策略?在java程序执行的过程中。会产生大量的对象,因每一个对象所能承担的职责不同所具有的功能不同所以也有着不一样的生命周期,有的对象生命周期较长。比方Http请求中的Session对象,线程,Socket连接等。有的对象生命周期较短,比方String对象,因为其不变类的特性,有的在使用一次后就可以回收。

试想,在不进行对象存活时间区分的情况下。每次垃圾回收都是对整个堆空间进行回收,那么消耗的时间相对会非常长,并且对于存活时间较长的对象进行的扫描工作等都是徒劳。因此须要进行分层,不同生命存活时间的对象使用不同回收方法。

怎样划分?将对象按其生命周期的不同划分成:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)。当中持久代主要存放的是类信息,所以与java对象的回收关系不大,与回收息息相关的是年轻代和年老代。


上面有7中收集器。分为两块。上面为新生代收集器,以下是老年代收集器。假设两个收集器之间存在连线。就说明它们能够搭配使用。

Serial(串行GC)收集器

Serial收集器是一个新生代收集器。单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其它全部的工作线程(用户线程)。

是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器因为没有线程交互的开销,专心做垃圾收集自然能够获得最高的单线程收集效率。

ParNew(并行GC)收集器

ParNew收集器事实上就是serial收集器的多线程版本号。除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。

Parallel Scavenge(并行回收GC)收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其它收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间。而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序执行时间/(程序执行时间 + 垃圾收集时间),虚拟机总共执行了100分钟。当中垃圾收集花掉1分钟,那吞吐量就是99%。

停顿时间越短就越适合须要与用户交互的程序。良好的响应速度能提升用户体验,而高吞吐量则能够高效率地利用CPU时间,尽快完毕程序的运算任务,主要适合在后台运算而不须要太多交互的任务。

并行回收GC通过调节内部參数(晋升老年代的年龄。回收间隔等)来控制吞吐量和速度。

Serial Old(串行GC)收集器

Serial Old是Serial收集器的老年代版本号,它相同使用一个单线程执行收集,使用“标记-整理”算法。

主要使用在Client模式下的虚拟机。

Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本号。使用多线程和“标记-整理”算法。

CMS(并发GC)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

①.初始标记(CMS initial mark)

②.并发标记(CMS concurrenr mark)

③.又一次标记(CMS remark)

④.并发清除(CMS concurrent sweep)

当中初始标记、又一次标记这两个步骤任然须要停顿其它用户线程。初始标记仅仅仅仅是标记出GC ROOTS能直接关联到的对象,速度非常快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而又一次标记阶段则是为了修正并发标记期间,因用户程序继续执行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。

因为整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都能够与用户线程一起工作,所以总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器的长处:并发收集、低停顿,可是CMS还远远达不到完美。其主要有三个显著缺点:

CMS收集器对CPU资源非常敏感。在并发阶段,尽管不会导致用户线程停顿。可是会占用CPU资源而导致引用程序变慢。总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。

CMS收集器无法处理浮动垃圾(gc执行时产生的垃圾)。可能出现“Concurrent Mode Failure“,失败后而导致还有一次Full GC(老生代GC)的产生。

最后一个缺点。因为採用​​标记清除​​方法,会产生大量碎片。空间碎片太多时。将会给对象分配带来非常多麻烦,比方说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。

G1收集器

G1(Garbage First)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代。而G1将整个Java堆(包含新生代,老年代)。

  • 基于标记-整理算法,不产生内存碎片。
  • 能够非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同一时候在后台维护一个优先级列表。每次依据所同意的收集时间。优先回收垃圾最多的区域。

区域划分和优先级区域回收机制,确保G1收集器能够在有限时间获得最高的垃圾收集效率。