Java 垃圾回收 - 方法区回收

很多人认为方法区(或者 HotSpot 虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,而永久代的垃圾收集效率远低于此。

永久代的垃圾收集主要回收两部分内容:

  1. 废弃常量
  2. 无用的类

回收废弃常量与回收 Java 堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串 “abc” 已经进入了常量池中,但是当前系统没有任何一个 String 对象是叫做 “abc” 的,换句话说,就是没有任何 String 对象引用常量池中的 “abc” 常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个 “abc” 常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是“无用的类”:

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading-XX:+TraceClassUnLoading 查看类加载和卸载信息,其中 -verbose:class-XX:+TraceClassLoading 可以在 Product 版的虚拟机中使用,-XX:+TraceClassUnLoading 参数需要 FastDebug 版的虚拟机支持。

在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

我们可以通过 java -help 或者 java -X 查看当前虚拟机支持的选项:

[root@niuhp ~]# java -X
    -Xmixed           mixed mode execution (default)
    -Xint             interpreted mode execution only
    -Xbootclasspath:<directories and zip/jar files separated by :>
                      set search path for bootstrap classes and resources
    -Xbootclasspath/a:<directories and zip/jar files separated by :>
                      append to end of bootstrap class path
    -Xbootclasspath/p:<directories and zip/jar files separated by :>
                      prepend in front of bootstrap class path
    -Xdiag            show additional diagnostic messages
    -Xnoclassgc       disable class garbage collection
    -Xincgc           enable incremental garbage collection
    -Xloggc:<file>    log GC status to a file with time stamps
    -Xbatch           disable background compilation
    -Xms<size>        set initial Java heap size
    -Xmx<size>        set maximum Java heap size
    -Xss<size>        set java thread stack size
    -Xprof            output cpu profiling data
    -Xfuture          enable strictest checks, anticipating future default
    -Xrs              reduce use of OS signals by Java/VM (see documentation)
    -Xcheck:jni       perform additional checks for JNI functions
    -Xshare:off       do not attempt to use shared class data
    -Xshare:auto      use shared class data if possible (default)
    -Xshare:on        require using shared class data, otherwise fail.
    -XshowSettings    show all settings and continue
    -XshowSettings:all
                      show all settings and continue
    -XshowSettings:vm show all vm related settings and continue
    -XshowSettings:properties
                      show all property settings and continue
    -XshowSettings:locale
                      show all locale related settings and continue

The -X options are non-standard and subject to change without notice.

我们写段代码来验证 classgc 的情况:

ClassGc.java

import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Set;

public class ClassGc {
  public static void main(String[] args) throws MalformedURLException {
    Set<Class<?>> classSet = new HashSet<>();
    for (Integer i = 0; i < 10000; i++) {
      URL url = new File("").toURI().toURL();
      URL[] urls = new URL[]{url};
      ClassLoader loader = new URLClassLoader(urls);
      InvocationHandler invocationHandler = (proxy, method, args1) -> method.invoke(proxy, args1);
      Object obj = Proxy.newProxyInstance(loader, BigObj.class.getInterfaces(), invocationHandler);
      classSet.add(obj.getClass());
    }
    classSet.clear();
    System.out.println("---------- call gc start -------------");
    System.gc();
    System.out.println("---------- call gc end -------------");
  }
}

在上面代码中,我们构造 10000 个 ClassLoader ,并生产出 10000 个代理类,先使用 java -XX:+PrintGC ClassGc 运行:

[root@niuhp ~]# java -XX:+PrintGC ClassGc
[GC (Allocation Failure)  64512K->8256K(245760K), 0.0093119 secs]
[GC (Metadata GC Threshold)  44023K->12560K(245760K), 0.0167798 secs]
[Full GC (Metadata GC Threshold)  12560K->12496K(190464K), 0.0374929 secs]
[GC (Allocation Failure)  77008K->20656K(190464K), 0.0094743 secs]
[GC (Metadata GC Threshold)  39634K->22808K(217088K), 0.0111336 secs]
[Full GC (Metadata GC Threshold)  22808K->22688K(291840K), 0.0761930 secs]
[GC (Allocation Failure)  113824K->34408K(291840K), 0.0133076 secs]
[GC (Metadata GC Threshold)  79401K->39904K(321024K), 0.0176166 secs]
[Full GC (Metadata GC Threshold)  39904K->39653K(411648K), 0.1793185 secs]
---------- call gc start -------------
[GC (System.gc())  71924K->43717K(414208K), 0.0063892 secs]
[Full GC (System.gc())  43717K->3700K(414208K), 0.0637551 secs]
---------- call gc end -------------

再使用 java -XX:+PrintGC -Xnoclassgc ClassGc 运行:

[root@niuhp ~]# java -XX:+PrintGC -Xnoclassgc ClassGc
[GC (Allocation Failure)  64512K->8256K(245760K), 0.0320764 secs]
[GC (Metadata GC Threshold)  44383K->12560K(310272K), 0.0215908 secs]
[Full GC (Metadata GC Threshold)  12560K->12496K(250368K), 0.0512321 secs]
[GC (Metadata GC Threshold)  96624K->22824K(250368K), 0.0142034 secs]
[Full GC (Metadata GC Threshold)  22824K->22688K(323072K), 0.1055647 secs]
[GC (Allocation Failure)  151712K->39184K(361472K), 0.0195401 secs]
[GC (Metadata GC Threshold)  45755K->39920K(382464K), 0.0101625 secs]
[Full GC (Metadata GC Threshold)  39920K->39653K(462848K), 0.2049295 secs]
---------- call gc start -------------
[GC (System.gc())  72918K->43685K(512512K), 0.0060701 secs]
[Full GC (System.gc())  43685K->43309K(512512K), 0.2632258 secs]
---------- call gc end -------------

看下最后的 GC 结果,在加入 -Xnoclassgc 后 GC 回收 43685K->43309K 的空间明显少于没有加入的情况 43717K->3700K ,这也说明了加入 -Xnoclassgc 后收集器不会对无用的类执行回收。

说明

文章摘自《深入理解Java虚拟机》第二版 周志明著,仅作为学习记录,书籍中用到的案例代码及描述有部分修改,但未改变原意。