很多人认为方法区(或者 HotSpot 虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:
- 废弃常量
- 无用的类
回收废弃常量与回收 Java 堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串 “abc” 已经进入了常量池中,但是当前系统没有任何一个 String 对象是叫做 “abc” 的,换句话说,就是没有任何 String 对象引用常量池中的 “abc” 常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个 “abc” 常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是“无用的类”:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的
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 的情况:
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虚拟机》第二版 周志明著,仅作为学习记录,书籍中用到的案例代码及描述有部分修改,但未改变原意。