jvm

Jingxc大约 18 分钟java后端java后端jvm

jvm

1. JVM 内存模型

线程独占:

  • 栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操作数栈,方法出口等信息,局部变量表存放的是 8大基础类型加上一个应用类型,所以还是一个指向地址的指针。
  • 本地方法栈:主要为 Native 方法服务
  • 程序计数器:记录当前线程执行的行号,执行Native 方法时 , 程序计数器为空 .

线程共享:

  • 堆:初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要在堆上分配。当堆没有可用空间时 , 会抛出 OOM 异常 . 根据对象的存活周期不同 ,JVM 把对象进行分代管理 , 由垃圾回收器进行垃圾的回收管理
  • 方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字节码)等数据。1.7的永久代和 1.8 的元空间都是方法区的一种实现
JVM内存模型
JVM内存模型

jvm模型示例对应的字节码文件

Compiled from "Math.java"
public class com.game.server.bean.Math {
  public com.game.server.bean.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int compute();
    Code:
       0: iconst_1 //将int类型常量1压入栈
       1: istore_1 //将int类型值存入局部变量1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/game/server/bean/Math
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method compute:()I
      12: pop
      13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: ldc           #6                  // String end
      18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      21: return
}

2. JVM 内存可见性

JMM 是定义程序中变量的访问规则 , 线程对于变量的操作只能在自己的工作内存中进行 , 而不能直接对主内存操作 . 由于指令重排序 , 读写的顺序会被打乱 , 因此 JMM 需要提供原子性 , 可见性 , 有序性保证 .

JMM:java 内存模型本身是一种抽象的概念并不真实存在,他是描述一组的规则和规范, 通过规范定义程序中变量的访问方式: JMM 规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁和解锁是同一把锁 JMM 特性:
  4. 可见性
  5. 原子性(不可分割,完整性,中间不可被加塞,保证数据一致性,完整性)
  6. 有序性 计算机在执行程序时,为了提高性能,编译器和处理器会对指令重排,一般分为以下 3 种
源代码->编译器的优化重排->指令并行的重排->内存系统重排->最终执行的指令

单线程环境里面确保了程序的最终执行结果和代码顺序执行的结果一致

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一直性是无法确定的,结果无法预测

3. 类的加载

其中 验证 , 准备 , 解析 合称链接

  • 加载: 通过类的完全限定名 , 查找此类字节码文件 , 利用字节码文件创建 Class 对象 .
  • 验证: 确保 Class 文件符合当前虚拟机的要求 , 不会危害到虚拟机自身安全 .
  • 准备: 进行内存分配 , 为 static 修饰的类变量分配内存 , 并设置初始值 (0 或 null). 不包含final 修饰的静态变量 , 因为final变量在编译时分配 .
  • 解析: 将常量池中的符号引用替换为直接引用的过程 . 直接引用为直接指向目标的指针或者相对偏移量等 .
  • 初始化: 主要完成静态块执行以及静态变量的赋值 . 先初始化父类 , 再初始化当前类 . 只有对类主动使用时才会初始化 .

提示

触发条件包括 , 创建类的实例时 , 访问类的静态方法或静态变量的时候 , 使用 Class.forName 反射类的时候 , 或者某个子类初始化的时候 .

Java 自带的加载器加载的类 , 在虚拟机的生命周期中是不会被卸载的 , 只有用户自定义的加载器加载的类才可以被卸 .

双亲委派模式 , 即加载器加载类时先把请求委托给自己的父类加载器执行 , 直到顶层的启动类加载器 .

  • 启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接引用。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH) 来加载 Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
  • 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现。

父类加载器能够完成加载则成功返回 , 不能则子类加载器才自己尝试加载 .

优点 :

  1. 避免类的重复加载
  2. 避免 Java 的核心 API 被篡改

4. GC判断

JAVA的垃圾回收是指回收内存中已经“死亡”的对象的内存空间

4.1 引用计数法


引用计数法是一种比较简单直接的算法,即在虚拟机中保存每个对象的被引用次数,例如对象 A 被对象 B 引用,则对象 A 的引用次数加一;当对象 B 释放对对象 A 的引用时,对象 A 的引用次数减一。当某个对象的引用次数为 0 时表示该对象已经死亡,会在下一次垃圾回收时被系统回收。

缺点: 无法解决循环引用的问题,例如对象 A 引用了对象 B,而对象 B 中又引用了对象 A,此时对象 A 和对象 B 之间就形成了循环引用,两者的引用次数一直不为 0,也就一直无法被回收。

