JVM性能调优与实战基础理论篇-上

Posted itxiaoshen博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM性能调优与实战基础理论篇-上相关的知识,希望对你有一定的参考价值。

JVM是一种规范,本篇围绕着JVM基础理论、了解JDK的体系结构、jvm的常见实现、逃逸分析、启动参数分类、类加载等层层分析,以理论和示例相结合方式更容易理解,浓缩JVM核心理论

Java虚拟机

概述

Java官方文档 https://docs.oracle.com/en/java/index.html

JVM是一种规范,通过Oracle Java 官方文档找到JVM的规范查阅。Java虚拟机可以看做虚拟出来一台计算机,主体功能字节码指令集(汇编语言)和内存管理(栈、堆、方法区)等

常见的JVM实现

  • Hotspot:目前使用的最多的 Java 虚拟机。
  • Jrocket:原来属于BEA 公司,曾号称世界上最快的 JVM,后被 Oracle 公司收购,合并于 Hotspot
  • J9: IBM 有自己的 java 虚拟机实现,它的名字叫做 J9. 主要是用在 IBM 产品(IBM WebSphere 和 IBM 的 AIX 平台上)
  • TaobaoVM: 只有一定体量、一定规模的厂商才会开发自己的虚拟机,比如淘宝有自己的 VM,它实际上是 Hotspot 的定制版,专门为淘宝准备的,阿里、天 猫都是用的这款虚拟机。
  • LiquidVM: 它是一个针对硬件的虚拟机,它下面是没有操作系统的(不是 Linux 也不是 windows),下面直接就是硬件,运行效率比较高。
  • zing: 它属于 zual 这家公司,非常牛,是一个商业产品,很贵!它的垃圾回收速度非常快(1 毫秒之内),是业界标杆。它的一个垃圾回收的算法后来被 Hotspot 吸收才有了现在的 ZGC。

体系结构

