垃圾回收器

主要负责在上进行内存回收

  • 自动垃圾回收可以降低实现难度, 降低回收bug的可能性
  • 但是程序员无法控制内存回收的及时性, 也无法完全避免内存溢出

应用场景

  1. 解决系统僵死(因为频繁的垃圾回收)
  2. 性能优化
  3. 常见垃圾回收, 四种引用等

方法区回收

线程不共享的程序计数器和Java虚拟机栈以及本地方法栈, 都只需要等待线程销毁自己就销毁了, 不需要垃圾回收

方法区回收的条件(三个)

方法区中的类不再使用, 即可被回收

  1. 类的所有实例都已经被回收了, 在堆中不存在任何该类的实例对象以及子类对象
    1
    2
    3
    Class<?> clazz = loader.loadClass("类的全限定名");
    Object o = clazz.newInstance();
    o = null; // 此时对象o不再使用, 就可以让gc自动回收clazz类
  2. 加载该类的类加载器已经被回收了
    1
    2
    URLClassLoader loader = new URLClassLoader(new URL[]{new URL(spec:"路径")});
    loader = null;
  3. 该类对应的java.lang.Class对象没有任何地方被引用
    1
    2
    Class<?> clazz = loader.loadClass("类的全限定名");
    clazz = null;

使用System.gc()可以手动触发垃圾回收

  • 开发过程中此类场景出现较少, 主要在OSGI, JSP的热部署等场景中
  • 每个jsp文件对应一个类加载器, 一个jsp文件被修改了, 直接写在这个jsp类加载器, 创建新的类加载器, 重新加载jsp文件

堆回收

堆上的对象主要看是否还被引用, 如果被引用, 说明不能回收

