Java内存模型与GC

Java内存模型

JVM Spec中的Runtime Data Area分为5个区域:pc register(PC寄存器)、java stack(JVM方法栈)、native stack(本地方法栈)、java heap、method area。前三个和大多数语言类似比较容易理解,java Heap就是我们常说的堆了,也是Young Generation和Tenured Generation所在,而Method Area就是我们所说的Permanent Generation。

GC的主要区块

《Java内存模型与GC》

上图是JDK工具JVisualVM中的Visaul GC插件运行时的截图。从图中可以看出,Java内存包含以下几个部分

  • Perm

    • 即Permanent Generation,称为持久代。在Sun JVM实现中,它**不属于**堆内存的一部分。
    • 该区域分为只读和读写两部分。只读区用于存储类元数据,方法元数据信息。读写区用于存储内联的字符串对象。
    • 该区域的在Full GC时会被清理,但只有内联的字符串对象可能会被清理,而类及方法元数据信息则不会。
    • 持久代一般为64m,通过参数-XX:MaxPermSize: 可以指定该区域最大值
  • Eden

    • 与S0, S1一起组成Young Generation, 称为年轻代。属于堆内存的一部分
    • 可通过-Xmn:设置年轻代大小。SUN官方推荐为整个堆的3/8。
    • 新对象的分配发生该区域内,当新对象(非大对象)无法在该区域分配时便引发Minor GC
    • 大对象是指,需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串及数组。当分配大对象时,如果年轻代中无法放置,则该大对象也会在Old区域中分配。
    • 发生Minor GC时,Eden + S0作为from space, S1 作为to space进行copy,将存活的对象复制到S1中,并清空Eden和S0, 下一次Minor GC时 from space变为Eden + S1, 而to space则变为S0, 并清空Eden和S1。如此反复进行。
    • 在Minor GC时,如果to space的空间无法容纳所有存活的对象,则这部分对象会被直接放到Old 区域
    • 经过数次Minor GC后仍然存活的对象将被移到Old区域。可以通过-XX:MaxTenuringThreshold:设置垃圾最大年龄,如果设置为0则年轻代对象不经过Servivor区,直接进入年老代
    • 可通过-XX:NewRatio:设置年轻代与年老代的大小比值
  • S0, S1

    • 称为Servivor, 与Eden区一起组成Young Generation
    • S0, S1在Minor GC时其中一个肯定会被清空(使用Copy算法)
    • S0, S1的大小是相同的
    • 可以通过-XX:SurvivorRatio:设置年轻代中Eden区与S0或S1大小比值
  • Old

    • 也叫Tenured Generation, 即年老代。属于堆的一部分
    • 当分配大对象时,如果年轻代中无法放置,则该大对象也会在Old区域中分配。
    • 经过数次Minor GC后Young Generation中仍然存活的对象将被移到Old区域。
    • 如果进行Minor GC时to space中放不下全部存活的对象,则这部分对象也会被直接放到该区域
    • 发生在该区域的垃圾回收称为Major GC,一般发生Major GC时伴随着对Perm区域和整个堆的清理,所以又称为Full GC

Java GC

GC的种类

两种类型的GC:

  1. Minor GC

    • 也称为YGC(Young Generation Collection)针对年轻代进行的垃圾回收。
    • 默认情况下Full GC/Major GC会触发Minor GC
    • 使用Copy 算法进行回收,速度较快
    • 对应的回收器有三种:串行GC(Serial Copying), 并行GC(ParNew)和并行回收GC(Parallel Scavenge)
  2. Major GC/Full GC

    • 发生在Old区域的垃圾回收,经常会伴随至少一次的Minor GC
    • 速度一般比Minor慢10倍以上
    • 由于Marjor GC除并发GC外均需要对整个堆及Permanent Generation进行扫描和回收,因此又称为Full GC
    • 对应的回收器有四种:串行GC(Serial MSC), 并行MS GC(Parallel MSC), 并行Compacting GC(Parallel Compacting)和并发GC(CMS)

MajorGC/Full GC的触发

  • 对于Seial MSC, Parallel MSC和Parllel Compactingd而言,触发机制为

    • Old Generation 空间不足
    • Permanent Generation 空间不足
    • Minor GC时的悲观策略
    • Minor GC后在Eden上分配内存仍然失败
    • 执行Heap Dump时
    • 外部调用 System.gc时(可通过*-XX:DisableExplicitGC*来禁止)
  • 对于CMS而言,触发机制为

    • Old Generation 空间使用到一定比率时(如92%)触发。(参考*PrintCMSInitiationiStatistics*和*CMSInitiatingOccupancyFaction*参数)
    • 当Permanent Generation 采用CMS收集且使用到一定比率时触发
    • Hotspot 根据成本计算决定是否需要执行CMS GC
    • 外部调用System.gc(), 且设置了*ExplicitGCInvokesConcurrent*

