聊聊四大引用和引用对象

创建对象

引用可以看作java中的指针,在执行引擎看来,引用就是一个地址值,告诉它从哪里获取对象。而大多数对象存放在堆中。
在做链表题型时,当我们删除一个链表节点,其实操作的就是链表节点的引用,包括向对象数组、容器存放元素时,本质上存储的也是引用。

对象不一定只存在于堆,因为堆中的对象生命周期相对较长,而出于优化,JVM会试图为我们创建“局部对象”,一方面使得对象的生命周期相对虽短,另一方面也是为了减轻GC压力。

栈上分配对象就是其中一种优化。通过逃逸分析,确定一个对象的作用域不会超出当前方法之外的范围,那么就可以将这个对象的各个成员打散,拆分为成一个“局部变量拼凑而出的对象”,此时这个对象的回收就不交由GC管理了,而是随着栈帧出栈而销毁,减轻了GC的压力。

栈上分配对象的基础:
【1】逃逸分析:判断对象的作用域是否可能逃逸出函数体
【2】标量替换:将对象字段视为局部变量分配在栈中

栈上创建对象失败后(需要共享的对象,逃逸出方法范围),还会经过一个优化——在TLAB上创建对象。
TLAB线程本地分配缓冲区,是一个线程专用的内存分配区域,可以简单理解为线程专用的小块内存(其他线程可以访问,但是不能分配对象),占用的是堆内存的Eden部分。

线程在堆上创建对象时,总是加锁进行的,这样可以避免多个线程执行一个new语句时选择同一块内存创建对象而产生竞争。而TLAB的出现就是给每个线程提供一个私有的内存区域,每次线程先尝试在私有内存区创建,失败(空间不够用)才去共享堆内存(Eden或Old)加锁创建对象。如果对象太大,那么可能导致频繁触发GC,因此直接放入老年代。

总结:首先尝试栈上创建,如果该对象是共享对象则创建失败,尝试TLAB创建,对象过大则尝试在Eden区创建,如果对象过大则直接分配到老年代,老年代也放不下则进行一次Full GC,仍然无法分配则报错OOM

四种引用

JDK1.2之后引入了强引用、软引用、弱引用、虚引用。目的是更加灵活地控制对象的生命周期。他们的不同之处就是GC对待他们的处理方式。

除了强引用外,软弱虚都在java.lang.ref包下,同时还有一个包下可见的finalReference终结引用,用于实现finalize()调用。

强引用

一般情况下obj指针指向new object()这个对象,GC不会清零这个对象,只有obj指向了另一块内存或者null的时候,new object()不再被任何一个指针指向,他将被GC回收

对于一个普通的对象,如果没有其他引用关系,只有超过了引用的作用域或者显式的将强引用赋值为null,那么该对象便是“可回收的”,具体回收时间取决于GC,一般是Eden区满了后会进行一次minor GC,old区满了后进行一次Full GC(FullGC前通常也伴随一次minor GC)

软引用

软引用用来描述一些有用但不是必须的对象
内存空间足够,垃圾回收器不会回收它。反之,则回收。适用于缓存,不会OOM。
对于软引用关联着的时候,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存。

Object o = new Object();
SoftReference<Object> soft = new SoftReference<>(o);
o=null;//释放强引用
System.out.println(soft.get());//有时候会返回null

软引用可以和一个引用队列(referenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用(软引用对象本身的引用)就会被加入到与之关联的引用队列中。

应用场景:用作缓存,指向被缓存的对象
当垃圾回收器在某个时刻决定回收软可达对象时,会将软引用放入一个引用队列,然后由GC线程释放指向的对象。

如果内存足够,直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度(类似缓存)。当内存不足的时候,自动删除这部分缓存数据,从真正的来源查询这些数据

    static private long clock;
    private long timestamp;

软引用有一个时钟字段clock,由GC更新。timestamp在每次get()时会更新。也就是说内存是否吃紧,GC时间间隔是其中一个考虑因素(jvm代码底层也会考虑到当前可用堆内存之类的)。

弱引用

只有当垃圾回收器扫描到弱引用指向的对象时,才会回收它。生命周期比软引用更短。
弱引用也是用来描述非必须对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中使用java.lang.ref.WeakReference类来表示

被软引用关联的对象只有在内存不足的时候才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收

WeakReference<String> ss = new WeakReference<String>(new String("hello"));
System.out.println(ss.get());//hello
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(ss.get());//null

虚引用

在任何时候都可能被垃圾回收器回收,必须与引用队列关联使用
使用java.lang.ref.PhantomReference类表示

Object obj = new Object(); // 声明强引用
ReferenceQueue phantomQueue = new ReferenceQueue();
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;

虚引用主要用来跟踪对象被垃圾回收的活动(唯一目的就是跟踪垃圾回收进程,能够在对象被收集器回收时收到一个jvm通知)

引用对象Reference

reference对象可以分为四种状态:
【1】active:创建后的初始状态
【2】pending:存在引用队列,但是引用对象未入队(等待被处理,处于reference成员的pending链表中)
【3】enqueued:存在引用队列,引用对象已经入队
【4】inactive:绑定对象被回收,对象终结

以上的状态通过Reference成员的链表和变量去表示。

transient private Reference<T> discovered;  /* used by VM */
//静态的,所有reference共同使用同一个pending链表,因此需要加锁操作
private static Reference<Object> pending = null;

Discovered 表示要处理对象的下一个对象
如果处于active,表示discovered链表中下一个待处理对象。
如果处于pending,表示pending列表中的下一个对象

Pending对应pending状态,可以看作一条指向“即将要被处理的reference对象”的链表

Dicovered中的元素是即将进入pending链表的元素,被JVM使用。表示当前要处理对象的下一个对象

Pending是等待进入引用队列的引用链表(只有具有引用队列的reference才存在)。GC会将被回收对象的reference指针添加到pending链表,而后台线程reference-handler则会轮询pending链表,取出并移入引用队列
Discovered和pending都是由GC进行进行赋值,具体的赋值动作在JVM源码中有所体现。

Reference next;

Reference对象本身可以是一个链表结构,实际上引用队列仅仅维护一个头结点

引用队列的意义在于,增加了一种监控机制,通过在外部监控这个队列来判断对象是否被回收,而不是主动调用reference的相关方法。
(由于引用队列不光用户线程可以调用,后台线程referenceHandler也可以使用,因此需要进行同步)

reference对象本质上就是一个包装对象的对象,通过包装堆中的某些对象,使得其能够被JVM特殊处理。
JVM会开启一个后台线程referenceHandler(Reference的static块中创建了referenceHandler线程对象的实例,并调用了start方法),专门处理pending链表中的reference对象

        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }

