admin管理员组

文章数量:1547177

为什么要jvm gc可能是java程序猿或非java程序猿讨论最多的话题,到底怎么回事?

本篇内容:内存管理发展史,JVM内存管理分析和实践(基础知识),JVM8调优(内存运行原理和编译优化)  ;20000字+,5分钟阅读。

篇幅较长分2篇,下一篇讲gc优化,内存分配优化,内存持续上升排查方法

第一部分,内存管理发展史

1.1硬件内存管理

1.2OS内存管理

有关于硬件和OS内存管理,有很多资料,比如 OS 内存管理 - 知乎,不再这里详细说明,这里主要讲jvm

第二部分:JVM内存管理分析

2.1 jvm oracle发展版本

jvm新版本性能指标

基准方法

硬件:一个稳定的机器不运行任何其他的计算要求苛刻的流程,配置:Intel® Xeon® Silver 4116 @ 2.1 GHz (12 cores total / 24 threads)和128 GiBRAM内存,运行RHEL 8 x86_64

运行次数:每个JDK 和每个垃圾收集器组合按顺序运行 3 次。下面的结果是这 3 次运行的平均值。

测试结果

备注:

查看 3 次单独运行的原始数据(此处未显示),机器重新分配数(B1 和 B10)在同一 JDK 和 GC 上的运行之间波动很大,通常超过10%,其他数字不会受到这种不可靠性的影响。

可以以说忽略 Machine Reassignment numbers 更好。但是为了避免挑选数据的问题,这些结果和平均值确实把它们包括进来了。

 6个重要的JVM性能参数 - 知乎

基准测试总结

  • 平均而言,以 OptaPlanner 为例的基准测试结果表明:

  • 对于 G1GC(默认),Java 17 比 Java 11 快 8.66%,比 Java 16 快 2.41%。

  • 对于 ParallelGC,Java 17 比 Java 11 快 6.54%,比 Java 16 快 0.37%。

  • Parallel GC 比 G1 GC 快 16.39%。

2.2 jvm内存模型设计

程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。

Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。

Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。

Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。

方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享
 

为快速理解和实践,以考促学

1  什么情况下会发生栈内存溢出。
栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。
如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多)
参数 -Xss 去调整JVM栈的大小

2 JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor
1)共享内存区划分

共享内存区 = 持久代 + 堆
持久代 = 方法区 + 其他
Java堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
2)一些参数的配置

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)
Survivor区中的对象被复制次数为15(对应虚拟机参数 -XX:+MaxTenuringThreshold)


3)为什么要分为Eden和Survivor?为什么要设置两个Survivor区?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

3 JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代
Java堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor区。
大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态;
如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。
Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上

4  你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点
1)几种垃圾收集器:
Serial收集器: 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。
ParNew收集器: Serial收集器的多线程版本,也需要stop the world,复制算法。
Parallel Scavenge收集器: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。
Serial Old收集器: 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
G1收集器: 标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。

2)CMS收集器和G1收集器的区别:
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
CMS收集器以最小的停顿时间为目标的收集器;
G1收集器可预测垃圾回收的停顿时间
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
 
 
5 JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存

1)内存模型

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
 

2)指令重排序

public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;
 
public static void main(String[] args) throws InterruptedException {
    Thread one = new Thread(new Runnable() {
        public void run() {
            a = 1;
            x = b;
        }
    });
 
    Thread other = new Thread(new Runnable() {
        public void run() {
            b = 1;
            y = a;
        }
    });
    one.start();other.start();
    one.join();other.join();
    System.out.println(“(” + x + “,” + y + “)”);
}

 运行结果可能为(1,0)、(0,1)或(1,1),也可能是(0,0)。因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是指令重排


 3)内存屏障

内存屏障,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。

LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

4) happen-before原则

单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
线程中断的happen-before原则 :对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
线程终结的happen-before原则: 线程中的所有操作都happen-before线程的终止检测。
对象创建的happen-before原则: 一个对象的初始化完成先于他的finalize方法调用

6 简单说说你了解的类加载器,可以打破双亲委派么,怎么打破
1) 什么是类加载器?

