简述Java垃圾回收

Java虚拟机区域

运行的时候讲内存分为多个不同的数据区域

内存运行区域示意图(来自网络)

程序计数器

比较小的内存空间,可以看成当前程序所执行的字节码的行号指示器。

虚拟机栈

描述了Java方法执行的内存模型,每个方法执行的时候会创建栈帧,存储局部变量表、操作数、动态连接、返回地址等。调用方法的过程是一个进栈和出栈的过程。
异常说明:线程请求栈深度大于虚拟机所允许的情况下,抛出StackOverflowErro异常;当栈可以动态扩展的时候,也可能抛出OutOfMemoryError

本地方法栈

为虚拟机使用到的Native方法提供服务

虚拟机所管理的内存中最大的一块。被所有的线程所共享,虚拟机启动的时候创建。用于存放对象的实例。也是垃圾收集器的主要工作区域。堆中是分代的:新生代,老年代。
主流虚拟机都可以动态扩展空间,可以通过(-Xmx -Xms)参数控制。

方法区

各个线程共享的内存区域,存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
运行时常量池是方法区的一部分。用于存放编译期生成的各种字面量和符号引用。
注意:运行期也可以将新的常量放入常量池中

虚拟机中的对象

对象的创建

当遇到一个new指令的时候,虚拟机是如何工作的那

  1. 常量池中检查是否存在此类的符号引用,并检查这个类是否被加载、解析、初始化过
  2. 如果没有:进行类的加载过程
  3. 类加载检查完成之后,虚拟机将为新对象分配内存。
    说明:内存分配存在指针碰撞和空闲列表两种方法。虚拟机采用CAS和失败重试保证内存分配操作的原子性。还可以使用本地线程缓冲的方法(-XX:+/-UseTLAB)
  4. 分配好的对象内存空间初始化为0
  5. 对对象进行必要设置,设置在对象头
  6. 执行初始化函数,完成对象初始化操作

对象在内存中的布局

内存中的对象有三个区域:对象头;实例数据;对齐填充

  1. 对象头:包括对象自身的运行时数据,类型指针(指向它的类元数据)
  2. 实例数据:代码中定义的各种类型的字段内容
  3. 对齐填充:并非必须,占位。整个对象必须满足8字节的整数倍

对象的访问定位

程序会通过栈上的引用访问堆中的对象,如何通过引用去定位对象规范没有具体定义。

  1. 句柄:在堆中分配句柄池,reference中存储的是句柄的地址,句柄中包含具体类的信息
  2. 直接地址访问:reference中存储的是直接的对象地址

虚拟机的溢出

堆溢出

不断创建对象,并且保证对象不被回收,产生堆溢出。通过VM参数限制堆不能自动扩展(Xms与Xmx相同)

import java.util.ArrayList;
import java.util.List;

/**
 * 产生堆内存不足异常
 * VM Args : -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError 
 * 
 * @author linxm
 *
 */
public class HeapOOM {
    static class OOMObject{
        
    }
    
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        
        for(;;){
            list.add(new OOMObject());
        }
    }
}

栈溢出

/**
 * 产生栈溢出
 * VM Args:-Xss128k
 * 
 * @author linxm
 *
 */
public class JavaVMStackOF {
    private static int stackLength = 1;
    
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }
    
    public static void main(String[] args) throws Throwable{
        JavaVMStackOF oom = new JavaVMStackOF();
        
        try{
            oom.stackLeak();
        }catch(Throwable e){
            System.out.println("堆栈深度:" + stackLength);
            throw e;
        }
    }
}

方法区溢出

通过Spring动态产生大量的类

import java.lang.reflect.Method;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

/**
 * Java方法区产生溢出
 * VM Args:  -XX:PermSize=2M -XX:MaxPermSize=2M
 * 
 * @author linxm
 *
 */
public class JavaMethodAreaOOM {
    public static void main(String[] args) {
        while(true){
            Enhancer enchancer = new Enhancer();
            enchancer.setSuperclass(OOMObject.class);
            enchancer.setUseCache(false);
            enchancer.setCallback(new MethodInterceptor() {
                
                @Override
                public Object intercept(Object arg0, Method arg1, Object[] arg2,
                        MethodProxy arg3) throws Throwable {
                    return arg3.invokeSuper(arg0, arg2);
                }
            });
        }
    }
    
    class OOMObject{
        
    }
}

本机直接溢出

直接内存可以通过 -XX: MaxDirectMemorySize指定,如不指定则默认与堆最大值(Xmx)大小相同。在使用NIO的时候,有可能会有这种溢出