GC过程

  1. 对象在Eden Space完成内存分配
  2. 当Eden Space满了,再创建对象,会因为申请不到空间,触发Minor GC,进行New(Eden + S0 或 Eden S1) Generation进行垃圾回收
  3. Minor GC时,Eden Space不能被回收的对象被放入到空的Survivor(S0或S1,Eden肯定会被清空),另一个Survivor里不能被GC回收的对象也会被放入这个Survivor,始终保证一个Survivor是空的
  4. 在Step3时,如果发现Survivor区满了,则这些对象被copy到old区,或者Survivor并没有满,但是有些对象已经足够Old,也被放入Old Space。
  5. 当Old Space被放满之后,进行Major GC|Full GC

垃圾回收器种类

目前的收集器主要有三种:串行收集器、并行收集器、并发收集器 。

并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态

并发:用户线程和垃圾收集线程同时工作,用户程序继续运行,而垃圾收集程序运行于另一个CPU上

  • 串行收集器

    • 使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。
    • 无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M 左右)情况下的多处理器机器上。
    • 使用-XX:+UseSerialGC 打开。
  • 并行收集器

    • 对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC .打开。
    • 使用-XX:+UseParallelOldGC 打开对年老代使用。
    • 使用-XX:ParallelGCThreads= 设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等 。
    • -XX:MaxGCPauseMillis= 指定最大垃圾回收暂停(垃圾回收时的最长暂停时间)。为毫秒.
    • 通过-XX:GCTimeRatio= 设定吞吐量。 吞吐量为垃圾回收时间与非垃圾回收时间的比值来设定,公式为1/(1+N)。
  • 并发收集器

    • 可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。
    • 使用-XX:+UseConcMarkSweepGC 打开。
    • 并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。
    • 通过设置-XX:CMSInitiatingOccupancyFraction= 指定还有多少剩余堆时开始执行并发收集

垃圾回收集实现

  1. Serial收集器

    • 采用复制算法
    • Serial收集器是最基本历史最悠久的收集器,它是一个单线程的收集器,它在垃圾收集的时候必须暂停其他所有的工作线程,直到收集完成。
    • 垃圾收集的工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是难以接受的
    • 优点:简单高效 对于限定单个CPU的环境来说,收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。
  2. ParNew收集器

    • 采用复制算法
    • ParNew收集器其实就是Serial收集器的多线程版本,包括收集器可用的控制参数、收集算法、暂停所有用户线程、对象分配策略、回收策略等都和Serial收集器完全一样
    • 它是运行在Server模式下的虚拟机中首选的新生代收集器,其中一个原因是,除了Serial收集器,目前只有它能和CMS收集器配合使用。CMS收集器是HotSpot虚拟机上第一款真正意义上的并发收集器
    • ParNew收集器在单CPU环境中不会比Serial收集器有更好的效果
  3. Parallel Scavenge收集器

    • 采用复制算法
    • Paraller Scavenge收集器也是一个新生代收集器,使用复制算法的收集器,是一个并行的多线程收集器
    • Parellel Scavenge收集器的特点是它的关注点和其它收集器不同,CMS等收集器尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量
    • 吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
    • 停顿时间越短越适合与用户交互的程序,良好的响应速度能提升用户的体验;高吞吐量可以更高效的利用CPU时间
    • Parallel Scavenge收集器有一种称为GC自适应调用策略,虚拟机根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或最大的吞吐量
    • 自适应策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别
  4. Serial Old收集器

    • 采用标记-整理算法
    • 它是Serial收集器的老年版本,作用于Old Generation单线程收集器。
  5. Parallel Old收集器

    • 采用标记-整理算法
    • Parallel Old是Parallel Scavenge收集器的老年版本,使用多线程。
  6. CMS收集器

    • 采用标记-**清理**算法
    • CMS收集器是一种以获得最短回收停顿时间为目标的收集器。
    • 它分为四个步骤:初始标记、并发标记、重新标记、并发清除其中初始标记和重新标记两个步骤仍然需要暂停其它线程。
    • 初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段一般比初始标记阶段稍长一些,但远比并发标记的时间短。
    • 由于整个过程中耗时最长的并发标记和并发清理过程中,收集器都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用于线程一起并发执行的。
    • 缺点1:CMS收集器对CPU资源非常敏感,面向并发设计的程序都对CPU资源比较敏感,虽然并发阶段不会导致应用线程停顿,但是会因为占用了一部分线程导致应用程序编码,总吞吐量会降低。
    • 缺点2:CMS是基于标记-清理算法,收集结束的时候会产生大量空间碎片,空间碎片过多的时候将会给大家分配带来很大的麻烦,往往会出现老年代有很大剩余空间,但是无法找打足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
  7. G1收集器

    • 采用标记-整理算法
    • G1收集器是收集器理论进一步发展的产物,它与CMS收集器相比有两个重要的改进:G1收集器基于标记-整理算法,不会产生空间碎片,对于长时间运行的应用程序来说非常重要。二是,它可以非常精确的控制停顿,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
    • G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,它能够避免全区域的垃圾收集。

