您的位置:首页 > 编程语言 > Java开发

JVM - 垃圾收集器与内存分配策略

2017-03-20 22:15 295 查看
当前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空间. 这样循环进行回收.

注:

1. 之所以在是回收新生代上使用此种算法, 是基于新生代中98%的对象都会消亡的调查, 所以一般情况下10%的Survivor能够容纳存活的对象.

2. 当不够用的时候, 依赖老年代进行分配担保. 关于这一部分, 下面会提到.

标记-整理算法

回收老年代时会使用这种算法, 因为老年代的对象存活率很高.

具体思路是让存活的对象向一端移动, 直接清理掉边界以外的内存:



分代收集算法

根据对象存货周期的不同将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岁), 就会被晋升到老年代中.

小结

垃圾收集器是影响系统性能, 并发能力的主要因素之一, 实际使用中需要根据应用需求, 选择最优的收集器组合策略及参数才能获取最高的性能.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java jvm GC