import java.lang.reflect.Field;

/**
 * 直接内存溢出
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 * 
 * @author linxm
 *
 */
public class DirectMemoryOOM {
    private static final int _1Mb = 1024 * 1024;
    
    @SuppressWarnings("restriction")
    public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
        Field unsafeField = sun.misc.Unsafe.class.getDeclaredFields()[0];
        
        unsafeField.setAccessible(true);
        sun.misc.Unsafe unsafe = (sun.misc.Unsafe)unsafeField.get(null);
        
        while(true){
            unsafe.allocateMemory(_1Mb);
        }
    }
}

垃圾回收

需要思考3件事情

  1. 那些内存需要回收
  2. 什么时候回收
  3. 怎么回收

虚拟机中程序计数器,本地方法区,虚拟机栈随着线程而消亡;栈中的栈帧随着方法调入和调出而产生和消亡。
垃圾回收主要考虑的是堆和方法区

什么需要回收

堆中的对象实例是主要回收的内容,需要判断是否不再被使用。主流的虚拟机都是通过可达性算法来实现。
通过GC Root对象为起点,从这些节点开始搜索,走过的路径就是引用链,如果一个对象没有任何引用链可以连接到GC Root则判断为需要回收

GC Root包括如下几种:

  1. 虚拟机栈中引用的对象
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中引用的对象

以上是判断什么需要回收的条件,所有的描述都是围绕着“引用”来讨论的。
关于引用又有4中强度的区分:

  1. 强引用:只要存在就不会被回收
Object.obj = new Object();
  1. 软引用:还有用但并非必须的对象,系统会在要发生内存溢出之前对这些对象进行二次回收。SoftReference来实现
  2. 弱引用:有用但并非必须,对象只能生存到下一次垃圾回收之前。WeakReference来实现
  3. 虚引用:最弱的一种引用关系,无法通过虚引用获取对象实例。只是在回收的时候收到一个系统通知。PhantomReference来实现
import java.lang.ref.SoftReference;

/**
 * 演示软引用的使用方法
 * 
 * @author Administrator
 *
 */
public class fReferenceType {
    public static void main(String[] args) {
        //  这是一个强引用
        Object o = new Object();
        System.out.println(o.hashCode());
        //  o会在适当时机被回收
        o = null;
        
        Object objRef = new Object();
        System.out.println(objRef.hashCode());
        //  软引用
        SoftReference<Object> aSoftRef = new SoftReference<Object>(objRef);
        //  SoftReference依然保持有objRef的引用,不会马上回收,但是在OutOfMemoryError之前回收对象
        objRef = null;
        //  回收前可以依然获取对象
        objRef = aSoftRef.get();        
        System.out.println(objRef.hashCode());
    }
}

什么时候回收

对象不可达表示可以回收,但是不是马上就回收,只是“死缓”而已。真正到达死亡的过程,需要两次标注:
发现对象不可达,进行一次标注并且进行一次筛选:看对象是否覆盖了finalize(),如果覆盖了并且虚拟机没有执行过finalize方法,则放入F-Queue队列中,表示等待销毁。finalize方法中可以拯救对象,只要这个时候与GC Root产生引用链,依然可以摆脱死亡。

/**
 * 拯救对象的方法
 * 1. 对象在GC时候自救
 * 2. 对象自救只有一次机会,因为一个对象的finalize()方法只执行一次
 * 
 * @author linxm
 *
 */
public class FinalizeEspaceGC {
    public static FinalizeEspaceGC SAVE_HOOK = null;
    
    public void isAlive(){
        System.out.println("我还活着!");
    }
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize方法被执行了!");
        SAVE_HOOK = this;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEspaceGC();
        SAVE_HOOK = null;
        
        //  第一次拯救自己
        System.gc();
        //  因为finalize方法优先级比较低,暂停一会
        Thread.sleep(1000);
        
        if(SAVE_HOOK == null){
            System.out.println("拯救自己失败");
        }else{
            SAVE_HOOK.isAlive();
        }
        
        SAVE_HOOK = null;
        
        //  第二次拯救自己
        System.gc();
        //  因为finalize方法优先级比较低,暂停一会
        Thread.sleep(1000);
        
        if(SAVE_HOOK == null){
            System.out.println("拯救自己失败");
        }else{
            SAVE_HOOK.isAlive();
        }
    }
}

任何对象的finalize系统只会执行一次!

回收方法区