类加载器 就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象
启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
   扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
   应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

双亲委派模型
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载

3)为什么需要双亲委派模型?

在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码

4)怎么打破双亲委派模型?

打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。

7  说说你知道的几种主要的JVM参数
1)堆栈配置相关
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k 
-XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=0
-Xmx3550m: 最大堆大小为3550m。

-Xms3550m: 设置初始堆大小为3550m。

-Xmn2g: 设置年轻代大小为2g。

-Xss128k: 每个线程的堆栈大小为128k。

-XX:MaxPermSize: 设置持久代大小为16m

-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。

-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代

2)垃圾收集器相关

-XX:+UseParallelGC
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC 
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection:

-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。

-XX:ParallelGCThreads=20: 配置并行收集器的线程数

-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
 
 3)辅助信息相关

-XX:+PrintGC
-XX:+PrintGCDetails

-XX:+PrintGC 输出形式:

[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails 输出形式:

[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs


8  怎么打出线程栈信息
输入jps,获得进程号。
top -Hp pid 获取本进程中所有线程的CPU耗时性能
jstack pid命令查看当前java进程的堆栈状态
或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件。
可以使用fastthread 堆栈定位,fastthread.io/

9 强引用、软引用、弱引用、虚引用的区别?
1)强引用
我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。

2)软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用
用处: 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出

Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用        
if(sr.get()!=null){ 
    rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
}else{
    prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了
    sr = new SoftReference(prev);       // 重新构建
}

4)虚引用

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动

综上所述,我们大概了解了JVM及GC设计和作用,回到主题,为什么要gc呢,在所有的程序设计中,都要进行内存管理,为什么不直接用OS内存管理呢?java设计者考虑到跨OS,肯定要独立于OS内存管理。

第三部分:jvm调优

3.1 JVM 的运行原理

那你可能又有疑问了,Java8 为什么使用元空间替代永久代,这样做有什么好处呢?

官方给出的解释是:

移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有 永久代,所以不需要配置永久代。 永久代内存经常不够用或发生内存溢出,爆出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩 很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于 很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

看到这里,相信你对 JVM 内存模型已经有个充分的了解了。接下来,我们通过一个案例来 了解下代码和对象是如何分配存储的,Java 代码又是如何在 JVM 中运行的。

public class JVMCase {

    public final static String MAN_SEX_TYPE="man";

    public final static String WOMEN_SEX_TYPE="woman";


    public static void main(String  []args) {
        Student stu=new Student();
        stu.setName("nick");
        stu.setSexType(MAN_SEX_TYPE);
        stu.setAge(20);
        JVMCase jvmcase=new JVMCase();
        print(stu);
        jvmcase.sayHello(stu);

    }
    public static void print(Student stu){
         System.out.println("name:"+stu.getName()+";sex:"+stu.getSexType());
    }
    public  void sayHello(Student stu){
        System.out.println(stu.getName()+";sayHello");
    }

}

public class Student {

    String name;
    String sexType;
    int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSexType() {
        return sexType;
    }

