当前GC技术已经基本自动化了, 为什么我们需要了解GC和内存分配呢? 答案是: 当需要排查各种内存溢出, 内存泄露问题时, 当垃圾收集成为系统达到更高并发量的瓶颈时, 我们就需要对这些"自动化"的技术实施必要的监控和调节.
在GC上, 程序计数器, 虚拟机栈, 本地方法栈这三个区域随着线程而生灭, 内存的分配和回收都是完备的, 不需要考虑回收问题. 本章主要基于Java堆和方法区来讨论.
判断对象是否可回收
引用计数法
给对象添加一个引用计数器, 每当有一个地方引用它时, 计数器值+1, 引用失效, -1, 为0的对象不能被使用.
python就是使用此种方法的, 但是JVM不是. 此方法的问题是, 如果对象相互循环引用则无法被回收.
可达性分析算法
通过一系列的称为"GC Roots"的对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots不可达(也就是不存在引用链)的时候, 证明对象是不可用的.
可作为GC Roots的对象包括
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI引用的对象
注意
即使是在可达性分析算法中不可达的对象, 也不会被立即回收. 要真正回收一个对象, 要经过额外的标记过程:
- 如果对象不可达, 会被第一次标记并且进行一次筛选, 筛选的条件是此对象是否有必要执行
finalize()
方法. 当对象没有覆盖finalize()
方法, 或者finalize()
方法已经被虚拟机调用过, 虚拟机将这两种情况视为"没有必要执行". 此种情况下对象将在稍后被清理. - 如果覆盖过
finalize()
方法, 并且从未被虚拟机调用过, 则会放置在一个F-Queue队列中, 并稍后在Finalizer
进程中执行finalize()
方法, 除非对象在finalize()
执行期间重新关联上GC Roots, 否则将被再次标记回收. - 实际操作中, 不提倡手动进行
finalize()
方法的相关操作.
引用的分类
Java中的引用分为强引用(Strong Reference), 软引用(Soft Reference), 弱引用(Weak Reference), 虚引用(Phantom Reference).
强引用
就是我们普通的Object obj = new Object()
这样的代码, 只要存在引用, 垃圾收集器就不会回收掉被引用的对象.
软引用
用来描述一些非必须的对象. 对于被引用的对象, 在系统将要发生内存溢出之前, 将会被划分进回收范围之内进行二次回收. 如果这次回收还没有足够的内存, 才会抛出内存溢出异常.
在Java中使用SoftReference类来实现软引用.
弱引用
同样是用来描述一些非必须的对象, 强度比软引用更低. 被若引用关联的对象只能生存到下一次垃圾收集发生之前.
虚引用
也被称为幻影引用, 无法通过虚引用来取得一个对象实例, 也不会对对象的生存期造成影响. 唯一的用处是在对象被回收时收到一个系统通知.
finalize()方法
注: 不建议使用这个方法, 这个方法只是Java设计初期为了使C/C++程序员更容易接受的一个妥协, 这个方法运行代价高, 不确定性大, 无法保证对象的调用顺序, 所以最好不要使用, 一切工作在Java内置的try-finally中完整即可.
实际上不可达的对象不会被立即回收, 它会经历一个两次标记的过程, 第一次标记的过程是看对象是否有必要执行finalize()
方法, 如果有必要执行, 那么会被加入一个F-Queue队列中, 并由虚拟机建立的Finalizer线程去执行它, 如果对象在finalize()
中重新关联上了GC Roots, 那么就不会被回收, 否则会被回收.
回收方法区
此部分主要是回收废弃常量和无用的类.
一个废弃常量的例子:
假如有一个字符串"abc", 那么没有任何String
对象引用它, 如果必要的话, 就会被清理出常量池.
回收无用的类在大量使用反射, 动态代理, CGLib, 动态生成JSP, 以及OSGi这样频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能.
垃圾收集算法
复制算法
现在的商业虚拟机都采用这种收集算法来回收新生代, 具体方法是将内存分为一块Eden空间和两块Survivor空间, 比例为8:1:1, 每次使用Eden和其中一块Survivor, 当回收时, 将Eden和Survivor中还存活的对象复制到另外一块Survivor空间上, 然后清理掉Eden和使用过的Survivor空间. 这样循环进行回收.
注:
- 之所以在是回收新生代上使用此种算法, 是基于新生代中98%的对象都会消亡的调查, 所以一般情况下10%的Survivor能够容纳存活的对象.
- 当不够用的时候, 依赖老年代进行分配担保. 关于这一部分, 下面会提到.
标记-整理算法
在回收老年代时会使用这种算法, 因为老年代的对象存活率很高.
具体思路是让存活的对象向一端移动, 直接清理掉边界以外的内存:
分代收集算法
根据对象存货周期的不同将Java堆划分为新生代和老年代, 在新生代上使用复制算法, 在老年代上使用标记-整理算法.
hotspot算法实现
GC的过程是需要停顿所有的Java执行线程, 以保证在一个全局一致性的快照中进行GC. GC发起时, 需要先枚举GC Roots, 此时线程需要走到最近的SafePoint或者在Safe Region内.
垃圾收集器
垃圾收集器是垃圾收集算法的具体实现. 一个虚拟机通常是由多个垃圾收集器组合而成的, 不同的垃圾收集器负责收集不同年代的垃圾. 这里讨论HotSpot的G1收集器, 以下是HotSpot虚拟机中可用的垃圾收集器的概览图:
这部分以后会单独开篇分析.
理解GC日志
首先, 如果想要在控制台查看GC日志的话, 需要在程序运行时加上-XX:+PrintGCDetails
参数. 这里分析一段打印的GC日志信息:
[Full GC (System.gc()) [PSYoungGen: 640K->0K(56320K)] [ParOldGen: 8K->487K(128512K)] 648K->487K(184832K), [Metaspace: 2781K->2781K(1056768K)], 0.0097749 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
- Full GC Full GC的意思是出现了"Stop The World", 一般在出现分配担保失败这类问题时发生, 手动调用
System.gc()
也会产生这样的效果. - PSYoungGen 表示GC发生的区域, 随着使用的垃圾收集器的不同而不同, 示例中使用的垃圾收集器是Parallel Scavenge, 它的新生代名称就是PSYoungGen.
- 640K->0K(56320K) 意为"GC前该区域已使用容量->GC后该区域已使用容量(该内存区域总容量)".
- 648K->487K(184832K) 意为"GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)"
- 0.0097749 secs GC使用的时间
- Times: user=0.01 sys=0.00, real=0.01 secs 更细粒度的GC时间展示, 分别是用户态CPU时间, 内核态CPU时间, 实际用时.
内存分配与回收策略
关于对象的内存分配与回收策略, 记住以下原则即可.
对象优先在Eden分配
大多数情况下, 对象在新生代Eden区中分配, 当Eden区没有足够空间进行分配时, 虚拟机将发起一次Minor GC.
什么是Minor GC, 什么是Full GC呢?
- Minor GC 就是新生代GC, 因为Java对象一般都是朝生夕灭, 所以Minor GC发生的很快速而频繁.
- Full GC 指包含了老年代的GC, 一般会伴随至少一次的Minor GC(通过前面的例子也可以看出). Full GC的时间一般是Minor GC的十倍以上.
大对象直接进入老年代
大对象指的是需要大量连续内存空间的Java对象, 常见的是超长字符串及大数组(例如byte[])
虚拟机有一个-XX:PretenureSIzeThreshold
参数, 大于这个设定值的对下给你直接在老年代分配, 这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制.
注意 虚拟机是非常怕出现大量寿命短的大对象的, 因为这样会导致内存还有不少空间就提前触发垃圾收集.
长期存活的对象将进入老年代
如果对象在经过一次Minor GC后仍然存活, 并且在Survivor中被容纳的话, 将对象年龄加1. 当年龄加到默认值(15岁), 就会被晋升到老年代中.
小结
垃圾收集器是影响系统性能, 并发能力的主要因素之一, 实际使用中需要根据应用需求, 选择最优的收集器组合策略及参数才能获取最高的性能.