面试准备-Java基础

Java中对象应用类型分哪几类?

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 当内存不足时 对象缓存 内存不足时终止
弱引用 正常垃圾回收时 对象缓存 垃圾回收后终止
虚引用 正常垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止

1.强引用

  • 1.1普通变量赋值即为强引用,如A a = new A();使用与成员变量、静态变量、局部变量;注意:如果直接把一个字符串直接赋值给String类型变量,那么它就会在常量池中保存,就会有一个额外的强引用,所以在使用软引用、弱引用、虚引用的时候,如果需要引用的是字符串对象,千万不要直接将一个字符串传入,而是要传入一个new String对象,否则就会产生一个额外的强引用,注意弱引用介绍完下边的代码演示
  • 1.2通过GC Root的引用链,如果强引用不到该对象,该对象才能被回收
  • 1.3当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题
  • 1.4如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象
  • 1.5比如ArraryList类的clear方法中就是通过将引用赋值为null来实现清理工作的
  • 1.6在ArrayList类中定义了一个私有的变量elementData数组,在调用方法清空数组时可以看到为每个数组内容赋值为null不同于elementData=null,(由于Object[] elementData,即elementData变量是在栈中分配的空间,但是elementData数组是在堆中分配的内存空间,数组的每一位都指向一个Object对象,每个Object对象是在堆中分配的内存空间,如果将elementData=null,栈中变量指向null,也就是说对数组取消了强引用,但是elementData数组中的每一位依然对他们指向的Object对象存在强引用)这样强引用仍然存在,这些Object对象并没有被释放;但是采用给数组的每一位赋值为null,可以取消数组的每一位对他们指向的Object对象的强引用;同时避免在后续调用 add()等方法添加元素时进行重新的内存分配。使用如clear()方法中释放内存的方法对数组中存放的引用类型特别适用,这样就可以及时释放内存。
public void clear() {
      modCount++;
 
      // Let gc do its work
      for (int i = 0; i < size; i++)
          elementData[i] = null;
 
      size = 0;
}
GC Root对象 a对象

2.软引用(SoftReference)

  • 2.1 例如:
SoftReference softReference = new SoftReference(new A());
SoftReference<A> softReference1 = new SoftReference<>(new A());
  • 2.2如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍然不足,再次回收时才会释放对象, 但是仅仅只会回收软引用指向的对象的内存,软引用自身不会被回收,如果二次回收完成之后,系统内存依然不够,才会抛出内存溢出错误
  • 2.3在回收这些对象之前,我们可以通过:MyObject anotherRef=(MyObject)aSoftRef.get();重新获得对该实例的强引用;而回收之后,调用get()方法就只能得到null了。
  • 2.3软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中
  • 2.4软引用自身需要配合引用队列来释放
  • 2.5虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的“较新的”软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue的原因
  • 2.6软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据
  • 2.7典型的例子反射数据,反射获取的对象都是软引用的
GC Root对象 SoftReference a对象

应用场景:

  • 浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
    • 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
    • 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。
  • 这时候就可以使用软引用,很好的解决了实际的问题:
 // 获取浏览器对象进行浏览
    Browser browser = new Browser();
    // 从后台程序加载浏览页面
    BrowserPage page = browser.getPage();
    // 将浏览完毕的页面置为软引用
    SoftReference softReference = new SoftReference(page);

    // 回退或者再次浏览此页面时
    if(softReference.get() != null) {
        // 内存充足,还没有被回收器回收,直接获取缓存
        page = softReference.get();
    } else {
        // 内存不足,软引用的对象已经回收
        page = browser.getPage();
        // 重新构建软引用
        softReference = new SoftReference(page);
    }
参考原文链接:https://blog.csdn.net/baidu_22254181/article/details/82555485

3.弱引用(WeakReference)

  • 3.1例如
WeakReference weakReference = new WeakReference(new A());
WeakReference<A> WeakReference1 = new WeakReference<>(new A());
  • 3.2如果仅有弱引用引用该对象时,只要发生垃圾回收,不管当前内存空间是否足够,都会释放该对象
  • 3.3弱引用自身需要配合引用队列来释放
  • 3.4由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象;被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收
  • 3.5典型的例子是ThreadLocalMap中的Entry对象的key
  • 3.6弱引用继承了抽象类Reference的get方法,在回收这些对象之前,我们可以通过:MyObject anotherRef=(MyObject)weakRef.get();重新获得对该实例的强引用;而回收之后,调用get()方法就只能得到null了
  • 3.7弱引用还可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中
GC Root对象 WeakReference a对象
注意:如果一个对象是偶尔(很少)的使用,并且希望在使用时随时就能获取到,
但又不想影响此对象的垃圾收集,那么你应该用WeakReference来引用此对象。

4.虚引用(PhantomReference)

4.1构造方法

