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

当前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. 当不够用的时候, 依赖老年代进行分配担保. 关于这一部分, 下面会提到.

标记-整理算法

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

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

mark-compact-algorithm

分代收集算法

根据对象存货周期的不同将Java堆划分为新生代和老年代, 在新生代上使用复制算法, 在老年代上使用标记-整理算法.

hotspot算法实现

GC的过程是需要停顿所有的Java执行线程, 以保证在一个全局一致性的快照中进行GC. GC发起时, 需要先枚举GC Roots, 此时线程需要走到最近的SafePoint或者在Safe Region内.

垃圾收集器

垃圾收集器是垃圾收集算法的具体实现. 一个虚拟机通常是由多个垃圾收集器组合而成的, 不同的垃圾收集器负责收集不同年代的垃圾. 这里讨论HotSpot的G1收集器, 以下是HotSpot虚拟机中可用的垃圾收集器的概览图:

hotspot-vm-collector

这部分以后会单独开篇分析.

理解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岁), 就会被晋升到老年代中.

小结

垃圾收集器是影响系统性能, 并发能力的主要因素之一, 实际使用中需要根据应用需求, 选择最优的收集器组合策略及参数才能获取最高的性能.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值