软件性能测试、分析与调优实践之路_读书笔记(六)
第五章 Java应用程序的性能分析和调优
5.1 JVM基础知识
5.1.1 JVM简介
JVM是Java Virtual Machine的英文简称,JVM是Java虚拟机,通过在实际的计算机上仿真模拟各种计算机功能来实现的,Java语言引用了JVM使得Java语言编译的字节码文件跨平台使用,因为不同平台只要安装不同JVM就可以运行,不需要重新编译源文件。JVM的本质是运行在操作系统上的一个程序,一个进程,Java虚拟机启动后就开始执行保存在字节码文件中的指令,其内部组成结果如下图所示。
5.1.2 类加载器
类加载器(Class Loader)负责将编译好的.class字节码文件编译到内存中,使得JVM可以实例化或以其他方式使用加载后的类。 类加载器支持在运行时动态加载,动态加载可以节省内存空间。
- 启动类加载器(BootStrap Class Loader):启动类加载器是最底层的加载器,由C/C++语言实现,非java语言实现,负责加载JDK中的rt.jar文件中的字节码文件,JDK目录下的rt.jar存放java语言自身的核心字节码文件,Java自身的核心字节码文件一般都是由启动类加载器进行加载的。
- 扩展类加载器(Extension Class Loader):负责加载一些扩展功能的jar包到内存中,一般加载/lib/ext目录下或者-Djava.ext.dir指定位置中的字节码文件。
- 系统类加载器 (System Class Loader) :负责将系统类路径java -classpath或-Djava.class.path参数指定的目录下的字节类库加载到内存中,通常程序员开发的程序由系统类加载器进行加载的。
类加载器加载类的过程
- 加载 :将指定的.class字节码文件加载到JVM中
- 连接 :将已经加载到JVM中的二进制字节流的类数据等信息,合并到JVM运行时状态中,连接的过程包括验证、准备和解析
- 验证 :校验.class文件的正确性,确保该文件是符合规范定义的,并且适合当前JVM版本的使用,包括四个步骤 :
- 文件格式校验:校验字节码文件格式是否符合规范,版本号是否正确,并且对应的版本是否是当前JVM可以支持的,常量池中的常量是否有不被支持的类型等。
- 元数据校验 :对字节码描述的信息进行语义分析,以确保其描述的信息符合Java语言规范
- 字节码校验 :通过对字节码文件的数据流和控制流进行分析,验证代码的语义是合法的、符合java语言编程规范的
- 符合引用校验 :符合引用是以一组符号来描述引用的目标,校验符号引用转化成为真正的内存地址是否正确。
- 准备 :为加载到JVM中的类分配内存,同时初始化类中的静态变量的初始值
- 解析 :将符合引用转化为直接引用,一般主要把类的常量池中的符号引用解析为直接引用,符号引用是被引用的对象没有实际加载到内存中,用符号代替;直接引用是被引用的对象已经加载到内存中了,直接指向了真的目标;
- 验证 :校验.class文件的正确性,确保该文件是符合规范定义的,并且适合当前JVM版本的使用,包括四个步骤 :
- 初始化 : 初始化类中的静态变量,并执行类中的static代码块,构造函数等,如果内有构造函数,系统会默认无参构造函数。如果类的构造函数中没有显示的调用父类构造函数,编译器会自动生成一个父类的无参数构造函数。
- 被调用 : 指在运行时被调用
- 卸载 : 指将类从JVM中移除
5.1.3 Java虚拟机栈和原生方法栈
Java虚拟机栈(Java JVM Stack )是Java方法执行的内存模型,是线程私有的,和线程直接相关。每创建一个新的线程,JVM就会为该线程分配一个对应的Java虚拟机栈,各个线程的Java虚拟机栈的内存区域是不能直接被访问的,以保证线程并发运行时线程的安全性。每调用一个方法,Java虚拟机栈就会为每一个方法生成一个栈帧 (Stack Frame),调用方法时压入栈帧,叫入栈, 方法返回时弹出栈帧并抛弃,叫出栈。栈帧中存储方法的局部变量,操作数栈,动态链接,中间运算结果,方法返回值等信息。 每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程,虚拟机的生命周期和线程是一样的,栈帧中的存储的局部变量随着线程运行的结束而结束。虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出*Error栈溢出,不过大多数虚拟机都允许动态扩展虚拟机栈的大小, 所以线程可以一直申请栈,直到内存不足抛出OutOfMemoryError内存溢出。
原生方法栈 (Native Method Stack),又叫做本地方法栈,主要存储了原生方法,即native修饰的方法,是为了JVM调用去调用原生方法和接口的栈区。native关键字 :修饰的方法可以简单理解为非 java语言实现的代码接口,由于java语言无法直接访问操作系统底层信息,需要借助C/C++语言来实现,并且被编译成了DDL,由java调用,不同平台的c/c++的实现也是不一样的,native修饰的方法可以被C语言重写,所以native方法的可移植性不高,主要用于加载文件和动态链接库。
5.1.4 程序计数器
程序计数器是一个记录着当前线程所执行的字节码的行号指示器。
JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。
从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。
首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。
特点 :
- 线程隔离性,每个线程工作时都有属于自己的独立计数器。
- 执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址(参考上一小节的描述)。
- 执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
- 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
- 程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。
5.1.5 方法区与元数据区
JDK 7 中原来是有方法区,也就是常说的永久代区域,存储着Java类信息,常量池,静态变量等,方法区占用的内存区域是线程共享的。由于方法区的大小是启动时就设置好的,有限制大小,虽然可以触发永久代OOM会动态调整,会发生OOM,此外永久代的GC特别难搞,加载类过多时严重影响Full GC的性能,于是Java8内存结构抛弃永久代,使用元数据区,解决这些问题。
Java 8 采用了元数据区和本地内存,常量池,包括字符串常量(JVM独此一份),运行时常量(一个类加载到 JVM 中后对应一个运行时常量) 和静态变量等数据则存放到了Java堆Heap 中 。 元数据区就是保存类的元数据,如方法、字段、类,包的描述信息,这些信息被类加载器加载类的时候写入,可以用于创建文档,跟踪代码的依赖性,执行编译时检查。元数据信息直接存放到JVM管理的本地内存中,本地内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义中的内存区域。 本机内存如果够大就不会出现OOM了,可以通过:-XX:MetaspaceSize
来控制它的初始大小,达到该值就会触发垃圾收集进行类型卸载,如不规定大小,会耗尽全部的机器内存。
每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元数据区。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。
元数据区如何提高性能
- 永久代里面的常量池都移到堆里面,只保存元数据,从而让 Full GC 不再关心方法区
- 元空间使用直接内存,理论上系统内存有多大,元空间就可以有多大,不存在 OOM
- 元空间有单独的元空间虚拟机执行内存分配与垃圾回收
5.1.6 堆区
Java是一门面向对象的程序设计语言,而JVM堆区是真正存储Java对象实例的内存区域,并且是所有线程共享的,所以程序在进行实例化对象等操作时,需要解决同步和线程安全问题。Java 中的堆是 JVM 所管理的最大的一块内存空间,java堆既可以是固定大小的,也可以是可扩展的(通过参数-Xmx
和-Xms
设定),如果堆无法扩展或者无法分配内存时也会报OOM。存储的数据类型如下 :
- 对象实例
- 类初始化生成的对象
- 基本数据类型的数组也是对象实例
- 字符串常量池
- 字符串常量池原本存放于方法区,jdk7开始放置于堆中。
- 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张
string table
- 静态变量
- 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
- 线程分配缓冲区(Thread Local Allocation Buffer)
-
线程私有
,但是不影响java堆的共性 - 增加线程分配缓冲区是为了提升对象分配时的效率
-
新生代区域和老年代区域
Java堆区可以细分为新生代区域和老年代区域,新生代区域还可以分为Eden空间区域、From Survivor区域,To Survivor区域,
本文地址:https://blog.csdn.net/LoveG_G/article/details/111869411