首页 > 代码库 > 探索笔记2-Java虚拟机的GC

探索笔记2-Java虚拟机的GC

上一篇blog中比较深入的去了解JVM中的各大内存,这些天除了努力工作之外,有空便继续深入的探索虚拟机的内部一些实现的原理,与C++想必,Java语言最大的特色主要有跨平台和无需手动回收内存(由虚拟机负责GC)。今天主要是来好好总结一些这若干天来的一些Java虚拟机GC的研究结果,如果读者觉得我的这篇博客叙述有误,欢迎读者们在我的博客下方留下评论,方面我进行参考确定后进行修改,以免误导了其他读者。
Java语言建立了垃圾收集机制,用以跟踪正在使用的对象和发现并回收不再使用(引用)的对象。该机制可以有效防范动态内存分配中可能发生的两个危险:因内存垃圾过多而引发的内存耗尽,以及不恰当的内存释放所造成的内存非法引用。而垃圾收集算法的核心思想是:对虚拟机可用内存空间,即堆空间中的对象进行识别,如果对象正在被引用,那么称其为存活对象,反之,如果对象不再被引用,则为垃圾对象,可以回收其占据的空间,用于再分配。垃圾收集算法的选择和垃圾收集系统参数的合理调节直接影响着系统性能,因此需要开发人员做比较深入的了解。对一名Java开发的程序员,熟悉虚拟机的GC也便于程序的排错调优。如果想要了解虚拟机的GC,首先要先对以下的一些基础概念有一些了解:

引用

关于Java的引用分为以下4种,其功能和对应的特性分别如下:
强引用:最常使用的引用;
软引用:虚拟机在堆内存不足的时候会对其指向的堆内存进行回收;
弱引用:虚拟机会在下一次GC的时将其指向的堆内存进行回收;
虚引用:最弱的引用,无法获取指向的对象,只可以知道该对象是否被GC;

堆内存的分代

Java的堆(Heap)是存放对象的内存区域。在逻辑上把堆细分为新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)。
1.新生代:可以再划分为Eden、From Survivor和To Survivor三个逻辑区域,对象优先存放在新生代的Eden区域。
2.老年代:新生代的对象经过几次垃圾回收之后,仍然存活的将存放到老年代,并且大对象可以不经过新生代而直接存放在老年代。
3.永久代:方法区使用永久代作为存储区域,在逻辑上,永久代是Java堆的一部分、但通常称之为“非堆”(Non-Heap)内存以示区别。方法区(Method Area)通常用来存放类的相关信息 (类加载器所加载的类的字段、方法签名等)、运行时常量池(如字符串常量池)、静态引用变量等。

GC的算法

复制算法(Copying):将堆内存划分为两块,当其中一块正在使用中的的内存空间紧张时、把其中“存活”(仍然被引用)着的对象复制到另外一块空闲着的内存区域,然后清空当前内存空间.复制算法通常作为新生代的垃圾回收策略。
标记-清除算法(Mark-Sweep):先标记出可回收的对象,然后进行统一清除.缺点:效率低、并且产生大量不连续的内存碎片。
标记-整理算法(Mark-Compact):标记出可回收的对象、将所有存活的对象向其中一端移动,然后直接清理掉另一端的内存区域。
分代收集算法(Generational Collection):将Java堆划分为新生代、老年代,新生代中的大多数对象都是可回收的,而老年代中的对象大多数都是不可回收的。新生代采用复制算法:大多数对象都是可回收的、只需复制少数存活的对象、回收效率较高。老年代只有少数对象可回收、标记效率较高,因此采用标记-清除(无须移动对象)、标记-整理(移动存活对象到其中一侧)算法相结合进行回收。

内存分配的策略

1.新创建的对象将存放在新生代的Eden区域、以及其中一个Survivor(存活者)区域(From Survivor)。
2.堆内存紧张时、进行新生代对象的回收,存活着的对象将从Eden和From Survivor区域复制到To Survivor区域,如果To Survivor区域内存紧张、一部分存活对象将直接复制到老年代存放,然后清空Eden和From Survivor区域.。在下一次新生代垃圾回收时、From Survivor和To Survivor区域的角色互换.
3.大对象(通常是指内容很长的字符串或者数组)直接放入老年代、以避免大对象在新生代的反复拷贝,这样可以减少性能上的开销。
4.(新生代中)长期存活的对象将放入老年代,新生代中的对象每在Survivor区域完成一次拷贝、该对象的年龄(Age)加1,当对象的年龄增加到一定值(默认为15)时、该对象将被存放到老年代,以避免该对象在新生代的反复拷贝。

GC Root和对象的finalize方法

Java虚拟机通过使用一种被称为根搜索算法(GC Root Tracing)的策略来,判断当前这个对象目前是否还存活,如果不存活则将内存进行回收,这种算法思想的基本思路大致为:通过一系列命名为“GC Root”的对象作为起始点,从这些节点开始向下进行搜索,搜索所走过的路径被称为引用链(Reference Chain)如果存在一个对象A到“GC Root”没有任何引用链与其相连(从算法思想上来讲称为GC Root节点到A节点的路径不可达),则这个对象意味着不被引用,它将被虚拟机进行垃圾回收。
在Java语言中,可以作为GC Root的对象包括以下几种:
1、 虚拟机栈中(栈帧中的变量表)的引用的对象;
2、 方法区中的静态类属性的引用的对象;
3、 方法区中的常量的引用的对象;
4、 Native方法(JNI)中的引用的对象;
如果堆中的对象到GC Roots之间没有任何引用链,GC就可以对其进行回收. 在回收之前会调用对象的finalize()方法,可以通过覆盖该方法、把当前对象的引用重新和GC Roots连接起来、以阻止GC进行回收。 需要注意的是,一个对象的finalize()方法只会被执行一次、如果GC再次回收该对象,无法阻止被GC回收。

GC的种类

新生代GC(Minor GC):新生代的垃圾回收非常频繁(尽可能快的释放出可用空间)、效率很高(采用复制算法,大多数对象可回收、只需复制少数存活对象)。
老年代GC(Major/Full GC):老年代的垃圾回收、效率通常比新生代的Minor GC慢至少10倍,(采用标记-清除、标记-整理算法),每次Full GC会同时进行至少一次Minor GC,通常在堆内存紧张、或者显示的调用System.gc()时触发Full GC。

GC的触发条件和性能开销

虚拟机进行次GC的频率很高,但因为这种GC占用时间极短,所以对系统产生的影响不大。更值得关注的是主GC的触发条件,因为它对系统影响很明显。总的来说,有两个条件会触发主GC:
①当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
②Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足, 虚拟机就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求, 虚拟机会再进行两次GC作进一步的尝试,若仍无法满足要求,则虚拟机将报“out of memory”的错误,Java应用将停止。
由于是否进行主GC由虚拟机根据系统环境决定,而系统环境在不断的变化当中,所以主GC的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主GC是反复进行的。根据虚拟机的GC机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。同时也尽量不要显式调用System.gc(),虽然此函数建议虚拟机进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数,对整个应用或者系统性能来说也是一笔相当大的性能开销,甚至有可能降低应用的工作效率和用户体验。

探索笔记2-Java虚拟机的GC