深入理解JAVA虚拟机之JVM性能篇---垃圾回收
一、基本垃圾回收算法
1. 判断对象是否需要回收的方法(如何判断垃圾):
1) 引用计数(Reference Counting)
对象增加一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
2) 可达性分析算法(Reachability Analysis)
通过一系列被称为”GC Roots“的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到达GC Roots没有任何引用链相关联,则证明该对象是不可用的。
通常可作为GC Roots的对象包含以下几种:
(a) 虚拟机栈(栈帧中的本地变量表)中引用的对象;
(b) 方法区中类静态属性引用的对象;
(c) 方法区中常量引用的对象;
(d) 本地方法栈中JNI(Native方法)引用的对象
3) 再谈引用
无论是通过引用计数法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”相关,通常将 引用 分为如下几种, 引用强度依次减弱:
(a)强引用: 程序代码中普遍存在的,类似于 Object obj = new Object() 这类的引用,垃圾收集器永远不会回收被引用的对象;
(b)软引用: 用来描述一些还有用但并非必须的对象,在系统将要发生内存溢出异常之前,会将这些对象列入回收范围内进行二次回收, JDK1.2后提供了SoftReference类来实现软引用。
(c)弱引用: 用来描述非必须对象,强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否充足,都会回收。JDK1.2后提供了WeakReference类来实现软引用。
(d)虚引用: 又称为幽灵引用或幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。JDK1.2后提供了PhantomReference类来实现软引用。
2. 基本回收策略
1)标记-清除(Mark-Sweep)---适用于老年代
执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。
缺点是此算法需要暂停整个应用,同时会产生内存碎片。
2)复制(Copying)---适用与新生代
把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。
此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。
但缺点也是需要两倍内存空间。
3)标记-整理(Mark-Compact) ---适用于老年代
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。
此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
3. 按分区对待的方式
1)增量收集
实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。
2)分代收集
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
4. 按系统线程分
1)串行收集
使用单线程处理所垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。
2)并行收集
使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。
3)并发收集
相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会更长,因为堆越大而越长。
二、内存分配与回收策略
对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代Eden上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能分配在老年代中,分配的规则不是百分百固定的。
1. 为什么要分代?
不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接等,这类对象跟业务直接挂钩,因此生命周期比较长。但也有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
所以,如果在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
2. 如何分代?
1)新生代
所有新生成的对象首先都是放在年轻代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。新生代分个区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制 过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空 的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
2)年老代
在年轻代中经历了N次垃圾回收后仍然存活的对象(即长期存活的对象),就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
大对象也会直接进入老年代,这里所谓的大对象是指需要大量连续内存空间的java对象,最典型的就是那中很长的字符串及数组,这样做的目的就是避免在Eden及两个Survivor区之间发生大量的内存复制。
3)持久代
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。
4) 示意图如下:
5)什么情况下触发回收?
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
- · 年老代(Tenured)被写满
- · 持久代(Perm)被写满
- · System.gc()被显示调用
- · 上一次GC之后Heap的各域分配策略动态变化