• 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成员变量和局部变量的定义

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

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

    1. class Car {
    2. String mColor; // 成员变量 作用范围是整个类
    3. float price; // 成员变量 作用范围是整个类
    4. private String test(String arg) { //这个arg是方法的形式参数,是局部变量
    5. // str1是在方法内部定义的变量,作用域为方法内部,当方法执行完后,生命周期就结束了,为局部变量
    6. String str1 = "hello";
    7. return str1;
    8. }
    9. }

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

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

    1. public class CarTest {
    2. static String DEFAULT_COLOR = "Whites";
    3. public static void main(String[] args) {
    4. Car c1 = new Car();
    5. c1.mColor = DEFAULT_COLOR;
    6. c1.price = 10000;
    7. c1.run();
    8. String redColor = "Red";
    9. Car c2 = new Car();
    10. c2.mColor = redColor;
    11. c2.price = 10000;
    12. c2.run();
    13. }
    14. }
    15. class Car {
    16. String mColor;
    17. float price;
    18. public void run() {
    19. }
    20. private String test(String arg) {
    21. String str1 = "hello";
    22. return str1;
    23. }
    24. }

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

     具体运行解析:

    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出来的对象,都是强引用。比如:

    1. public class ObjectTest {
    2. public static void main(String[] args) {
    3. //obj为强引用, 它指向一个 new Object() 对象
    4. //在方法内部定义,obj是一个局部变量,分配在栈内存中
    5. Object obj = new Object();
    6. }
    7. }

    如果一个对象具有强引用,只要强引用没有被销毁,那垃圾回收器绝不会回收它。当内存空间不足,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()对象回收。

    通过这个例子来说明:

    1. public class StrongReference {
    2. public static void main(String[] args) {
    3. Method m = new Method();
    4. m = null; //这句代码是关键
    5. System.gc();
    6. System.out.println(m);
    7. try {
    8. System.in.read(); //阻塞主线程,给垃圾回收线程时间工作
    9. } catch (IOException e) {
    10. e.printStackTrace();
    11. }
    12. }
    13. }
    1. public class Method {
    2. public Method() {
    3. }
    4. @Override
    5. protected void finalize() throws Throwable {
    6. super.finalize();
    7. //finalize方法只有在JVM执行gc时才会被执行
    8. System.out.println("当对象被GC回收时,会调用此方法");
    9. }
    10. }

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

    com.example.javademo.reference.Method@2a139a55

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

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

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

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

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

    4.2 软引用

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

    使用场景:

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

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

    1. public class SoftReferenceTest {
    2. public static void main(String[] args) {
    3. SoftReference<byte[]> sr = new SoftReference<>(new byte[1024 * 1024 * 5]);
    4. System.out.println(sr.get());
    5. System.gc();
    6. try {
    7. Thread.sleep(500);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. System.out.println(sr.get());
    12. }
    13. }

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

    4.3 弱引用

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

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

    1. public class WeakReferenceTest {
    2. public static void main(String[] args) {
    3. WeakReference wr = new WeakReference<>(new Method());
    4. System.out.println(wr.get());
    5. System.gc();
    6. try {
    7. Thread.sleep(500);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. System.out.println(wr.get());
    12. }
    13. }

    打印log:

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

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

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

    4.4 虚引用

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

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

     
    

    五、扩展

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

  • 相关阅读:
    CMake教程系列-05-选项及变量
    Windows环境变量 和 Linux环境变量
    Java高级---Spring Boot---1引言
    002 OpenCV dft 傅里叶变换
    二叉树习题总结
    基于象鼻虫损害优化算法求解单目标无约束问题并可视化分析附Matlab代码 (WDOA)
    剑指Offer 35.复杂链表的复制
    不知道显卡选择GeForce和Quadro哪个更好?超全科普来看
    基于单片机的多功能电子万年历系统
    如何缩减layout电路面积?减少晶体管的数量——以全加器为例【VLSI】
  • 原文地址:https://blog.csdn.net/u012514113/article/details/127976854