编程知识 cdmana.com

Java强软弱虚引用和ThreadLocal工作原理(一)

一、概述

        本篇文章先引入java的四种引用在android开发中的使用,然后结合弱引用来理解ThreadLocal的工作原理。

二、JVM名词介绍

        在提出四种引用之前,我们先提前说一下 Java运行时数据区域   虚拟机栈  堆  垃圾回收机制 这四个概念。

2.1 java运行时数据区域

        java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机的进程的启动而一直存在,有些区域是依赖用户线程的启动/结束 对应的 建立/销毁。 根据java虚拟机的规范,java虚拟机所管理的内存包括以下几个运行时数据区域:

 其中方法区堆区是所有线程共享的,而 虚拟机栈 本地方法栈  程序计数器 是属于线程私有。

先介绍在本文中必须要提前理解的 虚拟机栈  和 

2.2 虚拟机栈

        Java虚拟机栈(Java Virtual Machine Stack)是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行时的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储 局部变量表  操作数栈  动态链接  方法返回地址等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。虚拟机栈结构示意图如下:

局部变量表存放了编译期可知的各种java虚拟机基本类型变量:

1.基本数据类型(boolean  byte  char short int  float  long  double)

2.对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也有可能是指向一个代表对象的句柄)

3. 方法Return Address类型(指向了一条字节码指令的地址)。

2.3 java堆

        对于java应用程序来说,java堆(Java Heap)是虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,在此内存区域的唯一目的就是存放对象实例。在《java虚拟机规范》中对java堆的描述:“几乎所有的对象实例以及数组都应在堆上分配”。

堆,是GC(Garbage Collection,垃圾回收器)执行垃圾回收的重点区域。

Java 堆从 GC 的角度还可以细分为:新生代(Eden 区、SurvivorFrom 区和 SurvivorTo 区)和老年代。示意图如下:

了解 虚拟机栈 和 堆 这两个概念后,对我们下面画代码运行时内存分配图有帮助。

2.4 垃圾回收机制

1. 引用计数算法

判断对象是否存活的算法是这样的:在对象中添加一个引用计数器,每当有一个地方
引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的,

最大的缺陷:A如果引用B,B引用A 但是其他对象没有任何的引用A和B,相互存相互依赖,无法被垃圾回收。

2.  可达性分析算法

当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是
通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。如图3-1所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。示意图:

        Java虚拟机的内存区域分成五块,其中三个是线程私有的:程序计数器、Java虚拟机栈、本地方法栈;另两个是线程共享的:Java堆、方法区。线程私有的区域等到线程结束时(栈帧出栈时)会自动被释放,空间较易被清理。而线程共享的Java堆和方法区中空间较大且没有线程回收容易,GC垃圾回收主要负责这部分。

可作为GC Roots的对象:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象;

2)方法区中类静态属性引用的对象;

3)方法区中常量引用的对象;

4)本地方法栈中JNI(即一般说的Native方法)引用的对象;

GC Roots即指对象的引用

对对象的操作是通过引用来实现的引用是指向堆内存对象的指针

如果当前对象没有引用指向,那该对象无法被操作,被视为垃圾。

可达性分析算法主要从对象的引用出发,寻找对象是否存在引用,若不存在进行标识处理,为GC做准备

如何判断一个对象真正的死亡?

要真正宣告一个对象死亡,至少需要经历两次标记过程:

(1)第一次标记
在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记,进行一次筛选,此对象是否有必要执行finalize()方法

没有必要执行情况:对象没有覆盖finalize()方法;finalize()方法已被JVM调用过一次;这两种情况可认为对象已死,可以回收;

有必要执行:对有必要执行finalize()方法的对象被放入F-Queue队列中,稍后JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发此方法;

(2)第二次标记
GC将F-Queue队列中的对象进行第二次小规模标记,finalize()方法是对象逃脱死亡的最后一次机会

A)若对象在finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出“即将回收”的集合;

B)若对象没有,也可认为对象已死,可以回收了。

finalize()方法执行时间不确定,甚至是否被执行也不确定(Java程序不正常退出),且运行代价高昂,无法保证各对象调用顺序。


 

三、代码运行内存分配图

先复习一下java成员变量和局部变量的定义

成员变量:成员变量是指在类内   方法外定义的变量;

局部变量:是指在方法中定义的变量,作用范围是其所在的方法。

class Car {
    String mColor;  // 成员变量  作用范围是整个类
    float price;    // 成员变量  作用范围是整个类

    
    private String test(String arg) {  //这个arg是方法的形式参数,是局部变量
// str1是在方法内部定义的变量,作用域为方法内部,当方法执行完后,生命周期就结束了,为局部变量
        String str1 = "hello";
        return str1;
    }
}

1、在类中位置不同:    局部变量在方法中,成员变量在方法外。
2、在内存中位置不同:局部变量在栈内存中成员变量在堆内存中
3、生命周期不同:
     局部变量随方法的调用而存在,当方法被执行时局部变量被创建,当方法执行完毕出栈,局部变量跟随方法消失。
    成员变量随对象的存在而存在,当对象被创建之后,成员变量作为对象的属性,会与对象一同被存储在堆内存中,一直到对象消失才跟着消失。
4、初始化值不同:
    局部变量定义之后,必须主动赋值初始化才能被使用。
    成员变量作为对象的一部分,当对象被创建后,成员变量会被自动赋默认值(一般基本数据类型赋0,引用数据类型赋null)。
5、作用范围不同:局部变量只在其所在的方法内部生效,成员变量在其所在的整个类中生效。

我们看一个简单的java程序:

public class CarTest {
    static String DEFAULT_COLOR = "Whites";

