一、概述
本篇文章先引入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为强引用对象, 在方法内部定义,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如下:
com.example.javademo.reference.Method@2a139a55
对于强引用,不管你怎么调用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 虚引用
“虚引用”顾名思义,就是形同虚设,与其它几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。
五、扩展
这些基础概念有新的理解在补充进来