如何判断对象可以被回收
Java中对象能否被回收,是根据兑现是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收
main栈帧中demo变量存储着Demo实例对象的地址,与Demo实例对象建立了连接关系此时Demo实例对象可以通过demo访问,因此这个对象不能被回收。
当demo为null时,与Demo实例对象不存在连接关系,此时Demo对象就可以被回收了
引用计数法
引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。需要注意的是,Java从来没有使用过该方法去实现GC。
一个对象被引用时计数器就进行+1,被引用次数减少一次时就-1。
java
public class demo8 {
public static void main(String[] args) {
String s1 = new String(); //s1的计数器加一
String s2 = new String(); //s2的计数器加一
s1 = s2; //s2的计数器加一
s1 = null; //s1的计数器减一,此时为0,满足被释放条件。s2计数器减一,此时为1。
}
}
优点
即时性:当引用变为零时会被直接回收。
无需遍历堆可以准确释放对应内存。
缺点
计数器开销大,每个对象都需要一个计数器,计数器的存储开销比较大,计数器增减时时间开销大
循环引用时无法做到垃圾回收
java
class A {
B b;
public A(B b) {
this.b = b;
}
}
class B {
A a;
public B(A a) {
this.a = a;
}
}
public class Main {
public static void main(String[] args) {
A a = new A(new B(null));//创建A对象引用,计数器加1
B b = new B(a);//被对象b的属性a引用,计数器再加1
a.b = b;
a = null;//a不引用A了,计数器减1,但还被b.a引用。因此无法被回收。
b = null;
}
}
可达性分析算法
也可以称为根搜索算法、追踪性垃圾收集。所谓可达性分析就是,在垃圾回收之前,对堆中所有的对象进行扫描,看是否被根对象(一定不会被回收的对象)直接或间接引用,如果是则无法被回收,没有被引用则可回收。
GC Root对象是哪些?
- 加入了synchronized同步锁的对象可以被当作GC Root
- 本地方法栈内的引用的对象
- 虚拟机栈中所引用的对象。如各个线程被调用的方法中使用到的参数、局部变量等。
- 方法区中常量引用的对象。如String Table中里的引用的。
- 基本数据类型对应的 Class 对象
简单讲,凡是被常量、静态变量、全局变量、运行时方法中的变量直接引用的对象,原则上不能被GC释放。
四种引用
强引用:上图中的实线,对于强引用对象,JVM何时都不会对其进行回收,即使是出现OOM错误(因此内存泄漏主要原因就是强引用对象无法被回收)。但是当强引用对象超过了作用域(暂时不理解)又或是显式将强引用赋值null(也就是解除了强引用关系)则可以被回收。
软引用SoftReference:对于弱引用对象,进行一次GC回收后,内存还是不足时会对其进行回收,否则可以保持存活。(可以用来实现缓存)
弱引用WeakReference:在执行GC回收时,内存即使充足也会被回收
虚引用PhantomReference:并不会决定对象的生命周期,虚引用并不能单独使用,而是要和引用队列一起使用,也不能通过虚引用来获取引用的对象。在JDK8版本中,当GC准备回收一个虚引用指向的对象时,会将虚引用存入引用队列,而被指向的对象不会被真正的回收。由另一线程去读取引用队列中的引用来执行被引用的对象回收之前的内存释放操作。例如Clearner类就是虚引用,在ByteBuffer类对象被回收之前,先进行直接内存的释放(释放操作在Clearner类中实现)再进行回收对象。但在JDK9之后虚引用不会对对象的生存产生任何影响。
需要注意的是,上图中的软引用与弱引用实际上也是一个对象,当引用的对象回收时,这些引用对象并不会被回收(因为被GC Root强引用),而是会被放入一个引用队列当中,当内存不足时,会通过遍历引用队列将这些已经没有引用对象的引用释放。
除了以上四种引用,还存在一种终结器引用,当GC回收一个重写了finalize()方法的对象时,JVM会给被回收对象创建一个终结器引用,同时将该引用放入引用队列,通过一个FinalizerHandler线程去处理引用队列当中的终结器引用,首先是判断被回收对象是否执行了finalize()方法,如果没有则执行,等到下一次GC时才会去释放该对象。
java
//-Xmx20m
public class demo9 {
private final static int _4M = 4 * 1024 * 1024;
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList();
//list -->SoftReferencr -->byte
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> softReference = new SoftReference<>(new byte[_4M]);
System.out.println(softReference.get());
list.add(softReference);
System.out.println(list.size());
}
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
}
运行结果如下
java
[B@1540e19d
1
[B@677327b6
2
[B@14ae5a5
3
[B@7f31245a
4
[B@6d6f6e28
5
null
null
null
null
[B@6d6f6e28
指定堆内存为20M,然后进行创建5次字节对象,将会进行内存回收,由运行结果可以看出来,前四次的软引用对象已经被释放,只留下了最后一个软引用对象。
可以看出在进行第二次垃圾回收时,连续回收了两次,因为第一次回收并没有回收充足的内存,因此执行第二次垃圾回收将软引用回收。
但是我们并不需要已经被回收的软引用,仍存在list队列中占用内存空间。我们可以将软引用关联到引用队列。
java
public class demo9 {
private final static int _4M = 4 * 1024 * 1024;
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList();
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
//指定绑定队列,只有软引用对象被回收的话,才会被加入引用队列
SoftReference<byte[]> softReference = new SoftReference<>(new byte[_4M], queue);
System.out.println(softReference.get());
list.add(softReference);
System.out.println(list.size());
}
Reference<? extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
}
运行结果
可以看到软引用对象被回收的软引用也被释放