@TryLoveCatch
2022-05-07T16:36:57.000000Z
字数 4851
阅读 2088
Java知识体系
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
这种方案是目前主流语言里采用的对象存活性判断方案。基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。
上图中,object5
, object6
和 object7
便是不可达对象
,视为“死亡状态”,应该被垃圾回收器回收。
在Java语言里,可作为GC Roots对象的包括如下几种:
什么是引用(reference)?
在Java中,一切都被视为对象,引用则是用来操纵对象的途径。
Car myCar = new Car();
创建一个Car
的对象,并将这个新建的对象的引用存储在myCar
中,此时myCar
就是用来操作这个对象的引用
。当我们获得myCar
,就可以使用这个引用
去操作对象中
的方法
或者字段
了。
myCar.run();
注意,当我们尝试在一个未指向任何对象的引用上去操作对象时,就会遇到经典的空指针异常(NullPointerException)
。
Car myCar = null;
myCar.run();
Java的一个重要优点就是通过垃圾收集器(Garbage Collection,GC)
自动管理内存的回收,开发者不需要通过调用函数来释放内存。在Java
中,内存的分配
是由程序
分配的,而内存的回收
是由GC
来完成。
GC
为了能够正确释放对象
,会监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC
都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。
在Android
中,每一个应用程序对应有一个单独的Dalvik
虚拟机实例,而每一个Dalvik
虚拟机的大小是固定的(如32M,可以通过ActivityManager.getMemoryClass()获得)。这意味着我们可以使用的内存不是无节制的。所以即使有着GC
帮助我们回收无用内存,还是需要在开发过程中注意对内存的引用。否则,就会导致内存泄露
。
所谓
内存泄漏
,我们不再需要的对象资源仍然与GC Roots
存在可达路径,导致该资源无法被GC
回收。
Android
中的对象有着四种引用类型
,垃圾回收器
对于不同的引用类型
有着不同的处理方式。
强引用
是最普遍的引用。如果一个对象具有强引用
,系统JVM垃圾回
收绝对不会回收该对象。 当内存不足时,Java虚拟机(JVM)
宁愿抛出OutOfMemoryError
错误,终止程序,也不会随意回收强引用
对象来解决内存不足的问题。对象的强引用可以在程序中到处传递。很多情况下,会同时有多个引用指向同一个对象。
强引用的存在限制了对象在内存中的存活时间。假如对象A中包含了一个对象B的强引用,那么一般情况下,对象B的存活时间就不会短于对象A。如果对象A没有显式的把对象B的引用设为null的话,就只有当对象A被垃圾回收之后,对象B才不再有引用指向它,才可能获得被垃圾回收的机会。
这个特别要注意的就是,在Java中,非静态内部类会在其整个生命周期中持有对它外部类的强引用。
我们举个例子:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MyAsyncTask().execute();
}
private class MyAsyncTask extends AsyncTask<Void, Void, Void>{
@Override
protected Void doInBackground(Void... params) {
// 模拟耗时任务
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
// 更新UI
}
}
}
这段代码里,MainActivity
被销毁时,MyAsyncTask
中的耗时任务可能仍没有执行完成,所以MyAsyncTask
会一直存活。此时,由于MyAsyncTask
持有着其外部类
,即MainActivity
的引用,将导致MainActivity
不能被垃圾回收。如果MainActivity
中还持有着Bitmap
等大对象,反复进出这个页面几次可能就会出现OOM Crash
了。
将对象的引用显示的置为null
,可以帮助垃圾收集器
回收此对象。
软引用
用来描述一些还有用但是并非必须的对象,在Java
中用java.lang.ref.SoftReference
类来表示。对于软引用
关联着的对象,只有在内存不足
的时候JVM
才会回收该对象。
因此,表面上看来,软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。这一点可以很好地用来解决OOM
的问题。
但是,在实践中,使用软引用
作为缓存时效率是比较低的,系统并不知道哪些软引用
指向的对象应该被回收,哪些应该被保留。过早被回收的对象会导致不必要的工作,比如Bitmap要重新从SdCard或者网络上加载到内存。
在Android
开发中,一种更好的选择是使用LruCache
。
一般指非必须的对象,比
软引用
还要弱,它只能生存到下一次垃圾回收
前。在垃圾回收器运行的时候,如果对一个对象的所有引用都是弱引用
的话,该对象会被回收。
我们调整一下上面例子中的代码,使用弱引用去避免内存泄露:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MyAsyncTask(this).execute();
}
private static class MyAsyncTask extends AsyncTask<Void, Void, Void>{
private WeakReference<MainActivity> mainActivity;
public MyAsyncTask(MainActivity mainActivity) {
this.mainActivity = new WeakReference<>(mainActivity);
}
@Override
protected Void doInBackground(Void... params) {
// 模拟耗时任务
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
if (mainActivity.get() != null){
// 更新UI
}
}
}
}
大家可以注意到,主要的不同点在于,我们把MyAsyncTask
改为了静态内部类
,并且其对外部类MainActivity
的引用换成了弱引用(WeakReference)
。这样,当MainActivity
destroy
的时候,由于MyAsyncTask
是通过弱引用
的方式持有MainActivity
,所以并不会阻止MainActivity
被垃圾回收器回收,也就不会有内存泄露
产生了。
有同学可能会对此存疑:如果弱引用
在MainActivity
destroy
之前(即MainActivity正常工作时)被回收,这样不就导致mainActivity.get() == null
,无法更新UI
了吗?
需要注意的是,
GC
回收的是对象,在垃圾回收器运行的时候,如果对一个对象的所有引用
都是弱引用
的话,该对象会被回收。
在MainActivity
正常工作时,除了有mainActivity
这个弱引用指向MainActivity
,还会有其他强引用
指向MainActivity
(ActivityStack
等)。所以,GC
扫描的时候,对于MainActivity
这个对象并非都是弱引用
,GC Roots
与MainActivity
仍然是强可达的,所以,此时通过mainActivity.get()
并不会返回null
。
一个只被
虚引用
持有的对象可能会在任何时候
被GC
回收。虚引用
对对象的生存周期完全没有影响,也无法通过虚引用
来获取对象实例,仅仅能在对象被回收时,得到一个系统通知(只能通过是否被加入到ReferenceQueue
来判断是否被GC
,这也是唯一判断对象是否被GC
的途径)。
ReferenceQueue 引用队列 软引用、弱引用和虚引用都可以和它集合使用,如果软引用或者弱引用中的对象被垃圾回收了,java虚拟机会吧这个引用加入到与之关联的引用队列当中。
一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。如何添加和获取缓存这个比较好理解,那么为什么还要删除缓存呢?这是因为不管是内存缓存还是硬盘缓存,它们的缓存大小都是有限的。当缓存满了之后,再想其添加缓存,这个时候就需要删除一些旧的缓存并添加新的缓存。
因此LRU(Least Recently Used)缓存算法便应运而生,LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。
LruCache是个泛型类,主要算法原理是把最近使用的对象用强引用(即我们平常使用的对象引用方式)存储在 LinkedHashMap 中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作。
一般我们在图片缓存使用,大致代码如下:
private static final float ONE_MIB = 1024 * 1024;
// 7MB
private static final int CACHE_SIZE = (int) (7 * ONE_MIB);
private LruCache<String, Bitmap> bitmapCache;
this.bitmapCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
...
}
};
Java中的强引用,软引用,弱引用,虚引用有什么用?
理解Android中的引用类型
JAVA垃圾回收机制
LruCache 源码解析
Android DiskLruCache完全解析,硬盘缓存的最佳方案
https://blog.csdn.net/ZyClient/article/details/109378125