学习JVM必知必会的要点

标签:

本文出自jvm123.com-java技术分享站:http://jvm123.com/2019/07/jvm-points.html

1 运行时数据区

根据Java虚拟机定义,我们可以数据区域做如下区分,分为:堆、Java虚拟机栈、程序计数器、方法区(元数据区、运行时常量池)、本地方法栈。

学习JVM必知必会的要点插图

1 程序计数器

程序计数器是一块线程私有的区域,是一个较小的内存块,用来存放当前线程执行的字节码的指令地址,如果执行的是本地方法(Native),这个计数器就会为空(Undefined)。

2 Java虚拟机栈

Java虚拟机栈是线程私有的区域,生命周期与线程相同,它存储的是栈帧(Stack Frame),栈帧会来存储局部变量表、操作数栈、动态链接、方法出口和返回地址等信息。每一个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。如果线程请求的栈深度大于虚拟机所允许的最大深度,就会抛出StackOverflowError异常;如果申请栈内存不够,也会导致抛出OutOfMemoryError异常。

Jvm参数 -Xss:栈空间大小;栈的空间大小决定了栈能创建的深度

栈结构如下:

学习JVM必知必会的要点插图(1)

3 本地方法栈

本地方法栈和java方法栈非常类似,他们之前的区别主要是Java方法栈是提供给字节码服务的,本地方法栈是给本地方法(C语言实现)调用服务的。Java虚拟机并没有对本地方法栈中使用的语言、数据结构等进行强制规定,所以虚拟机可以自行实现它。Sun HotSpot虚拟机把虚拟机栈和Java方法栈进行了合二为一。本地方法栈也会和虚拟机栈一样抛出StackOverFlowError和OutOfMemoryError异常。

4 堆

Java堆是一个所有线程共享的区域,堆用来存储几乎所有对象的实例和数组,堆按照分代的思想进行划分,可以划分了新生代(YoungGeneration)和老年代(Old/Tenured Generation),新生代又可进一步细分为 eden、survivor space0(s0 或者 from space)和 survivor space1(s1或者to space)。我们用图来表示下堆的划分:

学习JVM必知必会的要点插图(2)

eden区:新建对象一般都放在该区域,除非是新建了大对象,该区域放不下就直接存放在老年代(Tenured)。 S0和S1区:该区域放置的对象至少经历了一次垃圾回收(Minor GC),如果经历了多次回收,到达指定次数还存活,那么就会被转移到老年代。

Java虚拟机规范规定堆可以是物理上不连续的空间,只需要逻辑上连续即可,我们可以通过命令(-Xmx和-Xms )来调整堆空间,如果申请的堆内存超过了堆的最大内存,将会抛出OutOfMemoryError异常。

Jvm参数

 -Xmx:最大堆空间大小

 -Xms:最小堆空间大小

 -Xmn:新生代空间大小

5 方法区(元数据区)

方法区是线程共享的区域,它用于存放已经被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。类信息包括类的完整名称、父类的完整名称、类型修饰符(public/protected/private)和类型的直接接口类表;常量池指运行时常量池(后面有介绍);方法区又被称为非堆(Non-Heap)。

在Host Spot虚拟机的实现中,方法区也被称为永久区,是一块独立于 Java 堆的内存空间。虽然叫永久区,但是永久区中的对象同样可以被 GC 回收的(注:方法区是 JVM 的一种规范,永久区是一种具体实现,在 Java8 中,永久区已经被 Metaspace 元空间取而代之。相应的,JVM参数 PermSize 和 MaxPermSize 被 MetaSpaceSize 和 MaxMetaSpaceSize 取代)。对永久区 GC 的回收,通常主要从两个方面分析:一是 GC 对永久区常量池的回收;二是永久区对类元数据的回收。

当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

5.1运行时常量池

运行时常量池(Run-Time Constant Pool)是方法区的一部分,它主要用来存放编译期生成的各种字面量和符号引用,既然是运行时常量池,理所应当的可以存放运行时产生的常量,比如调用String.intern()方法产生的字符串常量就会被放入运行池常量中。

垃圾判定算法

1 引用计数算法

引用计数法的思想比较简单,每个对象都有一个引用计数器,只要对象被引用,计数器就+1,当对象不再被引用时候,计数器就减一。这种算法很高效,但是有一个致命缺点,就是有循环引用的问题。对于两个无用对象的互相引用,就会导致两个对象的计数器不为0,从而无法被判定为无用对象,无法回收内存。

2 可达性分析算法

