一文带你深入了解JVM性能调优,深入浅出地为你剖析,满满的干货

前言

JVM咱们之前有简单介绍,就是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

今天,我们就来介绍一下JVM的性能优化,希望大家能够喜欢。对vm-性能优化不太熟悉,掌握不透彻的小伙伴可以借此学习一下。对vm-性能优化很熟悉,掌握很好的小伙伴可以当做复习巩固,如果有什么地方不准确,还请大家多多批评指正啊~

一文带你深入了解JVM性能调优,深入浅出地为你剖析,满是干货

一、常见JVM配置参数(引子)

Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。

Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。

Xss 是指设定每个线程的堆栈大小。这个就要依据你的程序,看一个线程大约需要占用多少内存,可能会有多少线程同时运行等。

以上三个参数的设置都是默认以Byte为单位的,也可以在数字后面添加[k/K]或者[m/M]来表示KB或者MB。而且,超过机器本身的内存大小也是不可以的,否则就等着机器变慢而不是程序变慢了。

-Xms 为jvm启动时分配的内存,比如-Xms200m,表示分配200M
-Xmx 为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存
-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

Total Memory -Xms -Xmx -Xss Spare Memory JDK Thread Count
102M 256M 256M 256K 768M 1.4 3072
1024M 256M 256M 256K 768M 1.5 768

上面的表格只是大致的估计了下在特定内存条件下可以在java中创建的最大线程数。随着-Xmx的加大,空闲的内存数就更少,那么可以创建的线程也就更少,同时在JDK1.4和1.5版本不同下,可创建的线程数也会根据每个线程的内存大小不同而不同。

类加载机制深度剖析

1、类加载过程

多个java文件经过编译打包生成可运行jar包,最终由java命令运行某个主类的main函数启动程序,这里首先需要通过类加载器把主类加载到JVM。

主类在运行过程中如果使用到其它类,会逐步加载这些类。

注意,jar包里的类不是一次性全部加载的,是使用到时才加载。

类加载到使用整个过程有如下几步:

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口验证:校验字节码文件的正确性准备:给类的静态变量分配内存,并赋予默认值解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下节课会讲到动态链接初始化:对类的静态变量初始化为指定的值,执行静态代码块

一文带你深入了解JVM性能调优,深入浅出地为你剖析,满是干货

2、类加载器和双亲委派机制

上面的类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器

  • 启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
  • 自定义加载器:负责加载用户自定义路径下的类包看一个类加载器

看一个类加载器示例:

public class TestJDKClassLoader {
    public static void main(String[] args){
        System.out.println(String.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
        System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
    }
}

运行结果

null //启动类加载器是C++语言实现,所以打印不出来
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader

自定义一个类加载器示例:

自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,大体逻辑

  • 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  • 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
  • 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

还有一个方法是findClass,默认实现是抛出异常,所以我们自定义类加载器主要是重写findClass方法。运行结果:

com.tuling.jvm.MyClassLoaderTest$MyClassLoader

全盘负责委托机制

“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。

双亲委派机制

JVM类加载器是有亲子层级结构的,如下图

一文带你深入了解JVM性能调优,深入浅出地为你剖析,满是干货

这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。

比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托启动类加载器,顶层启动类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。

双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载

为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

看一个类加载实例:

package java.lang;
public class String {
    public static void main(String[] args) {
        System.out.println("**************My String Class**************");
    }
}

运行结果:

错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展
javafx.application.Application

打破双亲委派

以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?

我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  • 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  • 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
  • web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  • web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。JVM内存模型深度剖析

再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行?

答案是不行的。为什么?

第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。

第三个问题和第一个问题一样。

我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

Tomcat自定义加载器详解

一文带你深入了解JVM性能调优,深入浅出地为你剖析,满是干货

tomcat的几个主要类加载器:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

从图中的委派关系中可以看出:

CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。

tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。

我们前面说过,双亲委派机制要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。

很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

JVM内存模型深度剖析

1、JVM整体结构及内存模型

一文带你深入了解JVM性能调优,深入浅出地为你剖析,满是干货

2、JVM内存参数设置

一文带你深入了解JVM性能调优,深入浅出地为你剖析,满是干货

关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N,对于64位JVM来说,元空间的默认初始大小是21MB,默认的元空间的最大值是无限。MaxMetaspaceSize用于设置metaspace区域的最大值。

元空间的动态扩展,默认–XX:MetaspaceSize值为21MB的高水位线。一旦触及则Full GC将被触发并卸载没有用的类(类对应的类加载器不再存活),然后高水位线将会重置。新的高水位线的值取决于GC后释放的元空间。如果释放的空间少,这个高水位线则上升。如果释放空间过多,则高水位线下降。

由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。

