jvm 垃圾回收

jvm 垃圾回收

Posted by julyerr on January 30, 2018

垃圾回收简介

大家应该都接触过c,在c中申请一块空间需要malloc,使用完全之后手动free,否则会造成内存泄漏。有句话说的话,对于从事C和C++程序开发的开发人员来说,在内存管理领域,他们既是拥有最高权力的皇帝,又是从事最基础工作的劳动人民。但是在java中我们使用对象直接new,而后对象的是否存活就不关我们的事情了。
显然,使用完的对象内存需要被回收的,只不过整个过程是jvm自动帮我们完成的。了解和理解垃圾回收机制有利于开发出robust的程序,因为很多bug主要出现在内存方面。
java中不同版本使用的gc(garbage collection)也不同,后文会针对具体gc进行说明。为了方便coder开发,很多编程语言有gc存在(如最近比较流行的go),实现有所差异但是gc中包含的思维大相径庭,是值得我们借鉴的。

判断对象回收

  1. 引用计数器
    为每个创建的对象分配一个引用计数器;当对象被引用的时候,计数器加1;对象引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减1;当对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减1;引用计数器为0,表示该对象可以被回收。
    • 优势 实现简单、高效
    • 缺点 致命缺点就是不能解决相互引用的问题,因此实际jvm实现不采用引用计数器而是下面的方式。 refrence-both
  2. 可达性分析 将所谓的”GC Roots”设置为起始节点,每一个对象看成是一个节点,对象之间的引用则是两个节点之间的边;如果起始节点到某个对象不存在通路(不联通),则该对象可以被回收。 GC Roots 可以设置
    • 虚拟机栈(栈帧中的局部变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中Native方法引用的对象

垃圾回收算法

标记清除法

为不同的区域块设置对应的flag,将垃圾对象清除之后,存活的对象通过引用串联起来。
算法分析
执行过程仅对垃圾对象进行清除(比较适合老年代),并且存活对象不进行移动,从而比较容易产生空间碎片,当多次申请大量连续内存的时候,垃圾收集操作频繁,效率低下。

复制算法

将可用内存空间划分为等分的两份,每次使用其中一块。仅将存活的对象(比较适合新生代)复制到另一块连续的内存空间,将原来的空间全部回收。 另一种比较常见的算法:

标记整理法

结合标记和复制算法两者的优点,直接在原有的区域上将存活的对象移动到连续的内存区域。

分代回收算法

  • 不同对象的生命周期不同,新生代(Young generation)对象生命周期通常较短,存储在新生代内存分配区;老年代(old generation)生命周期比较长,存储在老年代内存分配区(Tenured Space)永久代则是jvm中方法区的内存分配区,具体参见博文1
  • 通过设置不同的内存分配区,并在不同内存分配区使用不同的垃圾回收算法,利于提高gc的垃圾回收效率,提高jvm整体性能。
  • 新生代常采用复制算法,老年代通常使用标记-清除法
  1. 算法执行流程 新生代和老年代的区域大小比例通常为1:2。新生代具体划分为eden和两个survivor(suvivor0,suvivor1),比例通常为8:1:1
    • 大部分新生对象在eden中分配,当发生垃圾回收时,将eden区存活对象复制到survivor0,清空eden空间;
    • 如果survivor0空间不足,则将eden和survivor0中存活对象移至survivor1中,清空eden和survivor0;
    • 如果survivor1空间也不够,将存活对象移动到老年区,清空新生代区;如果老年代空间不足,进行Full Gc(新生代、老年代都进行回收),新生代垃圾回收过程也称为MinorGc,Full Gc 进行次数比较少,Minor Gc进行比较频繁。Full Gc发生的可能原因:老年代被写满、永久代被写满或者显示调用System.gc()(不建议使用,系统开销较大)。
  2. 垃圾回收策略

    • 需要分配大量连续内存的对象直接分配在老年代
    • 除了空间不足,新生代进入老年代之外;还有一种情况就是新生代经过minor Gc的次数(通过MaxTenuringThreshold设置,默认15次)依然存活的对象进入老年代。
    • 动态年龄判断
      在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  3. 永久代(Permanent generation)
    主要存放类 信息、常量、静态变量1。相对于对象回收而言,被回收的要求比较苛刻:

    • 该类所有的实例都已经被回收
    • 加载该类的ClassLoader已经被回收;
    • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 在大量使用反射、动态代理、CGLib等bytecode框架时注意永久代的内存溢出。

垃圾收集器

具体垃圾回收算法的实现,不同收集器之间可以相互结合使用

  • Serial/Serial Old收集器
    • serial:单线程收集器,垃圾收集时暂停其他的工作线程,是运行在client模式下默认的新生代收集器
    • serial old:使用标记清除算法,是serial的老年代收集器
  • ParNew收集器 Serial收集器的多线程版,运行在Server模式下首选的新生代收集器

  • Parallel Scavenge/Parallel old收集器
    • parallel scavenge:多线程新生代收集器,注重系统吞吐量的控制,XX:+UseAdaptiveSizePolicy参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或者最大吞吐量
    • parallel old:使用标记整理法,parallel scavenge的老年代收集器
  • CMS(Concurrent Mark Sweep)收集器 回收过程经过: 初始标记、并发标记、重新标记、并发清除,初始标记、重新标记需要停止用户线程,重新标记是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

    使用标记清楚算法,实现垃圾线程和用户线程并发执行,尽可能减少最短回收停顿时间。

  • G1收集器 经过初始标记、并发标记、最终标记和筛选回收过程

    G1收集器使用最前沿科技,可以运用在新生代和老年代上。通过将Java堆分为多个大小相等独立区域(Region),跟踪各个Region里垃圾堆积价值大小(从回收空间和回收所需时间经验值两方面考虑),维护一个优先列表,从而回收最有价值的Region。 整体上使用标记-整理算法,两个Region直接使用复制算法,不会产生内存碎片。

    特点
    追求低停顿时间之外,还能够建立可预测停顿时间模型(可以明确做到在长度M毫秒时间片内垃圾回收时间不超过N毫秒)

GC log

  • 开始数字表示gc发生的时间(相对于JVM启动时刻而言),GCFull Gc表示垃圾收集的停顿的类型,
  • [DefNew,[Tenured,[Perm表示GC发生的区域
  • 3324K -> 152K(3712K)表示GC前该内存区域已使用容量 -> GC后该内存区域使用容量(该内存区域总容量);
  • 3324K -> 152K(11904)表示GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆的总容量)
  • 后面时间user,sysreal分别表示用户态消耗CPU时间内核态消耗CPU时间操作开始到结束经过的时间(如果是多核CPU,多线程叠加CPU时间,可能导致user和sys之和超过real的时间)

jvm调优

jvm调优通常从内存、线程等方面进行考虑,java界也提供了很多非常优秀的jvm管理和监控工具.

jvm常见配置参数

堆设置

  • -Xms:初始堆大小
  • -Xmx:最大堆大小
  • -XX:NewSize=n:设置年轻代大小(1.3/1.4版本)
  • -Xmn:年轻代大小
  • -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
  • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
  • -XX:MaxPermSize=n:设置持久代大小

收集器设置

  • -XX:+UseSerialGC:设置串行收集器
  • -XX:+UseParallelGC:设置并行收集器
  • -XX:+UseParalledlOldGC:设置并行年老代收集器
  • -XX:+UseConcMarkSweepGC:设置并发收集器

垃圾回收统计信息

  • -XX:+PrintGC
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -Xloggc:filename

并行收集器设置

  • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数,并行收集线程数。
  • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
  • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

  • -XX:+CMSIncrementalMode:设置为增量模式,适用于单CPU情况。

并发和并行的区别

  • 并发:有多个任务来临,但是不一定需要同时执行这些任务

  • 并行: 有个任务来临,同时具备同时处理这些任务的能力

堆信息查看

  • 查看堆空间大小分配(年轻代、年老代、持久代分配)
  • 垃圾监控(长时间监控回收情况)

线程监控

  • 系统中线程数量
  • 各个线程处于的状态
  • 线程内部运行情况
  • 死锁检查

其中特别关注

  • CPU热点:检查系统哪些方法占用的大量CPU时间
  • 内存热点:检查哪些对象在系统中数量最大

内存泄露
内存泄露是jvm中较为常见和严重的错误,通常伴随着以下异常信息 :

  • java.lang.OutOfMemoryError: Java heap space(堆溢出),
  • java.lang.OutOfMemoryError: PermGen space(永久代溢出),
  • java.lang.StackOverflowError(栈溢出),
  • java.lang.OutOfMemoryError: unable to create new native thread(内存不足,创建新线程失败)

调试和监控工具

Jconsole :jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。

VisualVM :JDK自带,功能强大,与JProfiler类似,推荐使用。

JProfiler:商业软件,需要付费,功能强

参考资料