由于引用计数法有互相引用的缺陷,所以Java虚拟机采用了可达性分析算法来判定垃圾对象。这个算法的思想是,以一系列称为“GC Roots”的对象作为起始点,从这些起点往下搜索,搜索所走过的路径称为引用链(Referenc Chain),当一个对象到GC Roots没有任何引用链(从GC Roots到这个对象不可达)时,就说明这个对象不可用,可以被回收。

可以作为GC Roots的对象包括:

  • 虚拟机栈中引用的对象(局部变量)。
  • 方法区中类静态属性引用的对象(静态变量)。
  • 方法区中常量引用的对象(常量)。
  • 本地方法栈中JNI(即本地方法)引用的对象。

垃圾回收算法

1 标记-清除算法

学习JVM必知必会的要点插图(3)

标记-清除算法分为“标记”和“清除”两个阶段,首先需要标记出需要回收的对象,标记完成后再进行统一的垃圾回收。该算法有两个缺点:1.效率不高;2.清除后会产生大量不连续的内存碎片,内存碎片会导致分配大对象时候,无法找到足够的内存,从而提前触发一次GC。

2 复制算法

上面的标记清除算法效率不高,为了解决这个问题,就有了复制算法,复制算法就是把内存容量划分为大小相等的两块,每次只用其中一块,当一块内存用完后就将存活的对象复制到另外一块内存上,然后再对原内存块进行清理。这种算法的优点就是内存分配不用考虑碎片的问题,只需要移动堆顶的内存指针,按顺序分配内存即可。但是这算法的缺点就是空间利用率不高,将内存缩小为原来的一半,有一半的内存没有被真正利用起来。

虽然内存利用率不高,但是目前的虚拟机中堆中的新生代就是采用这种算法进行垃圾回收的。上面我们提到新生代分为 eden 空间、form 空间和 to空间3个部分。其中 from 和 to 空间可以视为用于复制的两块大小相同、地位相等,且可进行角色互换的空间块。from 和 to 空间也称为 survivor 空间,即幸存者空间,用于存放未被回收的对象。

在垃圾回收时,eden空间中存活的对象会被复制到未使用的survivor空间中(假设是 to),正在使用的survivor空间(假设是 from)中的年轻对象也会被复制到to空间中(大对象或者老年对象会直接进入老年代,如果to空间已满,则对象也会进入老年代)。此时eden和from空间中剩余对象就是垃圾对象,直接清空,to空间则存放此次回收后存活下来的对象。

3 标记-整理算法

复制算法只适用于存活率较低的新生代中,如果存活率较高就需要进行过多的复制操作,效率将会降低。老年代的存活率比较高,所以复制算法不适用于老年代的场景,之前提到的“标记-清除”算法,如果不会产生内存碎片的话,还是可以满足老年代的,那么有没有不产生碎片的类似算法呢?答案是有,“标记-整理”算法就派上用处了。它的核心思想是:先对可回收对象进行标记,然后把所有存活的对象移动到一端,接着直接清理掉边界意外的内存区域。因为清理过后,存活对象都紧密的在一端,所以不会产生内存碎片。

学习JVM必知必会的要点插图(4)

垃圾判定算法现在虚拟机主要使用可达性分析算法,垃圾回收算法有“标记-清除”算法、“复制”算法、“标记-整理”算法。“复制”算法比较适合存活对象较少的新生代,“标记-整理”算法比较适合老年代,整理的作用就是为了有连续的内存空间,防止内存碎片太多无法存放大对象。

JVM调优建议

jvm性能度量指标

  • 吞吐量:表示系统减去系统回收时间占总时间的比率,比如系统运行了100秒,垃圾回收占用了1秒,那么吞吐量量就是(100-1)/100=99%。
  • 垃圾回收消耗:和吞吐量相反,垃圾回收器消耗指垃圾回收器耗时与系统运行总时间的比值。
  • 停顿时间:指垃圾回收器运行时,系统停顿的时间。
  • 回收频率:指垃圾回收器多长时间会运行一次。一般来说,对于固定的应用而言,垃圾回收器的频率应该是越低越好。通常增大堆空间可以有效降低垃圾回收发生的频率,但是可能会增加回收产生的停顿时间。
  • 反应时间:当一个内存对象被标记为垃圾对象后到这个对象被真正回收产生的时间。