4.2 根搜索路径可达性算法


可达性算法又叫根搜索算法,该算法由每一个根节点触发,根据对象之间的引用关系遍历所有与根节点关联的对象节点,在遍历完成后那些没有被遍历到的对象即为死亡的对象,会在下一次垃圾回收时被系统回收。

可达性算法有四种对象可以作为根节点:

  • Java 虚拟机栈中的引用对象
  • 方法区类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地栈中JNI(既一般说的 Native 方法)引用的对象

5. 垃圾回收算法

垃圾回收主要有三种算法:标记-清除算法,标记-复制算法和标记-整理算法。

5.1 标记-清除算法


标记-清除算法即每次垃圾回收时直接从内存空间中回收那些已经死亡的对象,使用此算法进行垃圾回收会在内存中留下一段段不连续的大小不一致的内存空间,当需要创建新对象时,就从这些零碎的内存空间中寻找一片足够大的内存空间用于存放该对象。 缺点:会留下许多零碎的、难以管理的内存空间,造成内存浪费。

5.2 复制算法


复制算法将内存空间一分为二,每次只使用其中的一半内存,在这一半内存满了的时候就将这块内存中存活的对象复制到另一半的内存中,然后释放这一半的内存空间。

缺点:会浪费一半的内存空间。有可能遇到一半内存满了,并且这一半内存中的所有对象都是存活着的情况。

注:由于大部分对象的存活时间很短,因此大部分虚拟机按照 8:1:1 的比例将内存空间划分为 Eden 和两个 Survivor 空间,每次使用Eden和一块Survivor空间,垃圾回收时将存活的对象一次性复制到另一块Survivor空间上。

5.3 标记-整理算法


标记-清除算法即在垃圾回收时从内存空间中回收那些已经死亡的对象,然后将剩下的存活的对象整理到一起,留下一片连续的内存空间。即在标记-清除的基础上加上整理的步骤。

缺点:整理阶段,由于移动了可用对象,需要去更新引用。

6. 分代回收

年轻代 -> 标记 - 复制 老年代 -> 标记 - 清除

分代垃圾回收器是由:新生代(Young Generation)和老生代(Tenured Generation)组成的,默认情况下新生代和老生代的内存比例是 1:2。

新生代是由:Eden、Form Survivor、To Survivor 三个区域组成的,它们内存默认占比是 8:1:1。 对象从Young generation区域消失的过程我们称之为 minor GC

老年代:对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,我们称之为 major GC

虚拟机中一般会对内存区域进行划分:新生代,老年代。然后根据各个年代的特点选择合适的收集算法:对于新生代,每次垃圾回收都会有大量的对象死去,因此可以选用复制算法,只需要付出少量存活对象的复制成本就可以完成垃圾回收;在老年代中,对象的存活率比较高,所以一般使用“标记-清除”算法或者“标记-整理”算法进行回收。

  • 大部分情况下,对象会在新生代的Eden区域分配内存
  • 大对象(需要大量连续内存空间的java对象)会直接进入老年代
  • 新生代中对象每次经历一次GC,年龄就加1,达到一定年龄之后就会移入老年代中,阈值是15岁
  • 动态对象年龄判断:当survivor空间中相同年龄的所有对象大小综合超过survivor空间的一半时,Survivor空间中所有年龄大于等于该年龄的对象可以直接进入老年代,不需要等待年龄到达阈值。
  • 空间分配担保机制:在分代收集算法中,老年代为新生代起担保作用,新生代内存满了会触发一次GC,在GC期间发现survivor空间不足以存放存活对象,那么这些存活对象会通过担保机制进入老年代

7. 垃圾回收器的分类

垃圾回收的分类如下:

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

7.1 CMS(Concurrent Mark Sweep)


CMS优点:

多线程:一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统。

CMS缺点:

  • 对 CPU 资源要求敏感:CMS 回收器过分依赖于多线程环境,默认情况下,开启的线程数为(CPU 的数量 + 3)/ 4,当 CPU 数量少于 4 个时,CMS 对用户本身的操作的影响将会很大,因为要分出一半的运算能力去执行回收器线程;
  • CMS 无法清除浮动垃圾:浮动垃圾指的是 CMS 清除垃圾的时候,还有用户线程产生新的垃圾,这部分未被标记的垃圾叫做“浮动垃圾”,只能在下次 GC 的时候进行清除;
  • CMS 垃圾回收会产生大量空间碎片:CMS 使用的是标记-清除算法,所有在垃圾回收的时候回产生大量的空间碎片。