    public static void main(String[] args) {
        Car c1 = new Car();
        c1.mColor = DEFAULT_COLOR;
        c1.price = 10000;
        c1.run();

        String redColor = "Red";
        Car c2 = new Car();
        c2.mColor = redColor;
        c2.price = 10000;
        c2.run();
    }
}

class Car {
    String mColor;
    float price;

    public void run() {
    }

    private String test(String arg) {
        String str1 = "hello";
        return str1;
    }
}

代码运行时的内存分析图:

 具体运行解析:

1. 运行程序,CarTest.java由编译器编译就会变为CarTest.class,将CarTest.class加入方法区,检查字节码是否有常量,若有(DEFAULT_COLOR)加入运行时常量池;

2. 遇到main方法,创建一个栈帧,入虚拟机栈,然后开始运行main方法中的程序

3. Car c1 = new Car(); 第一次遇到Car这个类,所以将Car.java编译为Car.class文件,然后加入方法区,跟第一步一样。然后new Car()。就在堆中创建一块区域,用于存放创建出来的实例对象,地址为0X0010.其中有两个属性值 color和num。默认值是null 和 00


4. 然后通过c1这个引用变量去设置color和num的值,

5. 调用run方法,然后会创建一个栈帧,用来装run方法中的局部变量的,入虚拟机栈,run方法结束之后,该栈帧出虚拟机栈。又只剩下main方法这个栈帧了

6. 接着又创建了一个Car对象,所以又在堆中开辟了一块内存,之后就是跟之前的步骤一样了。

四、强软弱虚引用

        从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

4.1 强引用

官方定义:

强引用是最常见的一种,一般在代码中直接通过new出来的对象,都是强引用。比如:

public class ObjectTest {

    public static void main(String[] args) {

//obj为强引用, 它指向一个 new Object() 对象
//在方法内部定义,obj是一个局部变量,分配在栈内存中

        Object obj = new Object();

    }
}

如果一个对象具有强引用,只要强引用没有被销毁,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

加入一下我的理解

我们先来画一下运行时内存分析图:

就是说你的代码在运行期间,如果一个对象(new Object())具有强引用(指强引用 obj ),就算你执行System.gc()方法,它(指new Object()对象 )也不会被垃圾回收器回收。

那么就有一个问题,Object obj=new Object(),obj作为强引用存在虚拟机栈中而new Object()作为对象存在于堆中,当obj的作用域结束,对应的虚拟机栈消失,obj引用也同时消失,但new Object()对象却仍然存在于堆中,“JVM必定不会回收这个对象” ,那jvm不是很容易就OOM了吗?
 

你所考虑的问题,写垃圾回收机制的工程师肯定也考虑到了这点,第一种程序员设置obj=null,这样gc就会主动地将new Object()对象回收。

通过这个例子来说明:

public class StrongReference {

    public static void main(String[] args) {

        Method m = new Method();

        m = null; //这句代码是关键

        System.gc();

        System.out.println(m);

        try {
            System.in.read(); //阻塞主线程,给垃圾回收线程时间工作
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
public class Method {

    public Method() {
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        //finalize方法只有在JVM执行gc时才会被执行
        System.out.println("当对象被GC回收时,会调用此方法");
    }
}

1. 如果不设置 m = null  打印log如下:

[email protected]

对于强引用,不管你怎么调用System.gc()方法,这个new Objcet() 对象都不会被回收。

2. 手动设置 m = null 后,打印log如下:

null
当对象被GC回收时,会调用此方法

这种方式是程序员手动设置强引用 m = null 后,这个new Objcet()  对象就会被回收了。

写到这里,你肯定会杠精一下,平时做项目写代码,好像并没有特意去设置强引用对象为空啊,还是一样的跑咧。是的,没错,其实你也不用太担心这些问题,因为垃圾回收器已经默默的帮我们把这些事情做了,当你的程序运行结束后,这些强引用对象,垃圾回收机制会处理但并不是立即处理,至于原理,参考2.4 垃圾回收机制 可达性分析算法 

4.2 软引用

 如果一个对象只具有软引用,则内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。(备注:如果内存不足,随时有可能被回收。)。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

使用场景:

图片缓存。图片缓存框架中,“内存缓存”中的图片是以这种引用保存,使得 JVM 在发生 OOM 之前,可以回收这部分缓存。

如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出这时候就可以使用软引用

public class SoftReferenceTest<T> {

    public static void main(String[] args) {

        SoftReference<byte[]> sr = new SoftReference<>(new byte[1024 * 1024 * 5]);

        System.out.println(sr.get());

        System.gc();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(sr.get());
    }
}

 总结:虚引用就是只要运行垃圾回收器,当内存不足的情况下才会被回收

4.3 弱引用

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。

每次执行GC的时候,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

public class WeakReferenceTest {

    public static void main(String[] args) {

        WeakReference<Method> wr = new WeakReference<>(new Method());

        System.out.println(wr.get());

        System.gc();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(wr.get());


    }
}

打印log:

当对象引用被GC回收时,会调用此方法
null

总结:虚引用就是只要运行垃圾回收器,则引用对象就会被回收

弱引用比较重要,在Android开发中,很多地方用到,比如HandlerThread   ActivityThread   AMS 都有用到。

4.4 虚引用

“虚引用”顾名思义,就是形同虚设,与其它几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收

虚引用主要用来跟踪对象被垃圾回收器回收的活动。

五、扩展

        这些基础概念有新的理解在补充进来

版权声明
本文为[broadview_java]所创,转载请带上原文链接,感谢
https://blog.csdn.net/u012514113/article/details/127976854

Scroll to Top