[关闭]
@TryLoveCatch 2022-05-07T16:36:57.000000Z 字数 4851 阅读 2069

Java知识体系之引用类型

Java知识体系


什么对象可以被回收?

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

可达性分析算法

这种方案是目前主流语言里采用的对象存活性判断方案。基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。

上图中,object5, object6object7 便是不可达对象,视为“死亡状态”,应该被垃圾回收器回收。

在Java语言里,可作为GC Roots对象的包括如下几种:

引用

什么是引用(reference)?

在Java中,一切都被视为对象,引用则是用来操纵对象的途径。

  1. Car myCar = new Car();

创建一个Car的对象,并将这个新建的对象的引用存储在myCar中,此时myCar就是用来操作这个对象的引用。当我们获得myCar,就可以使用这个引用去操作对象中方法或者字段了。

  1. myCar.run();

注意,当我们尝试在一个未指向任何对象的引用上去操作对象时,就会遇到经典的空指针异常(NullPointerException)

  1. Car myCar = null;
  2. myCar.run();

GC与内存泄露

Java的一个重要优点就是通过垃圾收集器(Garbage Collection,GC)自动管理内存的回收,开发者不需要通过调用函数来释放内存。在Java中,内存的分配是由程序分配的,而内存的回收是由GC来完成。
GC为了能够正确释放对象,会监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

Android中,每一个应用程序对应有一个单独的Dalvik虚拟机实例,而每一个Dalvik虚拟机的大小是固定的(如32M,可以通过ActivityManager.getMemoryClass()获得)。这意味着我们可以使用的内存不是无节制的。所以即使有着GC帮助我们回收无用内存,还是需要在开发过程中注意对内存的引用。否则,就会导致内存泄露

所谓内存泄漏,我们不再需要的对象资源仍然与GC Roots存在可达路径,导致该资源无法被GC回收。

Android中的对象有着四种引用类型垃圾回收器对于不同的引用类型有着不同的处理方式。

强引用(StrongReference)

强引用是最普遍的引用。如果一个对象具有强引用,系统JVM垃圾回收绝对不会回收该对象。 当内存不足时,Java虚拟机(JVM)宁愿抛出OutOfMemoryError错误,终止程序,也不会随意回收强引用对象来解决内存不足的问题。

对象的强引用可以在程序中到处传递。很多情况下,会同时有多个引用指向同一个对象。

强引用的存在限制了对象在内存中的存活时间。假如对象A中包含了一个对象B的强引用,那么一般情况下,对象B的存活时间就不会短于对象A。如果对象A没有显式的把对象B的引用设为null的话,就只有当对象A被垃圾回收之后,对象B才不再有引用指向它,才可能获得被垃圾回收的机会。

这个特别要注意的就是,在Java中,非静态内部类会在其整个生命周期中持有对它外部类的强引用。

我们举个例子:

  1. public class MainActivity extends Activity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. new MyAsyncTask().execute();
  6. }
  7. private class MyAsyncTask extends AsyncTask<Void, Void, Void>{
  8. @Override
  9. protected Void doInBackground(Void... params) {
  10. // 模拟耗时任务
  11. return null;
  12. }
  13. @Override
  14. protected void onPostExecute(Void aVoid) {
  15. super.onPostExecute(aVoid);
  16. // 更新UI
  17. }
  18. }
  19. }

这段代码里,MainActivity被销毁时,MyAsyncTask中的耗时任务可能仍没有执行完成,所以MyAsyncTask会一直存活。此时,由于MyAsyncTask持有着其外部类,即MainActivity的引用,将导致MainActivity不能被垃圾回收。如果MainActivity中还持有着Bitmap等大对象,反复进出这个页面几次可能就会出现OOM Crash了。

将对象的引用显示的置为null,可以帮助垃圾收集器回收此对象。

软引用(SoftReference)

软引用用来描述一些还有用但是并非必须的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。

因此,表面上看来,软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。这一点可以很好地用来解决OOM的问题。

但是,在实践中,使用软引用作为缓存时效率是比较低的,系统并不知道哪些软引用指向的对象应该被回收,哪些应该被保留。过早被回收的对象会导致不必要的工作,比如Bitmap要重新从SdCard或者网络上加载到内存。
Android开发中,一种更好的选择是使用LruCache