7.2 G1 垃圾回收器

G1 垃圾回收器是一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于 CMS CG,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。

G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 Region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。

8. JVM Full GC的原因及应对策略

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收

(JDK8中无永生带了,元空间),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。

下边看看有那种情况触发JVM进行Full GC及应对策略。

8.1 System.gc()方法的调用


此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。

强烈建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

8.2 老年代空间不足


老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:

java.lang.OutOfMemoryError: Java heap space

为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

8.3 永生区空间不足


JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,

当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。

如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:

java.lang.OutOfMemoryError: PermGen space

为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

8.4 CMS GC时出现promotion failed和concurrent mode failure


对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能

会触发Full GC。 promotion failed:是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;

concurrent mode failure:是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。

措施为:增大survivor space、老年代空间或调低触发并发GC的比率

8.5 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间


这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之

前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。

例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,

则执行Full GC。

当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否

大于6MB,如小于,则触发对旧生代的回收。

8.6 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。


可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

8.7 堆中分配很大的对象

所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但停顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。

9. JVM调优

优先架构调优和代码调优,JVM优化是不得已的手段,大多数的Java应用不需要进行JVM优化

9.1 堆设置


  • -Xmx:3072M
  • -Xms:3072M

提示

参数-Xms和-Xmx,通常设置为相同的值,避免运行时要不断扩展JVM内存,每次垃圾回收都得重新分配,建议扩大至3-4倍FullGC后的老年代空间占用。

9.2 年轻代


  • -Xmn:1024M

提示

1-1.5倍FullGC之后的老年代空间占用。

避免新生代设置过小,当新生代设置过小时,会带来两个问题:一是minor GC次数频繁,二是可能导致 minor GC对象直接进老年代。当老年代内存不足时,会触发Full GC。 避免新生代设置过大,当新生代设置过大时,会带来两个问题:一是老年代变小,可能导致Full GC频繁执行;二是 minor GC 执行回收的时间大幅度增加。

线上生产环境,使用-Xmn一个即可(推荐)

或者同时使用 -XX:NewSize=1024m 和 -XX:MaxNewSize=1024m来设置

相关信息

-Xmn,-XX:NewSize/-XX:MaxNewSize,-XX:NewRatio 3组参数都可以影响年轻代的大小,混合使用的情况下,优先级是什么?

如下:

高优先级:-XX:NewSize/-XX:MaxNewSize

中优先级:-Xmn(默认等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=?)

低优先级:-XX:NewRatio

推荐使用-Xmn参数,原因是这个参数简洁,相当于一次设定 NewSize/MaxNewSIze,而且两者相等,适用于生产环境。-Xmn 配合 -Xms/-Xmx,即可将堆内存布局完成。

9.3 方法区


  • -XX:MetaspaceSize=256m
  • -XX:MaxMetaspaceSize=256m

提示

基于jdk1.7版本,永久代:参数-XX:PermSize和-XX:MaxPermSize;

基于jdk1.8版本,元空间:参数 -XX:MetaspaceSize和-XX:MaxMetaspaceSize;

通常设置为相同的值,避免运行时要不断扩展,建议扩大至1.2-1.5倍FullGc后的永久带空间占用。

9.4 年轻代中Eden区与Survivor区的比值


  • -XX:SurvivorRatio=4

提示

设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1个Eden区的比值为2:4,即1个Survivor区占整个年轻代大小的1/6。官方推荐幸存代占新生代的1/10。

9.5新生代存活区切换的次数


*-XX:MaxTenuringThreshold=15

提示

表示一个对象如果在Survivor区(救助空间)移动了15次还没有被垃圾回收就进入年老代。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。

9.6 堆dump


出现oom时生成堆dump

  • -XX:+HeapDumpOnOutOfMemoryError

生成堆文件地址

  • -XX:HeapDumpPath=/home/...

或者使用jmap生成 发现程序异常前通过执行指令,直接生成当前JVM的dump文件

jmap -dump:file=dump-log.dump pid

9.7 垃圾回收器

新生代使用ParNew

  • -XX:+UseParNewGC

老年代使用CMS

  • -XX:+UseConcMarkSweepGC
上次编辑于:
贡献者: Jingxc,jingxc