我们通常所说的JDK,其实是指Java开发包,里面包含Java开发用到的工具集。

  • JDK体系结构
    • Java运行环境(JRE)和开发工具(编译器,调试器,javadoc等)。
      • JRE:由JVM,Java运行时类库,动态链接库等组成;它为Java提供了运行环境,其中重要的一环就是通过JVM将字节码解释成可执行的机器码。
      • JDK的编译器Javac[.exe],会将Java代码编译成字节码(.class文件)。编译出的字节码在任何平台上都一样的内容,所以我们说Java语言是门跨平台语言;Writeonce, run anywhere。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2vmjOtk2-1644747481396)(http://www.itxiaoshen.com:3001/assets/1644552030402KQTxi6TP.png)]****

  • JVM和操作系统关系
    • 引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
    • 在运行时环境,JVM会将Java字节码解释成机器码。机器码和平台相关的(不同硬件环境、不同操作系统,产生的机器码不同),所以JVM在不同平台有不同的实现。目前JDK默认使用的实现是Hotspot VM。

  • JVM架构:Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ut6GY2lN-1644747481400)(http://www.itxiaoshen.com:3001/assets/16445520369994chXCWt4.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2TtOAZ9M-1644747481409)(http://www.itxiaoshen.com:3001/assets/1644641759500Hs1z7mMd.png)]

逃逸分析(Escape Analysis)

概述

  • 逃逸分析的是一个对象的动态作用域,2种情况
    • 方法逃逸:对象通过参数传递传给了另一个方法。
    • 线程逃特性逸:对象有另外的线程访问。
  • 逃逸分析的目的是确认一个对象是否只可能当前线程能访问。
  • 逃逸分析可以带来一定程度上的性能优化。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
  • JIT(Just In Time Compiler)即时编译技术,在即时编译过程中JVM可能会对我们的代码做一些优化如逃逸分析等。

优化策略

逃逸分析的优化策略主要有三个:对象可能分配在栈上、分离对象或标量替换、消除同步锁。

  • 对象可能分配在栈上:JVM通过逃逸分析,分析出新对象的使用范围,就可能将对象在栈上进行分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再将对象分配到堆空间,可以有效地减少 JVM 垃圾回收的压力。
  • 分离对象或标量替换:当JVM通过逃逸分析,确定要将对象分配到栈上时,即时编译可以将对象打散,将对象替换为一个个很小的局部变量,我们将这个打散的过程叫做标量替换。将对象替换为一个个局部变量后,就可以非常方便的在栈上进行分配了。所谓标量就是不能再分割的变量,如Java基本数据类型。
  • 同步锁消除:如果JVM通过逃逸分析,发现一个对象只能从一个线程被访问到,则访问这个对象时,可以不加同步锁。如果程序中使用了synchronized锁,则JVM会将synchronized锁消除。

逃逸分析开关

逃逸分析其实并不是新概念,早在1999年就有论文提出了该技术。但在Java中算是新颖而前言的优化技术,从 JDK1.6才开始引入该技术,JDK1.7开始默认开启逃逸分析,也可通过开关控制

  • -XX:+DoEscapeAnalysis开启逃逸分析
  • -XX:-DoEscapeAnalysis 关闭逃逸分析
  • -XX:+EliminateAllocations开启标量替换
  • -XX:-EliminateAllocations 关闭标量替换
  • -XX:+EliminateLocks开启锁消除
  • -XX:-EliminateLocks 关闭锁消除
  • 开启标量替换或锁消除 必须打开逃逸分析开关

常见面试问题

  • 是不是所有的对象和数组都会在堆内存分配空间?不一定,只要掌握了逃逸分析的原理,那就很清楚知道Java中的对象不一定是在堆上分配的,因为JVM通过逃逸分析,能够分析出一个新对象的使用范围,并以此确定是否要将这个对象分配到堆上。

  • 加了锁的代码锁就一定会生效吗?不一定

JVM运行模式

  • 解释模式(Interpreted Mode):只使用解释器(-Xint 强制JVM使用解释模式),执行一行JVM字节码就编译一行为机器码
  • 编译模式(Compiled Mode):只使用编译器(-Xcomp JVM使用编译模式),先将所有的JVM字节码一次编译为机器码,然后一次性执行所有机器码
  • 混合模式(Mixed Mode):(-Xmixed 设置JVM使用混合模式)依然使用解释模式执行代码,但是对于一些“热点”代码采取编译器模式执行,这些热点代码对应的机器码会被缓存起来,下次执行无需再编译。JVM一般采用混合模式执行代码
JVM运行模式 优点 适用场景
解释模式 启动快 只需要执行部分代码,且大多数代码只会执行一次的情况
编译模式 启动慢,但是后期执行速度快,比较占用内存1 适合代码可能会被反复执行的场景
混合模式 一般JVM所默认的模式

Java启动参数分类

  • 标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
  • 非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;jvm参数调优重点
  • 非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;jvm参数调优重点。可以通过java -XX:+PrintFlagsFinal -version 获取支持参数选项。

类加载

类加载过程

  • 类加载:类加载器将class文件加载到虚拟机的内存

  • 类的7个生命周期:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

    • 加载:在硬盘上查找并通过IO读入字节码文件

    • 连接:执行验证、准备、解析步骤

      • 验证:校验字节码文件的正确性,不是必要的,可以关闭校验提高虚拟机加载类的速度
      • 准备:给类的静态变量分配内存,并赋予默认值
      • 解析:将符号引用替换为直接引用;通过javap -v将字节码文件反汇编为更可读的文件,像类,方法等一切的字面量都转换为符号引用#1...N。包括类解析,字段解析,方法解析,接口解析。即将字节码的静态字面关联(字符串,静态)转换为JVM内存中的动态指针关联(指针,动态)。

    • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

    • 使用:new出对象程序中使用
    • 卸载:执行垃圾回收

类加载器

概述

  • 启动类加载器:C语言开发,加载Java核心类库。基于沙箱机制,只加载java、javax、sun包开头的类。
  • 扩展类加载器:Java语言编写,由sun.misc.Launcher$ExtClassLoader实现,上级加载器为启动类加载器,负责加载jre/lib/ext扩展目录中的jar类包。
  • 应用程序类加载器:Java语言编写,由sun.misc.Launcher$AppClassLoader实现,上级加载器为扩展类加载器,是默认的类加载器。负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
  • 自定义类加载器:负责加载用户自定义路径下的类包
    • 使用场景:字节码二进制流来自于网络,字节码文件不在指定的lib、ext、classpath路径下,需要对二进制流加工后才能得到字节码。
    • 不同的类加载器加载同一个Class字节码文件后,在JVM中产生的类对象是不同的。同一个类加载器,Class实例在JVM中才是全局唯一。
    • 在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

类加载器获取示例

JVM预定义的类加载器为启动类加载器、扩展类加载器、应用程序类加载器。

package cn.itxs.classloader;

import cn.itxs.entity.User;

public class ClassLoaderMain 
    public static void main(String[] args) 
        System.out.println(String.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.DESedeKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(User.class.getClassLoader().getClass().getName());
        System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
    

自定义类加载器示例

除了启动类加载器,所有类加载器都是ClassLoader的子类,所以我们可以通过继承ClassLoader来实现自己的类加载器。

package cn.itxs.entity;

public class Goods 
    public void sayHello()
        System.out.println("hello goods!");
    

生成字节码文件Goods.class后剪切(不是拷贝,删掉类路径下的字节码文件)到D:\\temp\\cn\\itxs\\entity目录下

package cn.itxs.classloader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class ItxsClassLoader extends ClassLoader
    private String classPath;

    public ItxsClassLoader(String classPath) 
        this.classPath = classPath;
    

    private byte[] loadBytes(String name) throws IOException 
        name = name.replaceAll("\\\\.","/");
        FileInputStream fis = new FileInputStream(classPath+"/"+name+".class");
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true)
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            
            by.flip();
            wbc.write(by);
            by.clear();
        
        fis.close();
        return baos.toByteArray();
    

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException 
        try 
            byte[] bytes = loadBytes(name);
            return defineClass(name,bytes,0, bytes.length);
        catch (Exception e) 
            e.printStackTrace();
            throw new ClassNotFoundException();
        
    

    public static void main(String[] args) throws Exception 
        ItxsClassLoader itxsClassLoader = new ItxsClassLoader("D:/temp");
        Class clazz = itxsClassLoader.loadClass("cn.itxs.entity.Goods");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sayHello", null);
        method.invoke(obj,null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    

双亲委派机制

  • 双亲委派机制:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
  • 当进行类加载的时候,虽然用户自定义类不会由Bootstrap ClassLoader或是Extension ClassLoader加载(由类加载器的加载范围决定),但是代码实现还是会一直委托到Bootstrap ClassLoader, 上层无法加载,再由下层是否可以加载,如果都无法加载,就会触发findClass,抛出ClassNotFoundException。
  • 类加载器之间的层级关系并不是以继承的方式存在的,而是以组合的方式处理的。

  • 双亲委派机制是在ClassLoader里的loadClass方法里实现的,如果想自己实现类加载器的话,可以继承ClassLoader后重写findClass方法,加载对应的类。源码实现流程简单如下:
    • 首先判断该类是否已经被加载。
    • 该类未被加载,如果父类不为空,交给父类加载。
    • 如果父类为空,交给Bootstrap Classloader 加载。
    • 如果类还是无法被加载到,则触发findClass,抛出ClassNotFoundException。
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    
        synchronized (getClassLoadingLock(name)) 
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) 
                long t0 = System.nanoTime();
                try 
                    if (parent != null) 
                        c = parent.loadClass(name, false);
                     else 
                        c = findBootstrapClassOrNull(name);
                    
                 catch (ClassNotFoundException e) 
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                

                if (c == null) 
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                
            
            if (resolve) 
                resolveClass(c);
            
            return c;
        
    
  • 双亲委派机制作用

    • 可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
    • 保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,这样可以有效的防止核心Java API被篡改。
  • 打破双亲委派

    • 双亲委派机制也非必然。比如Tomcat web容器里面部署了很多的应用程序,这些应用程序对于第三方类库的依赖版本却不一样,且通常这些第三方类库的路径又是一样的,如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。所以Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。
    • Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,不遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载。

**本人博客网站 **IT小神 www.itxiaoshen.com

以上是关于JVM性能调优与实战基础理论篇-上的主要内容,如果未能解决你的问题,请参考以下文章

JVM性能调优与底层原理分析(学习笔记)

MySQLMySQL参数调优与实战详解(调优篇)(实战篇)(MySQL专栏启动)

MySQL性能调优与架构设计 PDF扫描版 下载

Spark性能调优——扩展篇

Spark性能优化指南——高级篇

spark性能调优指南——高级篇