1
2
3
4
5
6
public class Demo {
public static void main(String[] args) {
Demo d = new Demo();
d = null; // 此时没有引用, 可以被回收
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ReferenceCounting {
public static void main(String[] args) {
A a1 = new A();
B b1 = new B();
a1.b = b1;
b1.a = a1;
}
}
class A{
B b;
}
class B{
A a;
}
// A实例要回收, 需要消除两个引用关系, 分别是栈中a1对对象的引用, 以及B对象中b1.a对对象的引用
// a1 = null;
// b1.a = null;
// 如果单纯的执行a1 = null; b1 = null; A和B对象也可以被回收, 因为方法里面没有办法访问到AB对象了
  • 如果想要查看垃圾回收的信息, 可以使用-verbose:gc

判断堆上的对象是否被引用(两种方法)

  1. 引用计数法: 每个对象维护一个计数器, 初始值为0, 对象被引用就加1, 取消引用就减1
    • 缺点:

    引用和取消引用需要维护计数器, 对性能有一定影响

    循环引用, 比如A引用B, B引用A, 就会导致对象无法回收(上面的例子如果用引用计数法, 就无法回收了)

  2. 可达性分析法(Java使用, 性能更高, 解决了循环引用的问题)
    • 可达性分析算法将对象分为两类: 垃圾回收的根对象(GC Root); 普通对象, 对象与对象之间存在引用关系
    • GC Root对象一般不可以被回收, JVM也会维护一个GC Root对象列表
    • 每次从某个GC Root遍历引用链, 如果某个普通对象可以从GC Root到达, 说明不可被回收
      achieve

什么样的对象可以作为GC Root对象?

  1. 线程Thread对象, 引用线程栈帧中的方法参数、局部变量等
  2. 系统类加载器加载的java.lang.Class对象, 引用类中的静态变量
    1
    2
    3
    4
    5
    6
    public class ReferenceCounting {
    public static A a2 = new A();
    }
    // sun.misc.Launcher是一个GC Root对象, 可以找到应用程序类加载器, 以及扩展类加载器
    // 自定义的ReferenceCounting是由应用程序类加载器加载的, 所以可以由GC Root找到
    // a2 引用了A, 所以GC Root可以找到a2, 所以不会回收
  3. 监视器对象, 用来保存同步锁synchronized关键字持有的对象
    synchronized(Reference.class) 只要这个关系建立起来, 监视器对象就可以找到ReferenceCounting, 就无法回收
  4. 本地方法调用时使用的全局对象(不需要程序员过多关注)
  • 通过Arthas以及eclise MAT(Memory Analyzer)工具可以查看GC Root
  • Arthas使用heapdump <dir/文件名.hprof>命令将堆内存快照保存到本地磁盘中
  • 使用MAT工具打开堆内存的快照文件
  • 使用GC Roots功能查看所有的GC Root

对象引用(五种)

强引用, 软引用, 弱引用, 虚引用, 终结器引用

  1. 强引用

    可达性算法中的对象引用一般指强引用, 就是GC Root对象对普通对象有引用关系, 那么普通对象就不会被回收

  2. 软引用

    如果一个对象只有软引用关联到它, 如果程序内存不足, 则会将软引用进行回收

    JDK 1.2提供SoftReference实现软引用, 经常用于缓存中

    因此使用软引用应该创建两个对象, 一个是SoftReference对象, 用于引用真正使用的对象, 而SoftReference本身应该被Gc Root引用, 保证可以找到

    软引用的执行过程

    1. 对象使用软引用包装起来, new SoftReference<对象类型>(对象);
    2. 内存不足时, JVM进行垃圾回收
    3. 垃圾回收仍然不能解决内存不足的问题, 回收软引用中的对象
    4. 如果依然内存不足, 会抛出OutOfMemory异常
    1
    2
    3
    byte[] bytes = new byte[1024 * 1024 * 100];
    SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes);
    // 这段代码将100M的数据放在软引用中
    • Java中的Caffeine可以在创建缓存的过程中将缓存对象设置成softValues()也就是软引用
    • 软引用中的对象如果内存不足会被回收, SoftReference对象本身也需要被回收, 但是SoftReference一旦被回收了, 就无法知道其引用的对象是否真的被回收了, 所以SoftReference提供了一套队列机制来进行判断:
      1. 软引用创建时, 通过构造器传入引用队列(程序员自定义)
      2. 软引用中包含对象被回收时, 该软引用对象会被放入引用队列
      3. 通过代码遍历引用队列, 将SoftReference的强引用删除
    • 软引用可以继承SoftReference类的方式来实现, SoftReference类就是一个软引用对象, 通过构造器传入软引用包含的对象, 以及引用队列
    • 使用软引用实现学生数据的缓存, 软引用如果被回收了, 则要清理HashMap中的key
      stucache
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    public class StudentCache {
    private static StudentCache cache = new StudentCache();
    public static void main(String[] args) {
    for (int i = 0; ; ++ i) {
    StudentCache.getInstance().cacheStudent(new Student(i, String.valueOf(i)));
    }
    }

    private Map<Integer, StudentRef> StudentRefs; // 用于Cache内容的存储
    private ReferenceQueue<Student> q; // 垃圾Reference队列
    // 继承SoftReference, 每个实例都具有一个可识别的标识
    // 并且标识与在HashMap中的key相同
    private class StudentRef extends SoftReference<Student> {
    private Integer _key = null;
    public StudentRef(Student em, ReferenceQueue<Student> q) {
    super(em, q); // 调用父类的构造方法
    _key = em.getId();
    }
    }

    // 构造一个缓存器实例
    private StudentCache() {
    StudentRefs = new HashMap<Integer, StudentRef>();
    q = new ReferenceQueue<Student>();
    }

    // 取得缓存器实例
    public static StudentCache getInstance() {return cache;}

    // 以软引用的方式对一个Student对象的实例进行引用并保存该引用(放入缓存)
    private void cacheStudent(Student em) {
    cleanCache(); // 清除垃圾引用
    StudentRef ref = new StudentRef(em, q);
    StudentRefs.put(em.getId(), ref);
    System.out.println(StudentRefs.size());
    }
    // 依据指定的ID, 重新获取相应的Student对象的实例
    public Student getStudent(Integer id) {
    Student em = null;
    // 缓存中是否有该Student实例的软引用, 如果有就从软引用中获得
    if (StudentRefs.containsKey(id)) {
    StudentRef ref = StudentRefs.get(id);
    em = ref.get();
    }
    // 如果没有这个软引用, 或者这个软引用得到的实例为空, 则重新构建一个实例, 保存对这个实例的软引用
    if (em == null) {
    em = new Student(id, String.valueOf(id));
    System.out.println("Retrieve From StudentInfoCenter. ID = " + id);
    this.cacheStudent(em);
    }
    return em;
    }

    // 清除那些软引用的所有Student对象已经被回收的StudentRef对象
    private void cleanCache() {
    StudenRef ref = null;
    while((ref = (StudentRef)q.poll() != null)) {
    StudentRefs.remove(ref._key);
    }
    }
    }
    class Student {
    int id;
    String name;
    public Student(int id, String name) {
    this.id = id;
    this.name = name;
    }
    public int getId() {return id;}
    public void setId(int id) {this.id = id;}
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    }
  3. 弱引用

    关联的对象在垃圾回收时, 不管内存够不够, 都会被直接回收

    JDK 1.2版本之后提供了WeakReference类来实现弱引用, 主要用在ThreadLocal中, 弱引用本身也可以使用引用队列回收

    除了ThreadLocal以外, 基本上不会使用这个, Caffeine中也有弱引用的实现, 但是一般不用

  4. 虚引用(幽灵引用, 幻影引用)

    在常规开发中不会使用

    不能通过虚引用对象获取包含的对象, 唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知

    使用PhantomReference实现了虚引用

    直接内存为了及时知道直接内存中的对象不再使用, 从而回收内存, 就会用虚引用实现Cleaner类(解决了直接内存中内存的释放问题)

  5. 终结器引用

    在常规开发中不会使用

    对象需要被回收的时候, 终结器引用会关联对象并且放在Finalizer类中的引用队列, 由一条FinalizerThread线程从队列中获取对象. 然后执行对象的finalize()方法(这个方法实际上重写了Object中的方法, 作用是回收对象时做一些收尾的工作), 在对象第二次被回收时, 该对象才会被真正的回收. 这个过程中finalize()方法再将自身对象使用强引用关联上, 但是不建议这么做, 因为这个finalize()方法什么时候调用, 甚至可能不调用, 都是GC决定的, 不是程序员决定的. (不管是实现自救, 也就是用强引用关联; 还是实现清理工作都是不合适的, 所以基本不会用)