public PhantomReference(T referent, ReferenceQueue<? super T> q)

4.2虚引用必须配合引用队列一起使用当虚引用引用的对象被回收时,会将虚引用对象入队列,由Reference Handler线程释放其关联的外部资源
4.3典型例子是虚引用的子类Cleaner释放DirectByteBuffer占用的直接内存
4.4虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会影响对象的生命周期如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动
4.5应用场景:
* 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
* 虚引用与软引用和弱引用的一个区别在于:

    虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,
    如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
 String str = new String("abc");
    ReferenceQueue queue = new ReferenceQueue();
    // 创建虚引用,要求必须与一个引用队列关联
    PhantomReference pr = new PhantomReference(str, queue);

4.6程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
4.7虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除

    public T get() {return null;}

面试准备-Java基础

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* 测试虚引用
*/
public class PhantomReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        // 定义一个引用队列
        ReferenceQueue<String> queue = new ReferenceQueue<>();

        // 此时list和字符串"aaa", "bbb", "ccc"之间就是虚引用关系而不是强引用关系了
        // 那么垃圾回收的时候就会直接把字符串"aaa", "bbb", "ccc"回收掉
        ArrayList<MyResource> list = new ArrayList<>();
        list.add(new MyResource(new String("aaa"), queue));
        list.add(new MyResource(new String("bbb"), queue));
        // 注意gc回收的是堆中的资源,如果不用new关键字,那么该字符串对象仍然会在字符串的常量池中存储一份,有一个引用,
        // 因此"ccc"不会被gc回收,那么指向该字符串对象的弱引用对象就不会被加入引用队列中
        list.add(new MyResource("ccc", queue));

        // 手动触发垃圾回收
        System.gc();
        // 三个字符串被回收掉以后,他们的虚引用对象会被加入到引用队列queue中去
        // 因此我们通过遍历引用队列queue就可以知道哪些虚引用对象关联的对象被gc回收了
        TimeUnit.SECONDS.sleep(1);
        Object ref;
        while ((ref = queue.poll()) != null) {
            if (ref instanceof MyResource) {
                ((MyResource) ref).clean();
            }
        }
    }
}

/**
 * 自定义自己的虚引用类型MyResource
 */
class MyResource extends PhantomReference<String> {
    public MyResource(String referent, ReferenceQueue<? super String> q) {
        super(referent, q);
    }
    /**
     * 释放外部资源
     */
    public void clean() {
        System.out.println("clean");
    }
}
输出结果:
clean
clean
  • 弱引用、虚引用都是配合引用队列,目的都是为了找到哪些java对象被回收,从而进行对他们关联的资源进行进一步清理
  • 为了减低api使用难度,从java9开始引入了Cleaner对象

Object类中的方法

  • getClass()
  • hashCode()
  • toString()
  • wait()
  • notify()
  • notifyAll()
  • finalize()
  • clone()
  • equals()

finalize的理解

  • 1.finalize是Object类中的一个方法,子类重写它,垃圾回收时此方法就会被调用,可以在其中进行一些资源的释放和清理工作
  • 2.然而将资源的释放和清理放在finalize方法中非常不好,非常影响性能,严重时甚至会引起OOM(OutOfMemory),从java9以后就被标注为@Deprecated,不建议使用了
public class FinalizeTest {
    public static void main(String[] args) throws IOException {
        new Dog("狗子a");
        new Dog("狗子b");
        new Dog("狗子c");
        System.gc();
        // 这里需要暂停一下
        System.in.read();
    }

}


class Dog {
    private String name;

    public Dog(String name) {
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        // 这里使用synchronized保证当前输出的线程和当前正在运行的线程是一致的
        synchronized (Dog.class) {
            System.out.println(Thread.currentThread() + ":" + name + "被干掉了?");
            int a = 1/0;
        }
    }
}
输出结果:
Thread[Finalizer,8,system]:狗子c被干掉了?
Thread[Finalizer,8,system]:狗子b被干掉了?
Thread[Finalizer,8,system]:狗子a被干掉了?
  • 通过以上的案例我们可以看出:
    • 1.每个对象的finalize()方法的调用次序无法保证
      • 它跟垃圾回收次序有关,哪个对象先被回收,就先调用哪个对象的finalize()方法
      • 与引用队列中引用对象的先后顺序有关
    • 2.是Finalizer线程来调用的每个对象的finalize()方法,该线程是一个守护线程,优先级为8
    • 3.不能把System.in.read()注释掉,否则回来不及打印结果,因为Finalizer实际上是一个守护线程,如果所有的用户线程结束了,那么守护线程也会结束
    • 4.如果键finalize()方法中抛出一些异常,那么根本没有异常输出
      • 因为在Finalizer线程的run()方法中,再执行对象的finalize()方法时,会对finalize()方法抛出的异常进行捕获,但是什么都不会做,也就是会把抛出的异常给吞掉
        面试准备-Java基础
    • 5.垃圾回收时会立刻调用fianlize()方法吗?
      • 不会;垃圾回收时会先把finalizer引用对象加入到引用队列queue中,然后在Finalizer线程中移除并取出引用队列头,然后断开该引用在双向链表unfinalized中的对应结点,然后通过该结点获取对应的finalizer对象并执行对象的finalize()方法;

