一、对象可达性
Java虚拟机有5个不同级别的对象的可达性。
●强可达(Strongly reachable)
如果一个对象可以被一些线程直接使用而不用通过其他引用对象(reference objects),那么它就是强可达。一个新创建的对象对创建它的线程来讲就是强可达的。
这是我们知道并且一直在使用的引用类型(译注:通常被new出来的对象都是强可达的,他们的引用就是强引用)。任何通过强引用所使用的对象(在一个活动线程中)都不会被GC回收。
●软可达(Softly reachable)
如果一个对象没有强可达性,但是它可以通过一个软引用(soft reference)来使用,那么它就具有软可达性。
只有当系统需要更多内存时,GC才会回收具有软可达性的对象。在内存不足前,GC保证一定回收软可达的对象。
有可能我们会在代码中写下这么几行:“嘿,我想要把一些数据保存在内存中。但只要JVM快把内存用光的时候,就可以直接将这些东西回收并将这些引用置为null。我会在代码里面处理这种情况。”关于软引用(SoftReference)何时应该被回收的算法依赖于不同的JVM发行版本。它往往是一个跟引用(reference)的使用频率和使用间隔有关的函数。
软引用可用来实现内存敏感的高速缓存.但是具体的行为还是得依赖于JVM。并且多少跟内存回收机制有关,保障很少并且跟具体的JVM发行版本有关。为了缓存的可靠(及其他更多特性),大多数人都会选用像Ehcache而不是用软引用实现自己的缓存。但在一些场合,使用软引用确实可以让代码非常优雅、简洁。
●弱可达(Weakly reachable)
如果一个对象既没有强可达性,也没有软可达性,但是它可以通过一个弱引用(weak reference)来使用,那么他就具有弱可达性。当弱引用指向的弱可达对象没有其他的引用,那么这个对象就会被回收。
弱引用不能阻止垃圾回收机制清理他指向的引用。弱引用最常见的使用情景是通过WeakHashMap。它是一种简单地将对象的生命周期跟Map中对象的索引域(key)绑定的方式。只有当WeakHashMap中的Key是强可达,也就是WeakHashMap中的数据域(Data域)的对象,在应用程序的其他地方有别的引用的时候,它里面的值才不会被回收。一旦应用程序中没有其他对WeakHashMap中对象的引用,那么它的所有的key就会变成弱可达,不需要用户的额外干预,所有WeakHashMap中的对象都会被清除。这是一种优雅地防止内存泄露的方式。
●虚可达(Phantom reachable)
如果一个对象既没有强可达性,也没有软可达性、弱可达性,他已经被终结(finalized),并且有一些虚引用(phantom reference)指向它,那么它就具有虚可达性。
虚引用(PhantomReference)指向的对象是不能被取回使用的。它的get()方法永远返回null,所以它有什么用呢?
所有的引用类型都允许在构造函数中指定一个引用队列(ReferenceQueue)。从语义上讲一个虚引用(PhantomReference)以什么方式、何时入队让对象终结(finalization)以一种更好、更健壮的方式进行。
●不可达(Unreachable)
当一个对象不能通过以上的方式指向,那么这个对象就变得不可达,并因此适合被回收。
二、可达性分析算法
在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图3-1所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
三、引用的概念及应用
在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
●强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
●软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之后,get()方法将返回null。
为什么要使用SoftReference?在不使用SoftReference的时候,程序所需要的数据要么保存在内存中,如果数据量比较大的话,会占用比较多的内存;要么保存在介质中,需要时进行加载,在性能上肯定比不上直接读取内存。SoftReference算是比较折中的一种解决方案,它先将数据保存在内存中,在内存不足的情况下,才将数据回收。软引用可用来实现内存敏感的高速缓存.但是具体的行为还是得依赖于JVM。并且多少跟内存回收机制有关,保障很少并且跟具体的JVM发行版本有关。为了缓存的可靠(及其他更多特性),大多数人都会选用像Ehcache而不是用软引用实现自己的缓存。
示例:
大数据对象:照片Image
/** * 图片,包含大量数据 * @author Grucee */ public class Image { private byte[][] buffer; private int id; public Image(int id, byte[][] data) { this.id = id; buffer = data; } public int getId() { return this.id; } public String toString() { return "image[" + this.id + "]"; } }
缓存管理ImageCacheManager
/** * 图片缓存 * * @author Grucee */ public class ImageCacheManager { private static ImageCacheManager imageCache = null; private ImageCacheManager() {} /** * 作为一个管理器,当然要单例了,不允许随便new一个就当做管理器 * @return */ public static ImageCacheManager instance() { if (imageCache == null) { synchronized (ImageCacheManager.class) { if (imageCache == null) { imageCache = new ImageCacheManager(); } } } return imageCache; } // 保存数据的地方 private Map<Integer, SoftReference<Image>> cache = new HashMap<Integer, SoftReference<Image>>(); public void put(Image image) { cache.put(image.getId(), new SoftReference<Image>(image)); } private Image loadImage(int id) { byte[][] data = new byte[1024][1024]; data[1023][1023] = 1; return new Image(id, data); } public Image get(int id) { SoftReference<Image> ref = cache.get(id); // 没有放入过缓存,第一次加载 if (ref == null) { Image loadFirst = loadImage(id); // 放入缓存 put(loadFirst); return loadFirst; } // 下面的情况是该图片已经被加载过,但是可能由于内存不足,又被回收了 // 为了便于理解,使用了if else分支方式,其实else不是必须的。 // 这两个分支,不管哪一个返回的都是对image的强引用 Image cachedImage = ref.get(); if (cachedImage == null) { // 从真实介质中读取(此处模拟这个操作) Image imageGetFromMedia = loadImage(id); // 缓存起来 put(imageGetFromMedia); System.out.println("get image:" + id + " from media."); return imageGetFromMedia; } else { System.out.println("get image:" + id + " from cache."); return cachedImage; } } }
测试类
/** * 测试类 * @author Grucee */ public class SoftReferenceTest { private ImageCacheManager cacheMgr = ImageCacheManager.instance(); /** * 设置启动参数 * @param args */ public static void main(String[] args) { SoftReferenceTest tester = new SoftReferenceTest(); //先把数据load进来 for (int i = 0; i < 10; i++) { tester.cacheMgr.get(i); } //一张特殊的图片,这里保存了一个对这张图片的强引用,所以该对象不会被回收 Image myPhoto = tester.cacheMgr.get(7); System.out.println("--------------------------"); System.out.println(myPhoto); System.out.println("--------------------------"); //打印图片 for (int i = 0; i < 10; i++) { tester.printImage(i); } } public void printImage(int id) { System.out.println(cacheMgr.get(id)); } }
启动设置内存堆大小为10M
-Xmx10M -Xms10M -verbose:gc
测试一:
当测试类SoftReferenceTest中特殊图片是第8张是,运行结果
[GC (Allocation Failure) 2048K->1972K(9728K), 0.0048794 secs] get image:8from cache. -------------------------- image[8] -------------------------- [GC (Allocation Failure) 2898K->3066K(9728K), 0.0019205 secs] get image:0from media. image[0] get image:1from media. image[1] [GC (Allocation Failure) 5114K->5338K(9728K), 0.0007850 secs] [Full GC (Ergonomics) 5338K->4866K(9728K), 0.0032548 secs] get image:2from media. image[2] get image:3from media. image[3] [GC (Allocation Failure) 6914K->7114K(8704K), 0.0006606 secs] [Full GC (Ergonomics) 7114K->6907K(8704K), 0.0028911 secs] get image:4from media. image[4] [Full GC (Ergonomics) 7931K->7912K(8704K), 0.0075340 secs] get image:5from media. image[5] [GC (Allocation Failure) 3014K->3022K(9216K), 0.0004105 secs] get image:6from media. image[6] [GC (Allocation Failure) 4045K->4118K(9216K), 0.0011056 secs] get image:7from media. image[7] get image:8from cache. image[8] [GC (Allocation Failure) 5141K->5262K(9216K), 0.0005411 secs] get image:9from media. image[9] |
测试二:
当测试类SoftReferenceTest中特殊图片是第7张是,运行结果
[GC (Allocation Failure) 2048K->1972K(9728K), 0.0048794 secs] get image:7from media. -------------------------- image[7] -------------------------- [GC (Allocation Failure) 2898K->3066K(9728K), 0.0019205 secs] get image:0from media. image[0] get image:1from media. image[1] [GC (Allocation Failure) 5114K->5338K(9728K), 0.0007850 secs] [Full GC (Ergonomics) 5338K->4866K(9728K), 0.0032548 secs] get image:2from media. image[2] get image:3from media. image[3] [GC (Allocation Failure) 6914K->7114K(8704K), 0.0006606 secs] [Full GC (Ergonomics) 7114K->6907K(8704K), 0.0028911 secs] get image:4from media. image[4] [Full GC (Ergonomics) 7931K->7912K(8704K), 0.0075340 secs] get image:5from media. image[5] [GC (Allocation Failure) 3014K->3022K(9216K), 0.0004105 secs] get image:6from media. image[6] [GC (Allocation Failure) 4045K->4118K(9216K), 0.0011056 secs] get image:7from cache. image[7] get image:8from media. image[8] [GC (Allocation Failure) 5141K->5262K(9216K), 0.0005411 secs] get image:9from media. image[9] |
测试一和测试二对比
首先我们通过启动参数-Xmx10M -Xms10M -verbose:gc,将jvm的堆内存大小设置为10M。
现在我们有10张照片,每张1M大小。由于JVM堆中还有方法区(存放类定义)、常量池等,所以这10张照片是不能够全部存放在cache中的。
通过上面测试一和测试二的对比我们可以看出,当全部加载10张图片后,我们获取第八张图片是从cache中取的;获取第七张图片时,是重新加载的(从media中加载)。所以实际上,由于内存有限,我们的cache只是在内存中保存了2张照片;之前加载的照片由被回收了,通过[GC (Allocation Failure) 6914K->7114K(8704K), 0.0006606 secs]可以看出进行了内存回收。
所以,我们这里实现的cache在内存不足的时候,会自动被回收的。
之后我们强引用了一张图片,可以看出存在强引用的这张图片是一直存在内存中的,不会被回收。
●弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
弱引用最常见的用处是在集合类中,尤其在哈希表中。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。
对于这种情况的解决办法就是使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。Java中提供了WeakHashMap来满足这一常见需求。
示例:
public staticvoid main(String[] args) { Map<Integer, String> map = new WeakHashMap<Integer, String>(); Integer key = new Integer(1); map.put(key, "test");
// key不再有强引用 key = null; System.gc();
//等待一段时间,进行垃圾回收 try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(map.size()); } |
输出:
[GC (Allocation Failure) 509K->520K(130560K), 0.0028151 secs]
[GC (System.gc()) 746K->560K(131072K), 0.0007948 secs]
[Full GC (System.gc()) 560K->513K(131072K), 0.0091938 secs]
0
可见当WeakHashMap中key不存在强引用时,随时都会从map中移除。
●虚引用(PhantomReference)
1、 基本概念
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
2、析构finalization
Java中内存管理是自动进行的。开发者无需关心已经释放的对象的内存回收。这样的一个缺点是开发者无法得知某个特定对象何时被回收;另外,开发者无法控制内存管理。不过,java.lang.ref包定义了一些可以和垃圾回收器进行有限交互的类。具体的类是SoftReference,WeakReference以及PhantomReference,它们是Reference的子类,以不同的方式和垃圾回收器交互。
有时需要在对象被回收前做一些清理工作,可以使用析构方法。该特性可以用来回收对象相关的本地资源。但是,Java中的析构方法有很多的问题。
第一:我们无法预期finalize()何时会被调用。没有任何保证某个对象一定会被垃圾回收。一个在JVM整个生命周期中可达的对象永远不会被回收;也有可能,在对象变成可回收之后和JVM停止之前,垃圾回收线程没有运行。
第二:Java析构方法会将应用程序拖慢。管理重载了finalize()方法的对象需要耗费JVM更多的资源。
第三:对象即使被调用了finalize()方法,也无法保证该对象一定会被回收;并且,可以在finalize()方法中对对象进行拯救(重新赋予强引用)。
虽然有可达性分析算法来判定对对象状态,但这并不是对象是否被回收的条件,对象回收的条件远远比这个复杂,比如无法通过ROOT找到的对象,也不一定会回收,会进入一个死缓的阶段,那些无法通过根节点引用链找到的对象,会被第一次标记,并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize() 方法已经被虚拟机调用过,虚拟机都视为“没必要执行”。
如果该对象被判定为有必要执行finalize(),那么对象会被放置在一个F-Queue 的队列中,并由一个优先级较低的Finalizer线程去执行,这里的执行是 JVM 会触发这个方法,但并不保证等待他运行结束,因为finalize() 方法执行慢,或者死循环,会影响该队列其他元素执行。
执行finalize() 方法就会进行第二次标记,然后等待JVM 进行回收了,而在finalize() 方法执行的同时,可以对对象进行“拯救”,也就是说在执行方法内部,再次对对象进行引用,那么对象就复活了。
示例:
相关推荐
000000_【课程介绍 —— 写在前面的话】_Java学习概述笔记.pdf 010101_【第1章:JAVA概述及开发环境搭建】_JAVA发展概述笔记.pdf 010102_【第1章:JAVA概述及开发环境搭建】_Java开发环境搭建笔记.pdf 010201_【第2...
在函数中定义的 一些基本类型的变量和对象的引用变量都是在函数 的栈内存中分配。当在一段代码块中定义一个变量时,java 就在栈中 为这个变量分配内存空间,当超过变量的作用域后,java 会自动释放 掉为该变量...
Java 是传值还是传引用,问题主要出在对象的传递上,因为 Java 中简单类型没有引用。既然争论中提到了引用这个东西,为了搞清楚这个问题,我们必须要知道引用是什么。 简单的说,引用其实就像是一个对象的名字...
java多态性详解——父类引用子类对象
Java 把内存划分成两种:一种是栈内存,另一种是堆内存。在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的 栈内存中分配,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当...
java多态性详解——父类引用子类对象.pdf
此外,本书的配套光盘还免费提供了价值人民币330元的java教学视频,对java语言进行了全面讲解,帮助一些不会java语言的读者快速地从java基础知识的学习中过渡到java web的学习与开发上. 第1部分 xml篇. 第1章 xml...
本人经过长时间收集的最经典、最有说服力、最易懂的关于值传递和引用传递的详解资料~包看包会~发出去真有些不舍得~
主要介绍了 JAVA 弱引用的相关资料,帮助大家更好的理解和学习java引用对象,感兴趣的朋友可以了解下
主要介绍了JAVA 虚引用的相关资料,帮助大家更好的理解和学习JAVA,感兴趣的朋友可以了解下
Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。 需要明确的几个问题: synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句...
Java8 十大新特性详解 接口的默认方法详解、Lambda 表达式详解、函数式接口详解、方法与构造函数引用详解、Lambda 作用域详解、访问对象字段与静态变量详解、访问接口的默认方法详解
主要介绍了Java中的对象和引用详解的相关资料,需要的朋友可以参考下
主要介绍了JAVA 强引用的相关资料,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
此外,本书的配套光盘还免费提供了价值人民币330元的java教学视频,对java语言进行了全面讲解,帮助一些不会java语言的读者快速地从java基础知识的学习中过渡到java web的学习与开发上. 第1部分 xml篇. 第1章 xml...
NULL 博文链接:https://jackyin5918.iteye.com/blog/1882071
1. java.util.concurrent - Java 并发工具包 2. 阻塞队列 BlockingQueue 3. 数组阻塞队列 ArrayBlockingQueue 4. 延迟队列 DelayQueue 5. 链阻塞队列 LinkedBlockingQueue 6. 具有优先级的阻塞队列 ...
5.2.4 值传递与引用传递 84 5.3 类变量和变量作用域 86 5.3.1 Java的类变量 86 5.3.2 Java的变量作用域 87 5.4 递归程序设计 89 5.4.1 递归方法概述 89 5.4.2 递归的阶乘函数 89 5.5 Math类及其类...
java 值传递和引用传递的比较区别,包括代码及详解