相关算法

  • 基本回收算法

    • 引用计数(Reference Counting)
      比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
    • 标记-清除(Mark-Sweep)
      此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
    • 复制(Copying)
      此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
    • 标记-整理(Mark-Compact)
      此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
  • 实施垃圾回收算法

    • 增量收集(Incremental Collecting)
      在应用进行的同时进行垃圾回收。
    • 分代收集(Generational Collecting)
      基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的
  • 判断对象是否有效算法

    • 引用计数算法
      给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1,任何时刻计数器值为0的对象就是不可能再被使用的。引用计数法的实现简单,判定效率比较高,但是java中并没有选择引用计数法来管理内存,其中主要的原因是它很难解决对象之间的相互循环引用问题。objA.instance=objB;objB.instance=objA;除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们相互引用,引用计数器无法通知GC收集器回收它们。
    • 根搜索算法
      java c#以及Lisp都是使用根搜索算法判定对象是否存活的。这个算法的基本思路就是通过一系列的名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径就称为引用链。当一个对象到引用链没有对象相连时,则证明此对象是不可用的。

JVM 相关参数

  • 堆大小

    • -Xmx, -Xms: 最大和最小可用内存
    • -Xmn: 年轻代大小。SUN官方推荐为整个堆的3/8。持久代一般为64m
    • -Xss: 每个线程的堆栈大小
    • -XX:NewRatio: 年轻代与年老代的比值
    • -XX:SurvivorRatio: 年轻代中Eden区与Survivor区(单个,两个Survivor区大小相同)大小比值
    • -XX:MaxPermSize: 持久代大小
    • -XX:MaxTenuringThreshold: 设置垃圾最大年龄,如果设置为0则年轻代对象不经过Servivor区,直接进入年老代
  • 收集器设置

    • -XX:+UseSerialGC:设置串行收集器
    • -XX:+UseParallelGC:设置并行收集器。仅对年轻代有效。
    • -XX:+UseParalledlOldGC:设置并行年老代收集器
    • -XX:+UseConcMarkSweepGC:设置年老代为并发收集器
    • -XX:+UserParNewGC: 设置年轻代为并行收集器
  • 垃圾回收统计信息

    • -XX:+PrintGC
    • -XX:+PrintGCDetails
    • -XX:+PrintGCTimeStamps
    • -Xloggc:filename
  • 并行收集器设置

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

    • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
    • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。 参考

辅助信息

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:

  • -XX:+PrintGC
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
  • -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
  • -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
  • -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
  • -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。

常用工具

JDK自带工具

Sum jdk自带了若干个内存相关的工具,位于%JAVA_HOME%bin目录下

  • jvsiualvm: Java VsiualVM

    • 该工具也可以单独下载http://visualvm.java.net/
    • 提供在 Java 虚拟机 (Java Virutal Machine, JVM) 上运行的 Java 应用程序的详细信息
    • 包含了jconsole, jps, jinfo, jtack, jmap等工具的功能
  • jps

    • 与unix上的ps类似,用来显示本地的java进程,可以查看本地运行着几个java程序,并显示他们的进程号。
  • jstat

    • 一个极强的监视VM内存工具。可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。
  • jmap

    • 打印出某个java进程(使用pid)内存内的,所有‘对象’的情况(如:产生那些对象,及其数量)
  • jconsole

    • 一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器VM
  • jstatd – Virtual Machine jstat Daemon

jstatd是一个RMI服务应用,用于提供接口允许远程监控工具连接到本地运行的JVM上。
如果没有找到已安装的安全管理器则jstatd服务会安装一个RMISecurityPolicy的实例,因此需要指定一个安全策略文件。该文件必须符合默认的”Policy File Syntax”规则。
下面的策略文件允许jstatd 服务运行而不受任何安全限制。

grant codebase "file:${java.home}/../lib/tools.jar" {
    permission java.security.AllPermission;
};

将上述文本拷贝到文件中,如jstatd.all.policy,并以如下命令运行jstatd服务:

jstatd -J-Djava.security.policy=statd.all.policy

成功启动后,就可以在其它机器上通过JVisualVM的远程主机连到该机器上。

关于jstatd的具体用法可参考oracle 官网说明

第三方工具

MAT: Eclipse内存分析插件 Memory Analyzer

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/scythe666/article/details/51852938
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