一、对象可达性
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 会自动释放 掉为该变量...
### JVM内幕:java虚拟机详解 #### 一、概述 Java虚拟机(JVM)是运行Java应用程序的核心组件,它提供了一个可移植、安全且高性能的环境。本文将深入探讨JVM的内部架构及其各个组成部分的功能。 #### 二、Java虚拟机...
Java集合框架是Java编程语言中的核心部分,它提供了一种高效、灵活的方式来组织和操作对象的集合。在Java中,集合主要分为两大类:Collection和Map。本文将深入讲解Java集合类,特别是Collection接口和其下的List、...
Java 多态详解 Java 多态是 Java 编程语言中的一种基本概念,它允许开发者定义一个接口,并且可以通过不同的类来实现该接口。多态性是 Java 面向对象编程的核心机制之一,它使得程序更加灵活、可维护和可扩展。 ...
在Java中,对象的引用方式对于内存管理和对象生命周期的控制至关重要。从JDK 1.2版本开始,Java引入了四种不同级别的引用:强引用(Strong Reference)、软...希望本文能够为您提供在Java引用类型的使用上清晰的指导
### 详解Java堆和栈 #### 一、引言 在Java编程中,理解堆(Heap)和栈(Stack)的概念及其区别对于程序员来说至关重要。本文将深入剖析这两个概念,并探讨它们之间的差异以及如何影响程序的运行。 #### 二、Java...
### Java内存模型详解 #### 1. JMM简介 ##### i. 内存模型概述 Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)的一部分,用于规定程序中的各种变量(包括实例字段、静态字段和数组元素等)在多个...
【Java程序详解】 Java是一种广泛使用的面向对象的编程语言,由Sun Microsystems(后被Oracle公司收购)于1995年推出。它的设计目标是具有跨平台性、可移植性、安全性和高效性,使得开发者可以编写一次代码,到处...
7. **内存泄漏**:Java的垃圾回收机制虽然可以自动管理内存,但不当的引用可能导致内存泄漏。书中会讲解如何检测和避免内存泄漏,使用内存分析工具进行诊断。 8. **集合类误用**:如ArrayList、HashMap等集合类的...
Java 8引入了更多的新特性,如Lambda表达式、方法引用来简化代码,Stream API以更直观的方式处理集合,以及日期时间API的改进等,这些都是进一步提升编程效率的关键。学习Java的过程中,理解并熟练应用这些知识点,...
### Java中ThreadLocal详解 #### 一、ThreadLocal概述 在Java多线程编程中,`ThreadLocal`是一个非常重要的工具类,它提供了一种在每个线程内部存储线程私有实例的方法。通常情况下,当多个线程共享某个变量时,...
变量是用来存储数据的容器,Java提供了基本数据类型(如int、float、char等)以及引用数据类型。运算符用于执行计算或改变变量值,而控制流(if语句、for循环、while循环)则决定了程序的执行顺序。 接着,文档会...
- 静态方法没有`this`引用,非静态方法的第一项是`this`引用,指向调用该方法的对象实例。 - 参数和局部变量按照声明的顺序存储在局部变量区,例如,方法参数和局部变量的存储结构可以通过图表清晰地展示。 4. **...
### jvm详解(java虚拟机详解) #### Java与JVM概览 Java作为一种广泛使用的编程语言,其核心优势之一便是“一次编写,到处运行”的特性,这背后的关键技术支撑即为Java虚拟机(JVM)。JVM是一种抽象计算模型,允许...
Java 堆栈详解 Java 是一种广泛使用的面向对象的编程语言,它的内存管理机制是其强大特性的关键部分。在 Java 中,内存分为两个主要区域:堆(Heap)和栈(Stack)。本文将深入探讨Java堆栈的概念、工作原理以及...
这展示了如何在不直接引用具体类名的情况下,通过反射动态地操作对象。 反射的核心类有以下这些: 1. **Class 类**:代表 Java 类的运行时信息,可以获取类的所有属性、方法、构造器等信息。 2. **Constructor 类*...
2. 变量与数据类型:Java支持基本数据类型(如int、double)和引用数据类型(如类、接口、数组)。理解它们的区别和用法是必要的。 3. 控制结构:包括条件语句(if-else、switch-case)和循环(for、while、do-while...