  • Jdk1.6及之前: 有永久代, 常量池在方法区
  • Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池在堆
  • Jdk1.8及之后: 无永久代,常量池在元空间

StackOverflowError示例:

// JVM设置  -Xss128k(默认1M)
public class StackOverflowTest {
    static int count = 0;
    static void redo() {
        count++;
        redo();
    }
    public static void main(String[] args) {
        try {
            redo();
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println(count);
        }
    }
}

运行结果:

java.lang.StackOverflowError
at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:12)
at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)

结论:

-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多

JVM内存参数大小该如何设置?

JVM参数大小设置并没有固定标准,需要根据实际项目情况分析,给大家举个例子

日均百万级订单交易系统如何设置JVM参数

一文带你深入了解JVM性能调优,深入浅出地为你剖析,满是干货

一天百万级订单这个绝对是现在顶尖电商公司交易量级,对于这种量级的系统我们该如何设置JVM参数了?

我们可以试着估算下,其实日均百万订单主要也就是集中在当日的几个小时生成的,我们假设是三小时,也就是每秒大概生成100单左右。

这种系统我们一般至少要三四台机器去支撑,假设我们部署了四台机器,也就是每台每秒钟大概处理完成25单左右,往上毛估每秒处理30单吧。

也就是每秒大概有30个订单对象在堆空间的新生代内生成,一个订单对象的大小跟里面的字段多少及类型有关,比如int类型的订单id和用户id等字段,double类型的订单金额等,int类型占用4字节,double类型占用8字节,初略估计下一个订单对象大概1KB左右,也就是说每秒会有30KB的订单对象分配在新生代内。

真实的订单交易系统肯定还有大量的其他业务对象,比如购物车、优惠券、积分、用户信息、物流信息等等,实际每秒分配在新生代内的对象大小应该要再扩大几十倍,我们假设30倍,也就是每秒订单系统会往新生代内分配近1M的对象数据,这些数据一般在订单提交完的操作做完之后基本都会成为垃圾对象。

一文带你深入了解JVM性能调优,深入浅出地为你剖析,满是干货

3、逃逸分析

JVM的运行模式有三种:

解释模式(Interpreted Mode):只使用解释器(-Xint 强制JVM使用解释模式),执行一行JVM字节码就编译一行为机器码

编译模式(Compiled Mode):只使用编译器(-Xcomp JVM使用编译模式),先将所有JVM字节码一次编译为机器码,然后一次性执行所有机器码

混合模式(Mixed Mode):依然使用解释模式执行代码,但是对于一些 "热点" 代码采用编译模式执行,JVM一般采用混合模式执行代码

解释模式启动快,对于只需要执行部分代码,并且大多数代码只会执行一次的情况比较适合;编译模式启动慢,但是后期执行速度快,而且比较占用内存,因为机器码的数量至少是JVM字节码的十倍以上,这种模式适合代码可能会被反复执行的场景;

混合模式是JVM默认采用的执行代码方式,一开始还是解释执行,但是对于少部分 “热点 ”代码会采用编译模式执行,这些热点代码对应的机器码会被缓存起来,下次再执行无需再编译,这就是我们常见的JIT(Just In Time Compiler)即时编译技术。

在即时编译过程中JVM可能会对我们的代码做一些优化,比如对象逃逸分析等。

对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。

JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)。

文章较长,码字不易,建议收藏后慢慢观看哦~

以上就是小编帮大家整理的JVM性能优化的一部分,有不准确的地方,还请大家多多指出批评,共同进步。

喜欢请大家多多点赞评论转发,另外小编帮大家整理了一些JVM调优的学习资料,需要的小伙伴请关注小编,并私信回复【spring】来免费领取吧~~~

猜你喜欢

转载自blog.csdn.net/python6_quanzhan/article/details/106505136