弱引用(WeakReference)

一般指非必须的对象,比软引用还要弱,它只能生存到下一次垃圾回收前。在垃圾回收器运行的时候,如果对一个对象的所有引用都是弱引用的话,该对象会被回收。

我们调整一下上面例子中的代码,使用弱引用去避免内存泄露:

  1. public class MainActivity extends Activity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. new MyAsyncTask(this).execute();
  6. }
  7. private static class MyAsyncTask extends AsyncTask<Void, Void, Void>{
  8. private WeakReference<MainActivity> mainActivity;
  9. public MyAsyncTask(MainActivity mainActivity) {
  10. this.mainActivity = new WeakReference<>(mainActivity);
  11. }
  12. @Override
  13. protected Void doInBackground(Void... params) {
  14. // 模拟耗时任务
  15. return null;
  16. }
  17. @Override
  18. protected void onPostExecute(Void aVoid) {
  19. super.onPostExecute(aVoid);
  20. if (mainActivity.get() != null){
  21. // 更新UI
  22. }
  23. }
  24. }
  25. }

大家可以注意到,主要的不同点在于,我们把MyAsyncTask改为了静态内部类,并且其对外部类MainActivity的引用换成了弱引用(WeakReference)。这样,当MainActivity destroy的时候,由于MyAsyncTask是通过弱引用的方式持有MainActivity,所以并不会阻止MainActivity被垃圾回收器回收,也就不会有内存泄露产生了。

有同学可能会对此存疑:如果弱引用MainActivity destroy之前(即MainActivity正常工作时)被回收,这样不就导致mainActivity.get() == null,无法更新UI了吗?

需要注意的是,GC回收的是对象,在垃圾回收器运行的时候,如果对一个对象的所有引用都是弱引用的话,该对象会被回收。
MainActivity正常工作时,除了有mainActivity这个弱引用指向MainActivity,还会有其他强引用指向MainActivityActivityStack等)。所以,GC扫描的时候,对于MainActivity这个对象并非都是弱引用GC RootsMainActivity仍然是强可达的,所以,此时通过mainActivity.get()并不会返回null

虚引用(PhantomReference)

一个只被虚引用持有的对象可能会在任何时候GC回收。虚引用对对象的生存周期完全没有影响,也无法通过虚引用来获取对象实例,仅仅能在对象被回收时,得到一个系统通知(只能通过是否被加入到ReferenceQueue来判断是否被GC,这也是唯一判断对象是否被GC的途径)。

总结

ReferenceQueue 引用队列 软引用、弱引用和虚引用都可以和它集合使用,如果软引用或者弱引用中的对象被垃圾回收了,java虚拟机会吧这个引用加入到与之关联的引用队列当中。

缓存策略

一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。如何添加和获取缓存这个比较好理解,那么为什么还要删除缓存呢?这是因为不管是内存缓存还是硬盘缓存,它们的缓存大小都是有限的。当缓存满了之后,再想其添加缓存,这个时候就需要删除一些旧的缓存并添加新的缓存。

因此LRU(Least Recently Used)缓存算法便应运而生,LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。

LruCache

LruCache是个泛型类,主要算法原理是把最近使用的对象用强引用(即我们平常使用的对象引用方式)存储在 LinkedHashMap 中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作。

一般我们在图片缓存使用,大致代码如下:

  1. private static final float ONE_MIB = 1024 * 1024;
  2. // 7MB
  3. private static final int CACHE_SIZE = (int) (7 * ONE_MIB);
  4. private LruCache<String, Bitmap> bitmapCache;
  5. this.bitmapCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
  6. protected int sizeOf(String key, Bitmap value) {
  7. return value.getByteCount();
  8. }
  9. @Override
  10. protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
  11. ...
  12. }
  13. };

DiskLruCache

参考

Java中的强引用,软引用,弱引用,虚引用有什么用?
理解Android中的引用类型
JAVA垃圾回收机制
LruCache 源码解析
Android DiskLruCache完全解析,硬盘缓存的最佳方案
https://blog.csdn.net/ZyClient/article/details/109378125

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注