垃圾收集器与内存分配策略
垃圾收集(Garbage Collection, GC)是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行在JVM上的语言,如Scala等)程序员在提升开发效率上获得了惊人的便利。理解GC,对于理解JVM和Java语言有着非常重要的作用。并且当我们需要排查各种内存溢出、内存泄漏问题时,当垃圾收集称为系统达到更高并发量的瓶颈时,只有深入理解GC和内存分配,才能对这些“自动化”的技术实施必要的监控和调节。
GC主要需要解决以下三个问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
下面将对这些问题进行一一介绍。
2.1 如何判断对象存活
在堆里存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,首要的就是确定这些对象中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。
引用计数算法
引用计数器判断对象是否存活的过程是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法的实现简单,判定效率也很高,大部分情况下是一个不错的算法。它没有被JVM采用的原因是它很难解决对象之间循环引用的问题。
可达性分析算法
在主流商用程序语言的实现中,都是通过可达性分析(tracing GC)来判定对象是否存活的。
算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots 到这个对象不可达)时,则证明此对象时不可用的。用下图来加以说明:
作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
2.2 各种引用
强引用
一般的Object obj = new Object() ,就属于强引用。被强引用关联的对象不会被回收。
软引用
一些有用但是并非必需,用软引用关联的对象,系统将要发生OOM之前,这些对象就会被回收。
下面的例子中,当程序发生OOM之前,尝试去回收软引用所关联的对象,导致后面获取到的值为null。
public class TestSoftRef {
public static class User{
public int id = 0;
public String name = "";
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}
}
public static void main(String[] args) {
User u = new User(1,"Vincent");
SoftReference<User> userSoft = new SoftReference<>(u);
u = null;//保证new User(1,"Vincent")这个实例只有userSoft在软引用
System.out.println(userSoft.get());
System.gc();//展示gc的时候,SoftReference不一定会被回收
System.out.println("AfterGc");
System.out.println(userSoft.get());//new User(1,"Vincent")没有被回收
List<byte[]> list = new LinkedList<>();
try {
for(int i=0;i<100;i++) {
//User(1,"Vincent")实例一直存在
System.out.println("********************"+userSoft.get());
list.add(new byte[1024*1024*1]);
}
} catch (Throwable e) {
//抛出了OOM异常后打印的,User(1,"Vincent")这个实例被回收了
System.out.println("Throwable********************"+userSoft.get());
}
}
}
程序输出结果:
弱引用 WeakReference
一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收。
下面的例子中,发生gc后,弱引用所关联的对象被回收。
public class TestWeakRef {
public static class User{
public int id = 0;
public String name = "";
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}
}
public static void main(String[] args) {
User u = new User(1,"Vincent");
WeakReference<User> userWeak = new WeakReference<>(u);
u = null;
System.out.println(userWeak.get());
System.gc();
System.out.println("AfterGc");
System.out.println(userWeak.get());
}
}
输出结果如下:
虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
为一个对象设置虚引用关联的唯一目的,就是能在这个对象被回收时收到一个系统通知。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
注意:软引用 SoftReference和弱引用 WeakReference,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。
2.3 方法区回收
很多人认为方法区没有垃圾回收,Java虚拟机规范中确实说过不要求,而且在方法区中进行垃圾收集的“性价比”较低:在堆中,尤其是新生代,常规应用进行一次垃圾收集可以回收70%~95%的空间,而方法区的效率远低于此。在JDK 1.8中,JVM摒弃了永久代,用元空间来作为方法区的实现,下面介绍的将是元空间的垃圾回收。
元空间的内存管理由元空间虚拟机来完成。先前,对于类的元数据我们需要不同的垃圾回收器进行处理,现在只需要执行元空间虚拟机的C++代码即可完成。在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的。
话句话说,只要类加载器存活,其加载的类的元数据也是存活的,因而不会被回收掉。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。