根据这几个指标,我们可以知道,垃圾回收性能好的表现是:吞吐量高,垃圾回收消耗低,停顿时间少,回收频率低,反应时间快。但是,并没有这么完美的性能表现,这几个指标有些是互斥的,比如要降低回收频率,就要扩大空间,但是就会增加停顿时间;同样要想反应时间快,就必须要提高回收频率。所以,这些性能的追求就是一个博弈平衡的过程,我们可以根据我们追求的某一方面来进行调优,比如,对于客户端应用而言,应该尽可能降低其停顿时间,给用户良好的使用体验,为此,可以牺牲垃圾回收的吞吐量;对服务端程序来说,可能会更加关注吞吐量。

1 将新对象预留在年轻代

众所周知,由于 Full GC 的成本远远高于 Minor GC,因此某些情况下需要尽可能将对象分配在年轻代,这在很多情况下是一个明智的选择。虽然在大部分情况下,JVM 会尝试在 Eden 区分配对象,但是由于空间紧张等问题,很可能不得不将部分年轻对象提前向年老代压缩。因此,在 JVM 参数调优时可以为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的情况发生。这里实际上是为了避免“朝生夕灭”的大对象发生,尽可能的把设置合理新生代空间,把“朝生夕灭 ”对象留在新生代中。

2 将大对象直接分配再老年代

我们分配对象一般都是分配在年轻代,分配大对象在年轻代,需要年轻代提供足够的空间,这个时候会导致原有的大量小对象进入老年代,占用老年代空间。基于以上原因,可以将大对象直接分配到年老代,从而保留为年轻代保留了空间,保证了年轻代原有的目的,这样也可以提高 GC 的效率。如果一个大对象同时又是一个短命的对象,假设这种情况出现很频繁,那对于 GC 来说会是一场灾难。原本应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路。因此,在软件开发过程中,应该尽可能避免使用“朝生夕灭”这样短命的大对象。可以使用参数-XX:PetenureSizeThreshold 设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。参数-XX:PetenureSizeThreshold 只对串行收集器和年轻代并行收集器有效,并行回收收集器不识别这个参数。

3 设置对象进入老年代的年龄

堆中的每一个对象都有自己的年龄。一般情况下,年轻对象存放在年轻代,老年对象存放在老年代。为了做到这点,虚拟机为每个对象都维护一个年龄。如果对象在 Eden 区,经过一次 GC 后依然存活,则被移动到 Survivor 区中,对象年龄加 1。以后,如果对象每经过一次 GC 依然存活,则年龄再加 1。当对象年龄达到阈值时,就移入老年代,成为老年对象。那么设置一个合适的老年代的年龄就有利于提升系统性能,可以通过-XX:MaxTenuringThreshold 来设置,默认值是 15。虽然-XX:MaxTenuringThreshold 的值可能是 15 或者更大,但这不意味着新对象非要达到这个年龄才能进入老年代。如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

4 稳定的堆与震荡的堆

一般来说,稳定的堆大小对垃圾回收是有利的。获得一个稳定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆) 一样。如果这样设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少 GC 的次数。因此,很多服务端应用都会将最大堆和最小堆设置为相同的数值。稳定的堆大小虽然可以减少 GC 次数,但同时也增加了每次 GC 的时间。让堆大小在一个区间中震荡,在系统不需要使用大内存时,压缩堆空间,使 GC 应对一个较小的堆,可以加快单次 GC 的速度。基于这样的考虑,JVM 还提供了两个参数用于压缩和扩展堆空间。

XX:MinHeapFreeRatio: 设置堆的最小空闲比例,默认是40,当堆空间的空闲空间小于这个数值时,jvm会自动扩展空间。

-XX:MaxHeapFreeRatio: 设置堆的最大空闲比例,默认是70,当堆空间的空闲空间大于这个数值时,jvm会自动压缩空间。

当-Xmx 和-Xms 相等时,-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 两个参数无效。

5 根据场景选择合适的收集器

对于对响应时间不敏感的场景,可以选择吞吐量优先的收集器来提升性能,比如Parallel Old 收集器。如果是对响应时间要求高的场景,就需要选择低停顿的垃圾回收器,比如CMS,G1,ZGC(虽然目前还不是非常成熟)。

程序内存溢出获取 堆快照(堆Dump)

获得程序的堆快照文件有很多方法, 比较常用的取得堆快照文件的方法是使用-XX:+HeapDumpOnOutOfMemoryError 参数在程序发生OOM时,导出应用程序的当前堆快照。

通过参数 -XX:heapDumpPath 可以指定堆快照的保存位置。

-Xmx10m -XX:+HeapDumpOnOutOfMemoryError -XX:heapDumpPath=D:\m.hprof

发表评论