1. 如何判断对象已死?
JVM 中判断对象是否已经死亡的算法主要有 2 种:引用计数法、可达性分析法。
1.1 引用计数法
如果一个对象被其他变量所引用,则让该对象的引用计数+1,如果该对象被引用2次则其引用计数为2,依次类推。
某个变量不再引用该对象,则让该对象的引用计数-1,当该对象的引用计数变为0时,则表示该对象没用被其他变量所引用,这时候该对象就可以被作为垃圾进行回收。
引用计数法弊端:循环引用时,两个对象的引用计数都为1,导致两个对象都无法被释放回收。最终就会造成内存泄漏!
1.2 可达性分析算法
可达性分析算法就是JVM中判断对象是否是垃圾的算法:该算法首先要确定GC Root(根对象,就是肯定不会被当成垃圾回收的对象)。
在垃圾回收之前,JVM会先对堆中的所有对象进行扫描,判断每一个对象是否能被GC Root直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收:
JVM中的垃圾回收器通过可达性分析来探索所有存活的对象。
扫描堆中的对象,看能否沿着GC Root为起点的引用链找到该对象,如果找不到,则表示可以回收,否则就可以回收。
**在Java技术体系里面,固定可作为GC Roots的对象包括以下几种 **:
虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
所有被同步锁(synchronized关键字)持有的对象。
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
1.3 Java中的五种引用
强引用
上图实心线表示强引用:比如,new 一个对象M,将对象M通过=(赋值运算符),赋值给某个变量m,则变量m就强引用了对象M。
强引用的特点:只要沿着GC Root的引用链能够找到该对象,就不会被垃圾回收;只有当GC Root都不引用该对象时,才会回收强引用对象。
如上图B、C对象都不引用A1对象时,A1对象才会被回收。
软引用
上图中宽虚线所表示的就是软引用:
软引用的特点:当GC Root指向软引用对象时,若内存不足,则会回收软引用所引用的对象。
- 如上图如果B对象不再引用A2对象且内存不足时,软引用所引用的A2对象就会被回收。
软引用的使用:
public class Demo1 { public static void main(String[] args) { final int _4M = 4*1024*1024; // 软引用对象内部包装new byte[_4M]对象 SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]); // 这时List 跟 SoftReference之间是强引用,SoftReference 跟 byte[] 之间是软引用 List 跟 <SoftReference<byte[]>> list = new ArrayList<>(); } }
如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理。
如果想要清理软引用,需要使用引用队列:
public class Demo04 { final static int _4M = 4 * 1024 * 1024; public static void main(String[] args) { // List和SoftReference是强引用,而SoftReference和byte数组则是软引用 List<SoftReference<byte[]>> list = new ArrayList<>(); // 引用队列,用于移除引用为空的软引用对象 ReferenceQueue<byte[]> queue = new ReferenceQueue<>(); for (int i = 0; i < 5; i++) { // 关联引用队列,当软引用所关联的 byte[]被回收时,软引用自己会假如到queue中去 SoftReference<byte[]> ref = new SoftReference<>(new byte[_4M], queue); System.out.println(ref.get()); list.add(ref); System.out.println(list.size()); } // 遍历,从引用队列中获取无用的软引用对象,并移除 Reference<? extends byte[]> poll = queue.poll(); while (poll != null) { // 引用队列不为空,则从集合中移除该元素 list.remove(poll); // 移动到引用队列中的下一个元素 poll = queue.poll(); } System.out.println("=========================="); for (SoftReference<byte[]> reference : list) { System.out.println(reference.get()); } } }
**大概思路为:**查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合)。
弱引用
只有当弱引用引用该对象,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象。
- 如上图如果B对象不再引用A3对象,则A3对象会被回收。
弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference。
虚引用
当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中:
然后调用Cleaner的clean
方法(Unsafe.freeMemory()
)来释放直接内存:
虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存。
如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存。
终结器引用
所有的类都继承自Object类,Object类有一个finalize()
方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize()
方法。调用以后,该对象就可以被垃圾回收了。
如上图,B对象不再引用A4对象。这是终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize()方法。调用以后,该对象就可以被垃圾回收了。
引用队列
软引用和弱引用可以配合引用队列(也可以不配合):
在弱引用和虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象。
虚引用和终结器引用必须配合引用队列:
虚引用和终结器引用在使用时会关联一个引用队列。
1.4 回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。举个常量池中字面量回收的例子,假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。
判定一个常量是否“废弃”需要同时满足下面三个条件:
该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
2. 垃圾回收算法
在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。
四种GC概念的介绍:
■ 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
■ 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
■ 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
■ 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
下面逐个介绍下4种回收算法:
2.1 标记-清除
定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识,清除相应的内容,给堆内存腾出相应的空间。
这里的腾出内存空间并不是将内存空间的字节清 0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存。
缺点:(容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc)。
第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过
程的执行效率都随对象数量增长而降低;
第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-清除算法的执行过程(书中配图):
2.2 标记-整理
标记-整理:会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。
标记-整理算法的执行过程(书中配图):
2.3 标记-复制
当需要回收对象时,先将GC Root直接引用的的对象(不需要回收)放入TO中:
然后清除FROM中的需要回收的对象:
最后 交换 FROM 和 TO 的位置:(FROM换成TO,TO换成FROM)
复制算法:将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。
标记-复制算法的执行过程(书中配图):
2.4 分代回收
把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域,顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快),如下图所示:
回收流程
新创建的对象都被放在了新生代的伊甸园中:
当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC:
Minor GC 会将伊甸园和幸存区FROM仍需要存活的对象先复制到 幸存区 TO中, 并让其寿命加1,再交换FROM和TO。
伊甸园中不需要存活的对象清除:
交换FROM和TO:
同理,继续向伊甸园新增对象,如果满了,则进行第二次Minor GC:
流程相同,仍需要存活的对象寿命+1
:(下图中FROM中寿命为1的对象是新从伊甸园复制过来的,而不是原来幸存区FROM中的寿命为1的对象,这里只是静态图片不好展示,只能用文字描述了)
再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1!
如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中:
如果新生代老年代中的内存都满了,就会先触发Minor Gc,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收:
分代回收小结:
新创建的对象首先会被分配在伊甸园区域。
新生代空间不足时,触发Minor GC,伊甸园和 FROM幸存区需要存活的对象会被COPY到TO幸存区中,存活的对象寿命+1,并且交换FROM和TO。
Minor GC会引发 Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
当对象寿命超过阈值15时,会晋升至老年代。
如果新生代、老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收。
后续会陆续更新,这本书的笔记记的差不多了,排版和格式需要花时间整理,文章都会同步到公众号上,也欢迎大家通过公众号加入我的交流qun互相讨论jvm这块的知识内容!