垃圾收集和内存分配策略

首先,解决第一个问题:哪些内存需要回收?

这个问题,得从Java虚拟机运行时区域的各个部分来谈:程序计数器、虚拟机栈、本地方法栈,这三个区域都是随线程的创建而生,随线程的销毁而灭;栈中的栈帧,随着方法的进入和退出,按照一定的次序执行出栈和入栈操作;因此,这几个区域的内存分配和回收都具备确定性,基本不用考虑垃圾回收的问题;因为方法或者线程结束时,内存自然而然就跟着回收了。

注解:这部分很好理解。

但是,Java堆和方法区,这两部分就不一样了,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间,才能知道会创建哪些对象;所以,这部分内存的分配和回收是动态的,垃圾收集器所关注的是这部分的内存。

注解:所以,区别在于,程序计数器,虚拟机栈和本地方法栈具备确定性,而Java堆和方法区具备不确定性,必须要有同样动态的垃圾收集策略来解决这方面的问题。而且,可以看出来,区分标准在于是否是线程私有的内存区域,如果是,则回收具有确定性;如果是各个线程所共享的区域,就是垃圾收集需要注意的区域了。

好了,现在我们知道垃圾收集针对的是哪一块内存区域了,但是并不是堆内存和方法区内存里的所有东西都会被一股脑回收,垃圾收集策略需要判断到底哪些才是要回收的内存区域。

先以堆为例,其中存放着Java虚拟机中几乎所有的对象实例(这里是几乎,还有一些对象实例并不是存储在堆中的);垃圾收集器在对堆进行回收之前,第一件事情就是要确定这些对象之中,哪些还存活,哪些已经死去。

简单介绍几种方法:

1:引用计数算法

给对象添加一个引用计数器,每当有一个地方引用这个对象的时候,计数器值就加1,当引用失效时,计数器值就减1;任何时候,计数器为0的对象就是不可能再被使用的。

客观情况下来说,这个算法好像效果很不错,也在大部分情况下能够实现功能,但是,主流的Java虚拟机里没有选用该方法来管理内存,其中最主要的原因,就在于这个算法的一个劣势:很难解决对象之间相互循环引用的问题。

2:可达性分析算法

在主流的商用程序语言的主流实现中,都是称通过可达性分析来判定对象是否存活的,这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达),则证明此对象是不可用的,也就会被判定为是可回收的对象。

毫无疑问,这里出现的GC Roots是非常重要的概念,关系到可达性分析算法的效果;在Java语言中,可以作为GC Roots的对象包括以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象:比如说,我们再调用方法的过程中,会引用到一些对象,最简单的就是,main方法跑的时候,通常会new一个对象,这个对象就是。
  2. 方法区中静态属性引用的对象:静态属性,这个应该是类加载使用之后里面全局静态变量指向的对象。
  3. 方法区中常量引用的对象:类中的常量应用的对象,比如一个字符串常量,其指向的字符串。
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。

这里,讨来了一段英文的GC Roots,有心人可以翻译一下:

Garbage Collection Roots

A garbage collection root is an object that is accessible from outside the heap. The following reasons make an object a GC root:

垃圾收集的GC roots,就是可以堆外访问的对象。

System Class
Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like  java.util.* .
类加载器不说了,很容易看懂。
JNI Local
Local variable in native code, such as user defined JNI code or JVM internal code.
JNI Global
Global variable in native code, such as user defined JNI code or JVM internal code.
Thread Block
Object referred to from a currently active thread block.
当前活跃线程的引用对象。
Thread
A started, but not stopped, thread.
启动,并未销毁的线程。
Busy Monitor
Everything that has called  wait() or  notify() or that is synchronized. For example, by calling  synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.
Java Local
Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.
Native Stack
In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.
Finalizable
An object which is in a queue awaiting its finalizer to be run.
Unfinalized
An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.
Unreachable
An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.
Java Stack Frame
A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects.
Unknown
An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.

现在,探讨一下垃圾收集的算法:

1:标记-清除算法

这是最基础的算法,顾名思义,该算法分为两个阶段:标记和清除两个阶段;首先,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

优点:简单,而且是后续收集算法的基础。

缺点:第一是效率问题,标记和清除两个过程的效率都不高;另一个则是空间问题,标记清除之后会产生大量不连续的内存碎片,很明显,如果空间碎片太多,会导致以后再程序运行过程中,万一需要分配较大的对象时,可能会无法找到足够的连续内存而不得不提前出发另一次垃圾收集操作。


不用多说,一图表明。

2:复制算法

通俗来讲,就是把可用内存按照容量,划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将上面还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉。

这样的好处在于,每次都是针对整个半区进行内存回收,内存分配时候就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按照顺序分配内存即可,实现简单,运行高效。

当然,缺点也是有的,每次只能有一半内存可用,代价太高,会提高垃圾回收的频率。


现在的商用虚拟机,都是采用这种收集算法来回收新生代;而根据IBM公司的专门研究,新生代中的对象98%都是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块survivor,当回收的时候,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间,最后清理掉原先占用的空间。

注意:这里说的是收集新生代的算法,因为回收老年代和新生代往往使用的算法不一样,因为其中对象的存活率不同;就咱们常用的HotSpot虚拟机来说,默认的Eden和Survivor的大小比例是8:1,就是说每次新生代中可用内存空间为整个新生代容量的90%。但是,并不是每次新生代对象的存活率都这么低,在一些特殊情况下,可能剩余的Survivor空间不够用,这时候,就需要依赖其他内存(如老年代)来进行分配担保。这里,更要注意的是,内存分配担保,会导致一些对象将直接通过该机制,进入了老年代。

3:标记-整理算法

毫无疑问,复制-收集算法在对象的存活率比较高的时候,就要进行较多的复制操作,这时候性能就比较低了;不仅如此,有时候还会出现对象100%存活的情况,需要额外的空间进行分配担保;所以,老年代一般不能直接使用这种算法。

根据老年代的特点,我觉得应该是对象存活率较高吧,提出了“标记-整理”算法,标记过程与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。


可以很明显看出与标记-清除算法的区别,不予赘述。

4:分代收集算法

对于当前商业虚拟机来说,垃圾收集都采用分带收集的算法,这种算法的本质,其实就是根据对象存活周期的不同,将内存划分为几块;一般来说,是划分为新生代和老生代,因为这种划分区别比较明显,也比较有针对性。

新生代,对象存活率很低,就像前面说的,在每次垃圾收集时,都会有大批的对象死去,可以选用复制算法,只需要付出很少的复制代价就可以完成收集。

老年代,因为其对象存活率比较高,而且没有额外空间来为其进行分配担保,就必须使用标记-清理或者标记-整理算法来进行内存回收。

综述:

  1. 标记-清除算法:操作简单,但会导致大内存对象无法找到空间进行分配,是其他收集算法的基础。
  2. 复制算法:可使用的内存空间减少,但完全不用考虑内存碎片这些问题。
  3. 标记-整理算法:针对对象存活比较多的情况,直接将其都整理到一端即可,因为可能存活的对象非常多,移动要比复制的代价低得多。
  4. 在前三种算法的基础上,第四种分代收集算法呼之欲出,针对不同的区域采用不同的垃圾回收算法,以取得高效率和高成果。

猜你喜欢

转载自blog.csdn.net/u013384984/article/details/79609831