调试过程:调用Dog构造方法 --> 调用Object构造方法 --> 调用Finalizer类的static register(Object finalizee)方法 --> 在register方法中构造一个Finalizer对象,并将我们正在创建的dog对象传入 -->构造Finalizer对象的同时会将创建的这个finalizer对象链入一个unfinalized双向链表 --> 返回register()方法 --> 返回Object构造方法 --> 返回Dog构造方法

  • 也就是说unfinalized双向链表中链入的是finalizer对象,每个finalizer对象都引用了一个实现了finalize()方法的对象,即通过unfinalized双向链表管理了所有重写finalize方法的对象
	static void register(Object finalizee) {
		new Finalizer(finalizee);
	}
  • 当我们重写了某个类的finalize()方法后,如果需要实例化对象,在调用构造方法时,JVM都会将该对象包装成一个Finalizer对象,并加入unfinalized队列中(静态成员变量,双向链表结构);后期一旦我们创建的对象被回收后,那么引用他的finalizer对象就会被加入到一个引用队列中,就可以知道哪些创建的对象被回收了
  • 但是在将finalizer对象加入到一个引用队列前,该finalizer对象所关联的对象并不能被直接回收,因为如果该对象被立刻回收后,就无法调用重写的finalized()方法了,所以此时该对象暂时不能直接被垃圾回收器回收;也就是说第一次垃圾回收是回收不掉的,只有当对象的finalize()方法调用完了,再次发生垃圾回收的时候该对象才会被回收掉;
  • 实际上finalize()方法是由一个叫做finalizer的守护线程来执行的,在该线程的run()方法中,会从引用队列中取出即将被回收的对象,然后将引用该对象的finalizer对象从双向链表中断开,就拿到了finalizer对象,然后就可以去调用该对象的finalized()方法
    面试准备-Java基础
    总结:
    1.如果一个类重写了finalize()方法,那么在构造该类的实例时,会调用该类的构造方法,然后调用Object()类的构造方法,因为Object类时所有类的父类,这里存在构造方法的向上回溯过程;
    2.然后会跳转到Finalizer类中,调用它的static 修饰的register()方法,将我们实例化的对象传入register方法中
    3.在register()方法中会实例化一个Finalizer类的对象,并将我们实例化的对象传入finalizer对象中;即将我们实例化的对象包装成一个finalizer对象
    4.在Finalizer类的构造方法中,第一步是super(finalizee, queue),这里的queue是一个引用队列,类似于弱引用的用法,这样当该对象被回收的时候就会把该finalizer对象放入引用队列;第二步会将该finalizer对象加入到unfinalized双向链表中
    5.然后向下回溯到Object类的构造方法,向下回溯到我们需要构造的类的构造方法中,然后执行剩下的初始化对象的步骤
    6.由于Finalizer类中,register()是静态方法,unfinalized是一个静态变量,所以所有重写了finalize()方法的类在实例化对象时,都会执行1-5的操作,那么这个unfinalized双向链表中就保存了所有引用了重写了finalize()方法的对象的finalizer对象
    7.当我们创建对象没有强引用后,那么关联这些对象的finalizer对象就会被加入到引用队列referenceQueue中,但是这里与软引用、弱引用、虚引用有些不同,当这三个引用被加入到引用队列时说明他们引用的对象已经被垃圾回收器回收了;但是这里当关联对象的finalizer对象被加入到引用队列时,这些对象还不能被直接回收,因为后边还需要调用该对象的finalize()方法;(这里就是重写finalize()方法存在的问题,第一次垃圾回收时回收不掉的,只有当该对象的finalize()方法被调用完了,该对象才会被回收掉)
    8.具体是在一个叫做Finalizer的线程来执行finalize()方法的:在该线程的run方法中,会从引用队列中移除finalizer对象,然后将finalizer对象从双向链表unfinalized中断开,并获得该finalizer引用对象,然后通过finalizer引用来调用实际对象的finalize()方法,那么此时由于引用队列queue和双向链表unfinalized对finalizer的引用都没有了,那么finalizer就可以被回收,那么我们的实例对象也就可以被回收了
    面试准备-Java基础面试准备-Java基础面试准备-Java基础

面试准备-Java基础
面试准备-Java基础
Java线程中数大的优先级高

上一篇:一文带你了解Java 中的垃圾回收机制


下一篇:题解 AT3855 【[AGC020A] Move and Win】