深入理解jvm虚拟机读书笔记-垃圾收集器与内存分配策略(一)
1.引用计数算法
给对象一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器就减一;任何时刻计数器为0的对象就是不可能再被使用。
优点:实现简单,判定效率很高,微软的COM技术、使用ActionScript 3的FlashPlayer等等都使用了引用计数进行内存管理。
缺点:Java虚拟机没有使用,最主要的原因是它很难解决对象之间互相循环引用的问题。
2.可达性分析算法
Java等主流商用程序语言都是通过可达性分析算法来判断对象是否存活。通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没用任何引用链相连时,则证明此对象是不可用的。
Java语言中可作为GC Roots的对象包括下面几种:
- 虚拟机栈中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
3.Java引用类型
为什么JDK1.2以后要引入四种引用类型?在此之前,对象只有引用或者没被引用两种状态,对于如何描述一些"食之无味,弃之可惜"的对象无能为力。我们希望描述一些对象:当内存还足够时,能保存在内存中;如果内存空间进行垃圾收集之后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的场景。
Java中的引用分为:强引用、软引用、弱引用和虚引用。
-
强引用普遍存在,类似“Object obj = new Object()”,只要强引用还在,垃圾收集器永远不会回收被引用的对象。
-
软引用是用来描述一些还有用但非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,会把这些对象列入进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
软引用主要应用于内存敏感的高速缓存,在android系统中经常使用到。一般情况下,Android应用会用到大量的默认图片,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软引用技术来避免这个问题发生。SoftReference可以解决oom的问题,每一个对象通过软引用进行实例化,这个对象就以cache的形式保存起来,当再次调用这个对象时,那么直接通过软引用中的get()方法,就可以得到对象中中的资源数据,这样就没必要再次进行读取了,直接从cache中就可以读取得到,当内存将要发生OOM的时候,GC会迅速把所有的软引用清除,防止oom发生。
public class BitMapManager { private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>(); //保存Bitmap的软引用到HashMap public void saveBitmapToCache(String path) { // 强引用的Bitmap对象 Bitmap bitmap = BitmapFactory.decodeFile(path); // 软引用的Bitmap对象 SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap); // 添加该对象到Map中使其缓存 imageCache.put(path, softBitmap); // 使用完后手动将位图对象置null bitmap = null; } public Bitmap getBitmapByPath(String path) { // 从缓存中取软引用的Bitmap对象 SoftReference<Bitmap> softBitmap = imageCache.get(path); // 判断是否存在软引用 if (softBitmap == null) { return null; } // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空 Bitmap bitmap = softBitmap.get(); return bitmap; } }
-
弱引用也是用来描述非必需对象的,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象。
-
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个持有虚引用的对象,和没有引用几乎是一样的,随时都有可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总是会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。
4.对象的生存和死亡
在可达性分析中不可达的对象那个,并非是“非死不可”的。要真正宣告一个对象死亡,至少经历两次标记过程:可达性分析后没有与GC Roots相连接的引用链,会被第一次标记并且进行一次筛选,筛选条件是此对象有没有必要执行finalize()方法。对象没有覆盖finalize()方法或者已经被执行过,都会被视为“没有必要执行”。
没有必要执行finalize()方法的对象将被第二次标记,随后便被回收。如果此对象被判定为有必要执行finalize()方法,那么这个对象将被放置在一个叫做F-Queue的队列中,这个队列将被一个由JVM自动建立,低优先级的Finalizer线程去执行。这里的“执行”只意味着JVM会触发对象的finalize()方法,但不承诺会等待它运行结束,因为如果一个对象在finalize()中执行缓慢,或者发生了死循环,这将可能导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次标记,如果对象要在finalize()方法中拯救自己,那么它只要重新与引用链上的任意一个对象建立关联即可,
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
运行程序:
finalize mehtod executed!
yes, i am still alive :)
no, i am dead :(
一次逃脱,一次失败是因为任何一个对象那个的finalize()方法都只会被系统自动调用一次。 建议大家不要使用此方法分,运行代价高昂,不确定性大,无法保证各个对象的调用顺序。
5.回收方法区
方法区(或者HotSpot虚拟机中的永久代)垃圾收集的“性价比”很低。
永久带垃圾收集主要回收两部分内容:废弃常量和无用的类。当系统中没有对象引用常量池中的对象时,如果这是发生内存回收,而且必要的话,这些常来那个就会被清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。
判断一个类是否是无用的类,需要满足下面三个条件:
- 该类所有的实例都已经回收,也就是java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象那个没有在任何地方被引用,无法在任何地方访问该类的方法。
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能。