方法区也就是永久代也有可能进行垃圾回收。主要是回收废弃的常量和无用的类,但是效率很差。

  1. 判断是否为废弃的常量比较简单,只要没有常量的引用即可
  2. 判断是否为无用的类比较麻烦,至少需要满足下面的条件
  1. 类的所有实例已被回收
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法通过反射构造此类

是否进行类的回收通过参数可以设置

-Xnoclassc

在大量使用反射和自定义ClassLoader的时候,需要虚拟机具有自动装卸类的功能

怎么回收垃圾

下面看一下垃圾回收的算法。

  1. 标记-清除算法:效率不高,空间碎片
  2. 复制算法:使用一半的内存,交替使用。现代虚拟机不使用1:1的内存方式。使用一块大的Eden空间和两个小的Survivor空间。HotSpot默认的比例是8:1,当Survivor内存不够的时候,就会使用老年代进行分配担保。
  3. 标记-整理算法:一般用于老年代
  4. 分代收集算法:java的堆分为新生代和老年代,采用不同的算法。新生代使用复制算法;老年代使用标记清除或者标记整理。

算法实现

枚举GC Root的算法,如何尽可能不造成卡顿是一个难题。
我们使用OopMap的结构,在虚拟机中存储引用关系,以供GC Root算法进行查看。不可能对每一个指令都生成OopMap(消耗太多的资源),这就有了安全点的概念,在安全点上生成OopMap,安全点选取的频率是一个对性能影响比较大的因素。
程序运行到安全点停下来的方法:抢占式中断,主动式中断(设置中断标志,各个线程轮休这个中断标志)
在进行GC的时候

  1. JVM对所有线程发起中断请求
  2. 如果程序的中断点不在安全点上,则运行到安全点
  3. 对于一些处于Sleep或者Blocked状态的线程,无法接收到JVM的中断请求,这时候需要使用安全区域(在一段代码中,引用关系不会发生变化)的概念来解决

垃圾回收器

有些垃圾回收器可以搭配使用,连线的表示可以配合使用


示意图

重要的概念:

  1. Minor GC
    又称新生代GC,指发生在新生代的垃圾收集动作;
    因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;
    触发条件:当Eden区满时,触发Minor GC。
  2. Full GC
    又称Major GC或老年代GC,指发生在老年代的GC;
    出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
    Major GC速度一般比Minor GC慢10倍以上;
    Full GC触发条件:
    (1)调用System.gc时,系统建议执行Full GC,但是不必然执行
    (2)老年代空间不足
    (3)方法区空间不足
    (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
    (5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

垃圾回收器的具体说明

  1. Serial收集器:
    单线程,会暂停所有的工作进行垃圾回收,默认用于Java client中。设置参数:-XX:+UseSerialGC
  2. ParNew:
    Serial的多线程版本。设置参数
    -XX:+UseConcMarkSweepGC,指定使用CMS后,会默认使用ParNew作为新生代收集器
    -XX:+UseParNewGC,强制指定使用ParNew
    -XX:ParallelGCThreads,指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同
  3. Parallel Scavenge:
    新生代的收集器,使用复制算法,并行的多线程收集器。主要的特点是控制吞吐量。
    -XX:MaxGCPauseMillis,用于设置回收器的停顿时间;
    -XX:GCTimeRatio,用于设置吞吐量的大小;
    -XX:+UseAdaptiveSizePolicy,打开这个参数则系统会自动设置很多参数,根据GC运行的情况进行调节(自省是这个收集器的主要特点)
  4. Serial Old:
    Serial收集器的老年代版本,可以作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
  5. Parallel Old:
    Parallel Scavenge收集器的老年代版本
  6. CMS
    是一种以最小停顿为目标的设计方案,是基于标记-清除的算法
    -XX:+UseConcMarkSweepGC,指定使用CMS收集器
    -XX:+UseCMSCompactAtFullCollection,打开这个参数则在需要Full GC的时候使用内存碎片的合并整理过程
    -XX:+CMSFullGCsBeforeCompaction,设置执行多少次不压缩的Full GC后,来一次压缩整理,为减少合并整理过程的停顿时间,默认值为0
  7. G1
    面向服务端应用的收集器,并行与并发;分代收集;空间整合;可预期的停顿
    -XX:+UseG1GC,指定使用G1收集器;
    -XX:InitiatingHeapOccupancyPercent,当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
    -XX:MaxGCPauseMillis,为G1设置暂停时间目标,默认值为200毫秒;
    -XX:G1HeapRegionSize,设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;

推荐阅读更多精彩内容