`

JVM学习笔记(三)

 
阅读更多

这是该专题Blog连载的第三部分,整理一下发上来。

-------------------------------------------------------

与垃圾收集GC相关的3件事:

1.哪些内存需要回收?

2.何时回收?

3.怎么回收?

上面3条分别对应了2部分知识:

1.垃圾收集算法(对应1)。

2.垃圾收集器(对应2、3)。

下面分别学习这些知识。

 

一、什么地方需要回收内存:

程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭。每一个栈帧中分配多少内存基本上是在类结构定下来时就已知,所以这几个区域的内存分配和回收都具备确定性,方法或线程结束时,内存自然就跟随着回收了。

Java堆和方法区则不一样。只有在程序运行时才能知道会创建哪些对象,这部分内存的分配和回收是动态的,垃圾收集器关注的是这部分内存。

判断对象是否存活方法:

1、引用计数算法:给对象中添加一个引用计数器,每有一个地方引用它时,计数器的值就加1.当引用失效时计数器值就减去1.任何计数器为0的对象就是不可能再被使用的。(Java语言没有选用这个算法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用问题)

2、根搜索算法:主流商用语言(Java、C#等)都是用根搜索算法判断对象是否存活。算法基本思路就是通过一系列名为GCRoots的对象作为起始点,从这些节点开始向下搜索,走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

Java中GC Roots对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中的常量引用的对象、本地方法栈中JNI的引用的对象。

在根搜索中不可达的对象,处于缓刑阶段,真正宣告一个对象死亡,至少要经历两次标记过程:

(1)根搜索后发现没有与GC Roots相连接的引用链,将会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。如果没有覆写finalize()方法或者已经被虚拟机调用过,虚拟机视为没有必要执行finalize(),放入即将回收的集合。

(2)如果被判定为有必要执行finalize()方法,那么这个对象将会被优先放置在一个队列之中,并由虚拟机自动建立的、低优先级的finalizer线程执行(只是触发该方法,并不一定等其结束,防止出现无限循环等情况)。GC将在队列中的对象进行第二次小规模标记(标记即将回收的集合),如果把自己赋值给某个类变量或对象成员变量,将被移出即将回收的集合。

注:finalize()方法不要使用,try-finally能做所有finalize能做的工作(释放资源等)。

3、JDK1.2之后的引用:将引用分为强引用、软引用、弱引用、虚引用四种。

4、回收方法区:

在方法区进行垃圾回收的性价比一般比较低。在堆中,新生代,常规应用进行一次垃圾收集一般可以回收70%-95%的空间,而永久代的垃圾收集效率远低于此。

永久代回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似,即没有其他地方引用了这个字面量,在内存回收时,该常量会被系统请出常量池。

回收无用的类的3个条件:

(1)该类所有的实例都已经被回收(Java堆中不存在该类的任何实例)。

(2)加载该类的ClassLoader已经被回收。

(3)该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收。

5、是否对类进行回收hotspot虚拟机提供参数:

-Xnoclassgc参数进行控制,还使用-verbose:class及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息。

大量使用反射、动态代理、CGLIB等框架的场景,以及动态生成JSP和OSGi频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

 

二、垃圾收集算法

1、标记-清除算法:

该算法分为两个阶段,标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

特点:最基础的收集算法。

缺点:时间问题,标记-清除的效率都不高。空间问题,清除后产生大量不连续空间的内存碎片,当程序运行需要分配大对象时无法找到足够连续的内存而不得不提前触发另一次垃圾收集动作。

2、复制算法(新生代算法):

特点:将可用内存按容量分为大小相等的2块,每次只使用其中的一块。当一块用完,就把存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

缺点:将内存缩小为原来的一半。

商业虚拟机都采用复制算法来回收新生代。

新生代对象一般98%都是朝生夕死,所以将内存分为较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor空间。回收时将Eden和Survivor中存活的对象一次性地拷贝到另外一块Survivor空间上,并清理掉Eden和刚才用过的Survivor空间。当存活对象大于待复制的Survivor的空间时,这些对象通过分配担保机制直接进入老年代。

3、标记-整理算法(老年代算法):

有人提出一种标记整理算法,标记过程仍然与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

4、分代收集算法:

一般把Java堆分为新生代和老年代。

在新生代中,每次垃圾收集是都有大批对象死去,只有少量存活,因此选用复制算法。只需付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记-清理或标记-整理算法来回收。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics