上一篇文章中讨论了Java内存运行时的各个区域,其中程序计数器、虚拟机栈、本地方法栈随线程生灭,且创建时需要多少内存,基本上在译期间就决定的了,所以在内存回收时无需特殊的关注。而堆和方法区则不同,首先堆中只能在运行时,随着方法的调用而确定创建哪些对象;方法区中也同样如此,常量池中的常量、加载的类信息也是随时在发生着变化且不可预知。所以说,JVM内存回收,主要针对的是这两部分的内容。
1、堆中“死”对象
笼统的说,没用的对象就是死对象。
1.1如何判定对象“已死”
1.1.1引用计数法
给对象添加一个引用计数器,当有其他对象引用该对象时,计数器+1;当引用失效时,计数器-1。原理简单,实现容易,听起来也不错。但这个算法无法解决对象间循环引用的问题。也就是说对象A引用了对象B,而对象B同时也引用了对象A,此时就形成了循环引用。这样两个对象就永远都不会被回收。主流的JVM中均没有采用这种算法。
1.1.2可达性分析
基本思路是找一些对象作为遍历的起始点(成为GC Roots),从这些起始点开始搜索,当某个对象并没有和任何GC Root产生关联,则认为这个对象已经不被使用了,可以清除。通常,可作为GC Root的对象有以下几种:
1)虚拟机栈,栈帧中的本地变量表中引用的对象
2)方法区中加载的类的静态属性引用的对象
3)方法区中常量引用的对象(疑问,方法区中的常量到底有哪些)
4)本地方法栈中引用的对象
由上可见,可作为GC Roots的对象基本上可以归类为全局性引用和执行上下文,而应用中这些对象实在太多,这就导致根节点的遍历(或者叫确定GC Roots)势必会消耗很多时间,那是如何确定GC Roots的呢?当前主流Java虚拟机使用的都是准确式GC,以HotSpot虚拟机为例,当系统确定GC Roots时,并不需要遍历整个方法区或者堆,而是知道哪些地方存放着对象的引用,这是有一个叫OopMap的数据结构来实现的。
1.2关于引用
上文谈到的引用,是我们平时经常谈到的、最直白的“引用”。在JDK1.2之前,“引用”的定义是:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义非常的纯粹,非黑即白。在内存回收的时候也是,有用就留着,没用立刻删。但有一些场景,例如,当内存充足的时候,某块内存留着备用;内存不充足的时候,回收这块内存。这时纯粹的“引用”就没办法了。在JDK1.2之后,Java对引用进行了扩展,将引用分为强引用、软引用、弱引用、虚引用。
1)强引用:就是上面说的纯粹的引用
String str=new String("abc");
2)软引用:表示对象还有用,但不是必须的。内存不够用的时候,才会回收这些内存。软引用可用来实现内存敏感的高速缓存。
String str=new String("abc"); // 强引用 SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
后续在垃圾回收时,JVM内部逻辑如下:
if(JVM.内存不足()) { str = null; // 转换为软引用 System.gc(); // 垃圾回收器进行回收 }
3)弱引用:表示对象还有用,但不是必须的,且不如软引用“硬”。只要发生垃圾回收,这些对象就会被回收。
String str=new String("abc"); WeakReference<String> weakRef = new WeakReference<String>(str);
后续在垃圾回收时,JVM内部逻辑如下:
str = null;
4)虚引用:最弱的引用,一个对象是否存在虚引用,并不影响其生存。为一个对象设置虚引用的唯一目的是在该对象被回收时,能够收到一个系统通知。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
2、方法区内内存
JDK1.8之前方法区是放到永久代中实现的(HotSpot虚拟机)。对于堆中的内存回收,尤其是新生代,回收率能够达到70%~95%;而永久代中的回收率则非常低。也就是说,回收方法区内的内存性价比很低。但这块内存又不能没有垃圾回收机制,SUN公司就曾公布过关于方法区内存泄漏的严重BUG。
方法区中垃圾回收主要关注两部分:无用的常量和类。
常量的回收与堆中对象的回收机制类似,当某个常量没有被任何对象引用的时候,这个常量就没有用了,就可以被回收。举例,当前系统中找不到任何String对象引用了常量池中的的某个字符串常量:abcd,那么abcd这个常量就会被回收。同理,常量池中其他类、方法、字段的符号引用也与此类似。
回收类的效率非常低,但在当前企业级应用大量使用反射(Spring IOC,在bean注入的时候,通过反射实例化一个类,将其通过setter方法放到bean中)、动态代理(Spring AOP,AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。)、CGLib(CGLib技术后续继续学习)等技术的前提下,类的回收也变得很重要。已加载类的回收条件非常苛刻,需要满足以下三个条件,才有可能被JVM回收:
1)该类产生的对象实例均被回收
2)加载该类的ClassLoader已经被回收(类加载机制后续继续学习)
3)该类的类对象没有在任何地方引用,无法反射出这个类的方法