    public void setSexType(String sexType) {
        this.sexType = sexType;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

 

javap -v JVMcase.class

M:\code-2022\NettyProtobufTcpClient\target\test-classes\cn\xiaosheng996\NettyProtobufTcpClient>javap -v JVMCase.class
Classfile /M:/code-2022/NettyProtobufTcpClient/target/test-classes/cn/xiaosheng996/NettyProtobufTcpClient/JVMCase.class
  Last modified 2022-4-10; size 1511 bytes
  MD5 checksum af92dfe5d624543ed942130d848a29cc
  Compiled from "JVMCase.java"
public class cn.xiaosheng996.NettyProtobufTcpClient.JVMCase
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #24.#49        // java/lang/Object."<init>":()V
   #2 = Class              #50            // cn/xiaosheng996/NettyProtobufTcpClient/Student
   #3 = Methodref          #2.#49         // cn/xiaosheng996/NettyProtobufTcpClient/Student."<init>":()V
   #4 = String             #51            // nick
   #5 = Methodref          #2.#52         // cn/xiaosheng996/NettyProtobufTcpClient/Student.setName:(Ljava/lang/String;)V
   #6 = Class              #53            // cn/xiaosheng996/NettyProtobufTcpClient/JVMCase
   #7 = String             #54            // man
   #8 = Methodref          #2.#55         // cn/xiaosheng996/NettyProtobufTcpClient/Student.setSexType:(Ljava/lang/String;)V
   #9 = Methodref          #2.#56         // cn/xiaosheng996/NettyProtobufTcpClient/Student.setAge:(I)V
  #10 = Methodref          #6.#49         // cn/xiaosheng996/NettyProtobufTcpClient/JVMCase."<init>":()V
  #11 = Methodref          #6.#57         // cn/xiaosheng996/NettyProtobufTcpClient/JVMCase.print:(Lcn/xiaosheng996/NettyProtobufTcpClient/Student;)V
  #12 = Methodref          #6.#58         // cn/xiaosheng996/NettyProtobufTcpClient/JVMCase.sayHello:(Lcn/xiaosheng996/NettyProtobufTcpClient/Student;)V
  #13 = Fieldref           #59.#60        // java/lang/System.out:Ljava/io/PrintStream;
  #14 = Class              #61            // java/lang/StringBuilder
  #15 = Methodref          #14.#49        // java/lang/StringBuilder."<init>":()V
  #16 = String             #62            // name:
  #17 = Methodref          #14.#63        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #18 = Methodref          #2.#64         // cn/xiaosheng996/NettyProtobufTcpClient/Student.getName:()Ljava/lang/String;
  #19 = String             #65            // ;sex:
  #20 = Methodref          #2.#66         // cn/xiaosheng996/NettyProtobufTcpClient/Student.getSexType:()Ljava/lang/String;
  #21 = Methodref          #14.#67        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #22 = Methodref          #68.#69        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #23 = String             #70            // ;sayHello
  #24 = Class              #71            // java/lang/Object
  #25 = Utf8               MAN_SEX_TYPE
  #26 = Utf8               Ljava/lang/String;
  #27 = Utf8               ConstantValue
  #28 = Utf8               WOMEN_SEX_TYPE
  #29 = String             #72            // woman
  #30 = Utf8               <init>
  #31 = Utf8               ()V
  #32 = Utf8               Code
  #33 = Utf8               LineNumberTable
  #34 = Utf8               LocalVariableTable
  #35 = Utf8               this
  #36 = Utf8               Lcn/xiaosheng996/NettyProtobufTcpClient/JVMCase;
  #37 = Utf8               main
  #38 = Utf8               ([Ljava/lang/String;)V
  #39 = Utf8               args
  #40 = Utf8               [Ljava/lang/String;
  #41 = Utf8               stu
  #42 = Utf8               Lcn/xiaosheng996/NettyProtobufTcpClient/Student;
  #43 = Utf8               jvmcase
  #44 = Utf8               print
  #45 = Utf8               (Lcn/xiaosheng996/NettyProtobufTcpClient/Student;)V
  #46 = Utf8               sayHello
  #47 = Utf8               SourceFile
  #48 = Utf8               JVMCase.java
  #49 = NameAndType        #30:#31        // "<init>":()V
  #50 = Utf8               cn/xiaosheng996/NettyProtobufTcpClient/Student
  #51 = Utf8               nick
  #52 = NameAndType        #73:#74        // setName:(Ljava/lang/String;)V
  #53 = Utf8               cn/xiaosheng996/NettyProtobufTcpClient/JVMCase
  #54 = Utf8               man
  #55 = NameAndType        #75:#74        // setSexType:(Ljava/lang/String;)V
  #56 = NameAndType        #76:#77        // setAge:(I)V
  #57 = NameAndType        #44:#45        // print:(Lcn/xiaosheng996/NettyProtobufTcpClient/Student;)V
  #58 = NameAndType        #46:#45        // sayHello:(Lcn/xiaosheng996/NettyProtobufTcpClient/Student;)V
  #59 = Class              #78            // java/lang/System
  #60 = NameAndType        #79:#80        // out:Ljava/io/PrintStream;
  #61 = Utf8               java/lang/StringBuilder
  #62 = Utf8               name:
  #63 = NameAndType        #81:#82        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #64 = NameAndType        #83:#84        // getName:()Ljava/lang/String;
  #65 = Utf8               ;sex:
  #66 = NameAndType        #85:#84        // getSexType:()Ljava/lang/String;
  #67 = NameAndType        #86:#84        // toString:()Ljava/lang/String;
  #68 = Class              #87            // java/io/PrintStream
  #69 = NameAndType        #88:#74        // println:(Ljava/lang/String;)V
  #70 = Utf8               ;sayHello
  #71 = Utf8               java/lang/Object
  #72 = Utf8               woman
  #73 = Utf8               setName
  #74 = Utf8               (Ljava/lang/String;)V
  #75 = Utf8               setSexType
  #76 = Utf8               setAge
  #77 = Utf8               (I)V
  #78 = Utf8               java/lang/System
  #79 = Utf8               out
  #80 = Utf8               Ljava/io/PrintStream;
  #81 = Utf8               append
  #82 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #83 = Utf8               getName
  #84 = Utf8               ()Ljava/lang/String;
  #85 = Utf8               getSexType
  #86 = Utf8               toString
  #87 = Utf8               java/io/PrintStream
  #88 = Utf8               println
{
  public static final java.lang.String MAN_SEX_TYPE;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: String man

  public static final java.lang.String WOMEN_SEX_TYPE;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: String woman

  public cn.xiaosheng996.NettyProtobufTcpClient.JVMCase();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/xiaosheng996/NettyProtobufTcpClient/JVMCase;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class cn/xiaosheng996/NettyProtobufTcpClient/Student
         3: dup
         4: invokespecial #3                  // Method cn/xiaosheng996/NettyProtobufTcpClient/Student."<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #4                  // String nick
        11: invokevirtual #5                  // Method cn/xiaosheng996/NettyProtobufTcpClient/Student.setName:(Ljava/lang/String;)V
        14: aload_1
        15: ldc           #7                  // String man
        17: invokevirtual #8                  // Method cn/xiaosheng996/NettyProtobufTcpClient/Student.setSexType:(Ljava/lang/String;)V
        20: aload_1
        21: bipush        20
        23: invokevirtual #9                  // Method cn/xiaosheng996/NettyProtobufTcpClient/Student.setAge:(I)V
        26: new           #6                  // class cn/xiaosheng996/NettyProtobufTcpClient/JVMCase
        29: dup
        30: invokespecial #10                 // Method "<init>":()V
        33: astore_2
        34: aload_1
        35: invokestatic  #11                 // Method print:(Lcn/xiaosheng996/NettyProtobufTcpClient/Student;)V
        38: aload_2
        39: aload_1
        40: invokevirtual #12                 // Method sayHello:(Lcn/xiaosheng996/NettyProtobufTcpClient/Student;)V
        43: return
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 14
        line 14: 20
        line 15: 26
        line 16: 34
        line 17: 38
        line 19: 43
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      44     0  args   [Ljava/lang/String;
            8      36     1   stu   Lcn/xiaosheng996/NettyProtobufTcpClient/Student;
           34      10     2 jvmcase   Lcn/xiaosheng996/NettyProtobufTcpClient/JVMCase;

  public static void print(cn.xiaosheng996.NettyProtobufTcpClient.Student);
    descriptor: (Lcn/xiaosheng996/NettyProtobufTcpClient/Student;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #14                 // class java/lang/StringBuilder
         6: dup
         7: invokespecial #15                 // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #16                 // String name:
        12: invokevirtual #17                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_0
        16: invokevirtual #18                 // Method cn/xiaosheng996/NettyProtobufTcpClient/Student.getName:()Ljava/lang/String;
        19: invokevirtual #17                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: ldc           #19                 // String ;sex:
        24: invokevirtual #17                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        27: aload_0
        28: invokevirtual #20                 // Method cn/xiaosheng996/NettyProtobufTcpClient/Student.getSexType:()Ljava/lang/String;
        31: invokevirtual #17                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        34: invokevirtual #21                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        37: invokevirtual #22                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        40: return
      LineNumberTable:
        line 21: 0
        line 22: 40
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      41     0   stu   Lcn/xiaosheng996/NettyProtobufTcpClient/Student;

  public void sayHello(cn.xiaosheng996.NettyProtobufTcpClient.Student);
    descriptor: (Lcn/xiaosheng996/NettyProtobufTcpClient/Student;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #14                 // class java/lang/StringBuilder
         6: dup
         7: invokespecial #15                 // Method java/lang/StringBuilder."<init>":()V
        10: aload_1
        11: invokevirtual #18                 // Method cn/xiaosheng996/NettyProtobufTcpClient/Student.getName:()Ljava/lang/String;
        14: invokevirtual #17                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: ldc           #23                 // String ;sayHello
        19: invokevirtual #17                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: invokevirtual #21                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: invokevirtual #22                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return
      LineNumberTable:
        line 24: 0
        line 25: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lcn/xiaosheng996/NettyProtobufTcpClient/JVMCase;
            0      29     1   stu   Lcn/xiaosheng996/NettyProtobufTcpClient/Student;
}
SourceFile: "JVMCase.java"

当我们通过 Java 运行以上代码时,JVM 的整个处理过程如下: 1.JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统 申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址 分配给 JVM,接下来 JVM 就进行内部分配。 2.JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。 3.class 文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始 化为系统的初始值(这部分我在第 21 讲还会详细介绍)。

4. 完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM 首先会执行构 <clinit>造器 方法,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代 码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 <clinit>() 方法。

5. 执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会 创建一个 student 对象,对象引用 student 就存放在栈(虚拟机栈)中。

6. 此时再次创建一个 JVMCase 对象,调用 sayHello 非静态方法,sayHello 方法属于对 象 JVMCase,此时 sayHello 方法入栈,并通过栈中的 student 引用调用堆中的 Student 对象;之后,调用静态方法 print,print 静态方法属于 JVMCase 类,是从静态方法中获 取,之后放入到栈中,也是通过 student 引用调用堆中的 student 对象。

如今,JVM 在很大程度上减轻了 Java 开发人员投入到对象生命周期的管理精力。在使用对 象的时候,JVM 会自动分配内存给对象,在不使用的时候,垃圾回收器会自动回收对象, 释放占用的内存。

3.2 深入JVM即时编译器JIT,优化Java编译

说到编译,我猜你一定会想到 .java 文件被编译成 .class 文件的过程,这个编译我们一般称 为前端编译。Java 的编译和运行过程非常复杂,除了前端编译,还有运行时编译。由于机 器无法直接运行 Java 生成的字节码,所以在运行时,JIT 或解释器会将字节码转换成机器 码,这个过程就叫运行时编译。 类文件在运行时被进一步编译,它们可以变成高度优化的机器代码,由于 C/C++ 编译器的 所有优化都是在编译期间完成的,运行期间的性能监控仅作为基础的优化措施则无法进行, 例如,调用频率预测、分支频率预测、裁剪未被选择的分支等,而 Java 在运行时的再次编 译,就可以进行基础的优化措施。因此,JIT 编译器可以说是 JVM 中运行时编译最重要的 部分之一

然而许多 Java 开发人员对 JIT 编译器的了解并不多,不深挖其工作原理,也不深究如何检 测应用程序的即时编译情况,线上发生问题后很难做到从容应对。今天我们就来学习运行时 编译如何实现对 Java 代码的优化。

类编译加载执行过程

3.2.1类编译

在编写好代码之后,我们需要将 .java 文件编译成 .class 文件,才能在虚拟机上正常运行代 码。文件的编译通常是由 JDK 中自带的 Javac 工具完成,一个简单的 .java 文件,我们可 以通过 javac 命令来生成 .class 文件。

 看似一个简单的命令执行,前期编译的过程其实是非常复杂的,包括词法分析、填充符号 表、注解处理、语义分析以及生成 class 文件,这个过程我们不用过多关注。只要从上图中 知道,编译后的字节码文件主要包括常量池和方法表集合这两部分就可以了。

常量池主要记录的是类文件中出现的字面量以及符号引用。字面常量包括字符串常量(例如 String str=“abc”,其中"abc"就是常量),声明为 final 的属性以及一些基本类型(例 如,范围在 -127-128 之间的整型)的属性。符号引用包括类和接口的全限定名、类引用、 方法引用以及成员变量引用(例如 String str=“abc”,其中 str 就是成员变量引用)等。

方法表集合中主要包含一些方法的字节码、方法访问权限(public、protect、prviate 等)、方法名索引(与常量池中的方法引用对应)、描述符索引、JVM 执行指令以及属性 集合等。

 

3.2.2类加载

当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类 加载器将字节码文件加载到内存中。

 不同的实现类由不同的类加载器加载,JDK 中的本地方法类一般由根加载器(Bootstrp loader)加载进来,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现 加载,而程序中的类文件则由系统加载器(AppClassLoader )实现加载。 在类加载后,class 类文件中的常量池信息以及其它数据会被保存到 JVM 内存的方法区 中。

3.2.3类连接

类在加载进来之后,会进行连接、初始化,最后才会被使用。在连接过程中,又包括验证、 准备和解析三个部分。

验证:验证类符合 Java 规范和 JVM 规范,在保证符合规范的前提下,避免危害虚拟机安 全。


准备:为类的静态变量分配内存,初始化为系统的初始值。对于 final static 修饰的变量, 直接赋值为用户的定义值。例如,private final static int value=123,会在准备阶段分配 内存,并初始化值为 123,而如果是 private static int value=123,这个阶段 value 的值 仍然为 0。

解析:将符号引用转为直接引用的过程。我们知道,在编译时,Java 类并不知道所引用的 类的实际地址,因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用,包 括类和接口的全限定名、类引用、方法引用以及成员变量引用等。如果要使用这些类和方 法,就需要把它们转化为 JVM 可以直接获取的内存地址或指针,即直接引用。

类初始化

类初始化阶段是类加载过程的最后阶段,在这个阶段中,JVM 首先将执行构造器 方法,编译器会在将 .java 文件编译成 .class 文件时,收集所有类初始化代码,包括静态变 量赋值语句、静态代码块、静态方法,收集在一起成为 <clinit>() 方法。 初始化类的静态变量和静态代码块为用户自定义的值,初始化的顺序和 Java 源码从上到下 的顺序一致。例如:

 

JVM 会保证 <clinit>() 方法的线程安全,保证同一时间只有一个线程执行。 JVM 在初始化执行代码时,如果实例化一个新对象,会调用 方法对实例变量进行初 始化,并执行对应的构造方法内的代码。

即时编译

初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中 才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编 译。 最初,虚拟机中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个方法 或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。 为了提高热点代码的执行效率,在运行时,即时编译器(JIT)会把这些代码编译成与本地 平台相关的机器码,并进行各层次的优化,然后保存到内存中。

 

即时编译器类型

在 HotSpot 虚拟机中,内置了两个 JIT,分别为 C1 编译器和 C2 编译器,这两个编译器的 编译过程是不一样的。 C1 编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较 短或对启动性能有要求的程序,例如,GUI 应用对界面启动速度就有一定要求。 C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或 对峰值性能有要求的程序。根据各自的适配性,这两种即时编译也被称为 Client Compiler 和 Server Compiler。 在 Java7 之前,需要根据程序的特性来选择对应的 JIT,虚拟机默认采用解释器和其中一个 编译器配合工作。 Java7 引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,我们 也可以通过参数 “-client”“-server” 强制指定虚拟机的即时编译模式。分层编译将 JVM 的执行状态分为了 5 个层次:

第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第 二层编译;

第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling;

第 2 层:也称为 C1 编译,开启 Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译;

第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译;

第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较 长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

在 Java8 中,默认开启分层编译,-client 和 -server 的设置已经是无效的了。如果只想开 启 C2,可以关闭分层编译(-XX:-TieredCompilation),如果只想用 C1,可以在打开分 层编译的同时,使用参数:-XX:TieredStopAtLevel=1。

除了这种默认的混合编译模式,我们还可以使用“-Xint”参数强制虚拟机运行于只有解释 器的编译模式下,这时 JIT 完全不介入工作;我们还可以使用参数“-Xcomp”强制虚拟机 运行于只有 JIT 的编译模式下。 通过 java -version 命令行可以直接查看到当前系统使用的编译模式。如下图所示:

热点探测

在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测, 采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一 定的阈值就认为它是“热点方法” 。

虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计 数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这两个计数器都有一个 确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。

方法调用计数器:用于统计方法被调用的次数,方法调用计数器的默认阈值在 C1 模式下是 1500 次,在 C2 模式在是 10000 次,可通过 -XX: CompileThreshold 来设定;而在分层 编译的情况下,-XX: CompileThreshold 指定的阈值将失效,此时将会根据当前待编译的 方法数以及编译线程数来动态调整。当方法计数器和回边计数器之和超过方法计数器阈值 时,就会触发 JIT 编译器。

回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转 的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值,在不开启分 层编译的情况下,C1 默认为 13995,C2 默认为 10700,可通过 -XX: OnStackReplacePercentage=N 来设置;而在分层编译的情况下,-XX: OnStackReplacePercentage 指定的阈值同样会失效,此时将根据当前待编译的方法数以 及编译线程数来动态调整。

建立回边计数器的主要目的是为了触发 OSR(On StackReplacement)编译,即栈上编 译。在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段 是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直 接将执行代码替换,执行缓存的机器语言。

编译优化技术

JIT 编译运用了一些经典的编译优化技术来实现代码的优化,即通过一些例行检查优化,可 以智能地编译出运行时的最优性能代码。今天我们主要来学习以下两种优化手段:

1. 方法内联

调用一个方法通常要经历压栈和出栈。调用方法是将程序执行顺序转移到存储该方法的内存 地址,将方法的内容执行完后,再返回到执行该方法前的位置。

这种执行操作要求在执行前保护现场并记忆执行的地址,执行后要恢复现场,并按原来保存 的地址继续执行。 因此,方法调用会产生一定的时间和空间方面的开销

那么对于那些方法体代码不是很大,又频繁调用的方法来说,这个时间和空间的消耗会很 大。方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实 的方法调用。

例如以下方法:

最终会被优化为:

JVM 会自动识别热点方法,并对它们使用方法内联进行优化。我们可以通过 - XX:CompileThreshold 来设置热点方法的阈值。但要强调一点,热点方法不一定会被 JVM 做内联优化,如果这个方法体太大了,JVM 将不执行内联操作。而方法体的大小阈值,我 们也可以通过参数设置来优化:

经常执行的方法,默认情况下,方法体大小小于 325 字节的都会进行内联,我们可以通 过 -XX:MaxFreqInlineSize=N 来设置大小值; 不是经常执行的方法,默认情况下,方法大小小于 35 字节才会进行内联,我们也可以通 过 -XX:MaxInlineSize=N 来重置大小值。

之后我们就可以通过配置 JVM 参数来查看到方法被内联的情况:

当我们设置 VM 参数:-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions - XX:+PrintInlining 之后,运行以下代码:

我们可以看到运行结果中,显示了方法内联的日志:

热点方法的优化可以有效提高系统性能,一般我们可以通过以下几种方式来提高方法内联:

通过设置 JVM 参数来减小热点阈值或增加方法体阈值,以便更多的方法可以进行内联, 但这种方法意味着需要占用更多地内存; 在编程中,避免在一个方法中写大量代码,习惯使用小方法体; 尽量使用 final、private、static 关键字修饰方法,编码方法因为继承,会需要额外的类 型检查

2. 逃逸分析

逃逸分析(Escape Analysis)是判断一个对象是否被外部方法引用或外部线程访问的分析 技术,编译器会根据逃逸分析的结果对代码进行优化。

栈上分配

我们知道,在 Java 中默认创建一个对象是在堆中分配内存的,而当堆内存中的对象不再使 用时,则需要通过垃圾回收机制回收,这个过程相对分配在栈中的对象的创建和销毁来说, 更消耗时间和性能。这个时候,逃逸分析如果发现一个对象只在方法中使用,就会将对象分 配在栈上。 以下是通过循环获取学生年龄的案例,方法中创建一个学生对象,我们现在通过案例来看看 打开逃逸分析和关闭逃逸分析后,堆内存对象创建的数量对比。

然后,我们分别设置 VM 参数:Xmx1000m -Xms1000m -XX:-DoEscapeAnalysis - XX:+PrintGC 以及 -Xmx1000m -Xms1000m -XX:+DoEscapeAnalysis -XX:+PrintGC, 通过之前讲过的 VisualVM 工具,查看堆中创建的对象数量。 然而,运行结果却没有达到我们想要的优化效果,也许你怀疑是 JDK 版本的问题,然而我 分别在 1.6~1.8 版本都测试过了,效果还是一样的: (-server -Xmx1000m -Xms1000m -XX:-DoEscapeAnalysis -XX:+PrintGC)

(-server -Xmx1000m -Xms1000m -XX:+DoEscapeAnalysis -XX:+PrintGC)

 这其实是因为由于 HotSpot 虚拟机目前的实现导致栈上分配实现比较复杂,可以说,在 HotSpot 中暂时没有实现这项优化。随着即时编译器的发展与逃逸分析技术的逐渐成熟, 相信不久的将来 HotSpot 也会实现这项优化功能。

锁消除

在非线程安全的情况下,尽量不要使用线程安全容器,比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 关键字修饰,会使用到锁,从而导致性 能下降。 但实际上,在以下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这 是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读 写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。

标量替换

逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的 时候可能不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象 的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量 替换。 我们用以下代码验证:

逃逸分析后,代码会被优化为:

我们可以通过设置 JVM 参数来开关逃逸分析,还可以单独开关同步消除和标量替换,在 JDK1.8 中 JVM 是默认开启这些操作的。

-XX:+DoEscapeAnalysis 开启逃逸分析(jdk1.8 默认开启,其它版本未测试)

-XX:-DoEscapeAnalysis 关闭逃逸分析

-XX:+EliminateLocks 开启锁消除(jdk1.8 默认开启,其它版本未测试)

-XX:-EliminateLocks 关闭锁消除

-XX:+EliminateAllocations 开启标量替换(jdk1.8 默认开启,其它版本未测试)

-XX:-EliminateAllocations 关闭就可以了

总结

今天我们主要了解了 JKD1.8 以及之前的类的编译和加载过程,Java 源程序是通过 Javac 编译器编译成 .class 文件,其中文件中包含的代码格式我们称之为 Java 字节码 (bytecode)。 这种代码格式无法直接运行,但可以被不同平台 JVM 中的 Interpreter 解释执行。由于 Interpreter 的效率低下,JVM 中的 JIT 会在运行时有选择性地将运行次数较多的方法编译 成二进制代码,直接运行在底层硬件上。 在 Java8 之前,HotSpot 集成了两个 JIT,用 C1 和 C2 来完成 JVM 中的即时编译。虽然 JIT 优化了代码,但收集监控信息会消耗运行时的性能,且编译过程会占用程序的运行时 间。 到了 Java9,AOT 编译器被引入。和 JIT 不同,AOT 是在程序运行前进行的静态编译,这 样就可以避免运行时的编译消耗和内存消耗,且 .class 文件通过 AOT 编译器是可以编译成 .so 的二进制文件的。 到了 Java10,一个新的 JIT 编译器 Graal 被引入。Graal 是一个以 Java 为主要编程语言、 面向 Java bytecode 的编译器。与用 C++ 实现的 C1 和 C2 相比,它的模块化更加明显, 也更容易维护。Graal 既可以作为动态编译器,在运行时编译热点方法;也可以作为静态编 译器,实现 AOT 编译。

本文标签: 沉思性能JVM