方法内部(每一轮)不断处理队列的头结点(同步方法),如果当前reference具有引用队列对象,那么就将处理完毕的reference加入该队列。

注意:只有当reference绑定了一个引用队列,才会具有pending和enqueue状态(后台线程referenceHandler处理pending队列后放入引用队列),否则被回收后直接进入inactive状态

值得学习的地方:通过内部类定义一个线程类,并且在外部类静态块中去创建了该类的对象,并且设置为后台线程后start——一旦外部类被加载,那么就相当于自动开启了一个后台线程

引用对象的使用场景
例如:在某一个对象的生命周期方法的destroy方法内,创建引用对象和引用队列,并设置后台线程去检测。一段时间后拿到引用队列,看看当前对象绑定的reference对象是否被加入到队列中,如果不存在则说明有内存泄露的可能,可以尝试显示调用GC,重复多次,如果还是无法从队列中读取这个对象的reference对象,那么就可以断定发生内存泄露了。
然后就可以通知程序员,或者做一些后续处理,如dump出来堆内存信息,然后用jdk分析工具去找到内存泄露的路径。

finalize方法

java的对象起初只有两种状态:生或死。
而通过引用finalize方法,对象的状态可以分为三种:可达复活不可达
Java提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
当垃圾回收器发现没有引用指向一个对象时,会调用该对象的finalize方法。

当一个对象是“根结点可达”的,那么它便是可达状态。当jvm发现一个根结点不可达的对象时,会调用该对象的finalize()方法。finalize方法的执行,给了不可达对象一次尝试复活的机会,如果finalize中具有给对象添加引用的逻辑,那么该对象将进入复活状态。(finalize如果没有被重写,那么调用finalize方法将是没有意义的,因此JVM可能不会调用“没有必要”调用的finalize方法)
一个对象的finalize方法最多之后执行一次(最多只能复活一次,如果每次都调用finalize方法,那么很可能出现内存泄露的情况)。
如果一个不可达对象的finalize没有被重写,或者已经被调用过一次,那么它将被视为垃圾对象被回收。

对象的回收

一个对象被回收之前,至少需要经过两次标记
【1】如果对象处于根结点不可达状态,它将经过第一次标记
【2】对于被第一次标记的对象,如果它的finalize方法没有被重写,或者已经被调用过一次,那么它将被标记为垃圾对象并被回收。

如果finalize方法被重写,但是没有被执行,将会被插入到finalize队列,java的一个专门负责扫描finalize队列的后台线程,会在某个时间段对存放在finalize队列的对象,调用finalize方法。不过这个线程的优先级很低,因此finalize方法的执行是无法被包装的(无法保证什么时候执行,甚至是否被执行)

finalize队列其实也是一个引用队列,finalize方法的调用依赖终结器引用对象,而存入finalize队列前的对象将会被封装为终结器引用对象

终结器引用

终结引用器引用用于实现对象的finalize方法,由JVM创建,内部配合引用队列使用

final class Finalizer extends FinalReference<Object> 

GC时,终结器引用入队,由finalize后台线程通过终结器引用找到被引用对象,调用它的finalize方法(如果没有调用的必要就不会调用了),调用finalize方法后一个对象可能有两种状态:复活和不可达

内存泄露问题

对象都是有生命周期的,如果长生命周期的对象持有短生命周期的引用,就有可能出现内存泄露。
例如一个对象仅仅在方法内使用一次,但是由于外部的成员引用了该对象,因此这个对象无法被释放,生命周期被强行拉长(但是却没有被再次使用)

