From:深入理解Java虚拟机
- 目录
BiBi - JVM -0- 开篇
BiBi - JVM -1- Java内存区域
BiBi - JVM -2- 对象
BiBi - JVM -3- 垃圾收集算法
BiBi - JVM -4- HotSpot JVM
BiBi - JVM -5- 垃圾回收器
BiBi - JVM -6- 回收策略
BiBi - JVM -7- Java类文件结构
BiBi - JVM -8- 类加载机制
BiBi - JVM -9- 类加载器
BiBi - JVM -10- 虚拟机字节码
BiBi - JVM -11- 编译期优化
BiBi - JVM -12- 运行期优化
BiBi - JVM -13- 并发
程序计数器、虚拟机栈、本地方法栈这3部分区域随线程而生而灭,每一个栈桢中分配多少内存基本是在编译期间就可知的,他们的分配和回收具有确定性,不需要考虑回收的问题。而Java堆和方法区不一样,一个方法中的多个分支需要的内存是不一样的,需要程序处于运行期间才能知道要创建哪些对象,这部分内存的分配和回收是动态的。
- GC需要完成的3个件事情:
1)哪些内存需要回收?
2)什么时候回收?
3)如何回收?
1. 判断对象“存活”的方法
-
引用计数算法【不好】
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是没有被引用,死亡的对象。
问题:难以解决对象之间互相循环引用
例子:对象a、b已经不能再被访问了,但是他们因为互相引用这对方,导致他们的引用计数都不为0。所以,主流的Java虚拟机没有采用这种方法进行内存管理。
public static void main(String[] args) {
A a = new A();
B b = new B();
a.obj = b;
b.obj = a;
a = null;
b = null;
System.gc();
}
-
可达性分析算法【主流程序语言采用的方法】
通过一系列【GC Roots】对象作为起始点,从这些节点向下搜索,所走过的路径称为【引用链】,当一个对象到GC Roots没有任何引用链相连时,则此对象不可用。
- Java中可以作为GC Roots的对象包括:
1)虚拟机栈中引用的对象
2)本地方法栈中JNI引用的对象
3)方法区中类静态属性引用的对象
4)方法区中常量引用的对象
2. finalize()方法
宣告一个对象真正死亡,至少要经历两次标记过程:
第一次:对象进行可达性分析后发现没有与GC Root相连的引用链,进行第一次标记,并且还要进行一次筛选,筛选得条件是该对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过了,虚拟机将这两种情况视为【没有必要执行】。
第二次:对象有必要执行finalize()方法,将这个对象放置在F-Queue队列中,并由一个低优先级的Finalizer线程去执行它。finalize()方法是对象逃离死亡命运的最后机会,GC会对F-Queue队列中的对象进行第二次小规模的标记。
注意:任何一个对象的finalize()方法只会被系统自动调用一次,所以在finalize()方法中逃脱的对象,只能够逃脱一次。
F-Queue队列的执行,无法保证各个对象的调用顺序。所以,最好不要在finalize()中做任何事情。
3. 回收方法区
永久代的垃圾回收主要分两部分:【废弃的常量】和【无用的类】。回收废弃的常量跟Java堆中对象的回收类似。
- 无用类的判断条件:
1)Java堆中不存在该类的任何实例
2)加载该类的ClassLoader已经被回收
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景,需要虚拟机具备【类卸载的功能】,以避免永久代溢出。
4. 垃圾收集算法
-
1)标记 - 清除算法
分两个阶段,先标记出需要回收的对象,在标记完成后再统一回收所有标记的对象。
缺陷:标记和清除两个过程效率不高;标记清除后会产生大量的不连续的内存碎片,当程序运行过程中需要分配大对象时,由于无法找到连续的内存空间,而不得不提前触发另一次垃圾回收动作。
-
2)复制算法【JVM新生代使用的算法】
将内存按容量分为大小相等的两块,每次只使用其中一块。当这块内存用完,就将还存活的对象复制到另外一块上面,然后再把已使用过的这块内存空间一次全部清理掉。
优点:实现简单、效率高、没有锁片。
缺陷:内存空闲一半,浪费。
因为【新生代】中的对象98%都是“朝生夕死”【存活率不高】,适合用复制算法来回收,但并不一定按照1:1来划分内存空间。
HotSpot划分比例为Eden:Survivor1:Survivor2 = 8:1:1,每次使用Eden和其中一个Survivor,当回收时,将Eden和Survivor1中存活着的对象一次性复制到Survivor2中,最后清理掉Eden和Survivor1空间。这样新生代只浪费了【10%】的内存空间。
当新生代中回收的空间大于10%,即Survivor2空间不够用时,多的对象将直接通过【分配担保机制】进入到【老年代】。
-
3)标记 - 整理【JVM老年代使用的算法】
与标记-清除算法一样,只是不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
老年代中对象【存活率高】【没有额外的空间进行担保】,所以必须使用:标记-清理/整理算法,不能使用复制算法进行担保。
网友评论