学习参考资料:周志明老师的著作《深入理解Java虚拟机(第3版)》
垃圾收集(Garbage Collection),简称GC。
在垃圾收集前必须需要先思考下面三件事情:
后面也就会围绕解决这些事情的方法展开介绍。
1.判断对象是否“死亡”算法
在堆中存放着Java世界几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了
1.1引用计数算法
在对象中添加一个计数器,每被引用一次计数器+1,引用失效计数器-1。
客观地说,引用计数器虽然占用了一定的空间,但它的原理简单,效率也很高,大多数情况下是一个不错的算法。但是在Java领域,主流的Java虚拟机并没有采用引用计数器,因为该算法还有许多例外没有考虑到,必须要配合大量额外处理才能保证正确地工作。
例如,两个对象互相引用,最终都不会被回收!
1.2可达性分析算法
在Java虚拟机中都是通过可达性分析来判断对象是否存活的。
可达性分析算法的基本思路是从“GC Roots”出发,向下搜索,被搜索到的对象就是可达(也称作被“GC Roots”直接引用或间接引用),无法搜索到的对象就是不可达(也就是需要被回收的对象)。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种 :
-
虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。 -
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 -
在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。 -
在本地方法栈中JNI(即通常所说的Native方法)引用的对象。 -
所有被同步锁(synchronized关键字)持有的对象。 -
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton 、OutOfMemoryError )等,还有系统类加载器。
刚才提到的引用也分为强引用、软引用、弱引用、虚引用,这四种引用强度依次逐渐减弱。
方法区也是会存在垃圾收集行为的,只不过判断条件相对堆中比较苛刻;
方法区垃圾收集主要包括两部分内容:废弃的常量和不再使用的类型。
2.垃圾收集算法
当解决了哪些对象需要被回收后,接下来就要开始想什么时候回收以及怎样回收了!
垃圾收集的算法有下面几种。
2.1标记—清除
定义:将需要清理的对象打上标记,然后从内存中删除。
缺点:容易产生大量的内存碎片,可能无法满足较大对象的存储,一旦无法分配对象内存,就会又触发一次GC。
2.2标记—复制
定义:将存储区域分为大小两份,一份用来直接存储,另一份用来存储垃圾收集后剩余的对象。
缺点:将可用存储内存变为原先的一半,有点浪费空间。如果大多数对象是存活的,不免会浪费很大开销,但对于新生代还是很试用的。
Java虚拟机大多数采用这种类似的复制算法去回收新生代。
2.3标记—整理
定义:将不需要删除的对象打上标记,然后向前进行整理,删除掉界限以外的对象。
对于老年代来说,存储的都是存活率较高的对象,复制算法并不适用。
缺点:牵扯到对象的移动,必须全程暂停用户程序才能进行。
2.4分代收集
现在商用的Java虚拟机中,设计者一般至少会把Java堆划分为新生代和老年代两个区域。顾名思义,在新生代中,每次垃圾回收都有大部分对象死去,而每次回收后存活的少量对象会逐步晋升到老年代中。
3.经典收集器
如果说收集算法是方法论,那么垃圾收集者就是实践者。在《Java虚拟机规范》中并没对垃圾收集器的实现并没有做出任何规定,不同的Java虚拟机都会提供各种参数供用户根据应用特点和要求组合出各个内存分代使用何种垃圾收集器。
各个经典收集器之间的关系如下:
上图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用!
要说明的是,到现在为止还没有出现最好的收集器出现,我们只能根据具体应用情况去选择或组合相对更合适的垃圾收集器。
4.内存分配与回收策略
接下来就来聊聊在HotSpot使用Serial加Serial Old客户端默认收集器组合下的内存分配和回收策略,这种配置和收集器组合也许是开发人员做研发时的默认组合。
对象优先在 Eden 区分配 多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。
Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快; Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。 大对象直接进入老年代 所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。
前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。
长期存活对象将进入老年代 虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。
动态对象年龄判定 为了更好的适应不同程序的内存情况,虚拟机并不是永远要求对象的年龄必需达到某个固定的值(比如前面说的 15)才会被晋升到老年代,而是会去动态的判断对象年龄。如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代。
空间分配担保 在新生代触发 Minor GC 后,如果 Survivor 中任然有大量的对象存活就需要老年队来进行分配担保,让 Survivor 区中无法容纳的对象直接进入到老年代。
后面还会陆陆续续更新这系列的读书笔记,期待您的关注~~
|