public E pop(){
    if(size == 0)
    return null;
else{
        E e = (E) elementData[--size];
        elementData[size] = null;
        return e;
    }
}

elementData[size] = null的目前就是为了及时释放强引用,使得elementData[size]对应的对象可以被GC处理。否则可能整个集合对象被GC时,集合成员对应的内存才会被释放

流操作、一些框架、JDBC等我们都必须记得调用close()方法,实质上就是防止内存泄露。

Session session=sessionFactory.openSession();

Session是一个短周期对象,而sessionFactory是一个长周期对象,使用完成后,我们必须调用 session.close(),如果在这之前遇到异常导致方法不能调用就会造成内存泄露,因此close()往往放到finally语句中执行

内存泄露的排查与解决

内存泄露的几个表现就是内存吃紧“抖动”频繁full GC、“OOM”、系统在一段时间内变慢(频繁GC导致STW造成的停顿明显)。

问题排除的第一步总是Jps,拿到java进程的PID。然后拿到pid后,去使用其他JDK提供的工具。例如,虚拟机统计信息监视工具Jstat,可以拿到内存和GC相关的一些信息,例如发送过几次GC、总共耗时多长时间、各区的内存占比等。
然后可以使用Jmap分析一些堆内存的情况,看看有哪些对象大小比较异常,可能是由于对象创建后无法被GC回收而造成内存泄露。然后根据对象所属类型,可以定位到具体代码(多半是存在无效的长生命周期引用,使得对象无法被GC释放)

threadLocal

threadLocal是线程安全的一种解决方案,更具体的,它算是一种线程隔离机制
每个线程对象都一个独立的ThreadLocalMap成员,而ThreadLocalMap的entry就是threadLocal。
现在有一个threadLocal类型的共享变量,我们调用threadLocal.set(123),本质上是向当前线程的ThreadLocalMap底层数组放入一个key为当前threadLocal对象,值为Integer类型123的entry对象
而get方法,也是先通过当前threadLocal对象计算出index,然后从每个线程对应的ThreadLocalMap取出entry,然后拿到value。

threadLocalMap是我们的钱包,threadLocal是不同的银行,threadLocal.set(123)就是去不同的银行办理不同的银行卡。

static class Entry extends WeakReference<ThreadLocal<?>> {
    
    Object value;	//指向值的强引用

    Entry(ThreadLocal<?> k, Object v) {
        super(k);  //指向threadLocal是一个弱引用
        value = v;
    }
}

threadLocal内存泄露问题

值得注意的是,ThreadLocalMap的key是一个弱引用,也就是说entry对象的key有可能会被GC默默回收掉(前提是key指向的threadLocal对象不存在强引用指向),但是entry属于ThreadLocalMap,他被ThreadLocalMap强引用指向,并且value指针也是一个强引用,因此他们的生命周期实际上可以是与线程绑定的
这就造成了内存泄露的问题:
虽然threadLocal会被释放,但是value与值直接的引用仍然是强引用,也就是说key的引用最终会变为null,但是value仍然是指向对象的,因此使用弱引用只是一定程度上减轻了内存泄露,但是没有根治内存泄露。
解决方法:当不使用一个threadLocal时主动调用threadLocal的remove方法,从某个线程中删除对应entry。
这段内存泄露发生在key指针指向null,同时线程对象没有被回收的时间。
但是如果使用的是线程池线程不会被真正销毁,可能会出现真正的内存泄露

Java为了最小化内存泄露的可能性和影响,在ThreadLocal的get、set的时候,都会检查当前key所指的对象是否为null,是则删除对应的value,让它能被GC回收

WeakHashMap

weakHashMap底层entry是一个弱引用类型,当key不被外部强引用时,它将在GC时被回收。但是回收的也仅仅是key指向对象。value和entry对象并没有被回收。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> 

weakHashMap主要通过expungeStaleEntries来实现:移除内部不使用的条目,从而达到对象本身以及map中的entry共同释放。
可以看作是“渐进式删除”,因为增删改查、size等方法都会调用该方法

private Entry<K,V>[] getTable() {
    expungeStaleEntries();
    return table;
}

缺点:如果内存溢出之前没有访问过,相当于功能失效。
也就是说,释放内存的行为不是自动完成的,而是访问的时候捎带释放内部不使用的对象。

weakHashMap底层数组的成员entry继承了weakReference,它的K是弱引用,而V仍然是被value指针指向的强引用。
K在GC之后就被清除了,而V是通过调用expungeStaleEntries方法之后清除的

private void expungeStaleEntries() {
//从引用队列中拿到“失效”的entry,进行移除操作
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>) x;
            int i = indexFor(e.hash, table.length);

            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev; //首节点
            while (p != null) { 
                Entry<K,V> next = p.next; //哈希冲突链表的情况处理
                if (p == e) {
		//下面会执行释放entry的逻辑
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
		//value指针置空,帮助value指向的原对象GC
                        e.value = null;  
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

另一种解决方案:将值插入weakHashMap之前,保证成一个weakReference,拿到的时候使用get进行解包。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值