前言
JVM调优是每个高级程序员的必修课,在本章中,我会从发展过程以及核心价值来剖析JVM的体系结构。为了让大家更好的理解JVM的工作机制,
我会在讲解完运行时数据区之后,再通过一个类的加载过程到这个类最终在运行时数据区中的存储来更进一步理解JVM的工作原理。最后,通过对内存的回收机制和垃圾回收算法的讲解,引出到JVM的性能调优这一主题,在这个部分会着重讲解垃圾回收算法以及常见的垃圾回收器的区别和使用场景。
JVM内存区域划分
程序计数器(线程私有)
程序计数器(Program Counter Register),也有称作为 PC 寄存器。保存的是程序当 前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当 CPU 需要执 行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得 到的地址获取到指令,在得到指令之后,程序计数器便自动加 1 或者根据转移指针得到下 一条指令的地址,如此循环,直至执行完所有的指令。也就是说是用来指示执行哪条指令的。
java栈
Java 栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局 部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址、额 外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压 栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧 必定位于 Java 栈的顶部。
本地方法栈
本地方法栈与 Java 栈的作用和原理非常相似。区别只不过是 Java 栈是为执行 Java 方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在 JVM 规 范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现 它。在 HotSopt 虚拟机中直接就把本地方法栈和 Java 栈合二为一。
堆
Java 中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在 Java 栈中的), 堆是被所有线程共享的,在 JVM 中只有一个堆。所有对象实例以及数组都要在堆上分配内 存,单随着 JIT 发展,栈上分配,标量替换优化技术,在堆上分配变得不那么到绝对,只能 在 server 模式下才能启用逃逸分析。
方法区
方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变 量、常量以及编译器编译后的代码等。 在 Class 文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池, 用来存储编译期间生成的字面量和符号引用。
直接内存
NIO,使用 native 函数库直接分配堆外内存,不经过 JVM 内存直接访问系统物理内存的类 ——DirectBuffer。 DirectBuffer 类继承自 ByteBuffer,但和普通的 ByteBuffer 不同, 普通的 ByteBuffer 仍在 JVM 堆上分配内存,其最大内存受到最大堆内存的限制;而 DirectBuffer 直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统 限制
JVM执行子系统
Class 类文件结构
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是 构成平台无关性的基石,也是语言无关性的基础。Java 虚拟机不和包括 Java 在内的任何语 言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联,Class 文件中包含了 Java 虚拟机指令集和符号表以及若干其他辅助信息。
Java 跨平台的基础
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是 构成平台无关性的基石,也是语言无关性的基础。Java 虚拟机不和包括 Java 在内的任何语 言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联,Class 文件中包含了 Java 虚拟机指令集和符号表以及若干其他辅助信息。
Class 类的本质
任何一个 Class 文件都对应着唯一一个类或接口的定义信息,但反过来说,Class 文件实际 上它并不一定以磁盘文件的形式存在。 Class 文件是一组以 8 位字节为基础单位的二进制流。
字节码指令
Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码, Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。 由于限制了 Java 虚拟机操作码的长度为一个字节(即 0~255),这意味着指令集的操作 码总数不可能超过 256 条。
大多数的指令都包含了其操作所对应的数据类型信息。例如: iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float 类型的数据。
大部分的指令都没有支持整数类型 byte、char 和 short,甚至没有任何指令支持 boolean 类 型。大多数对于 boolean、byte、short 和 char 类型数据的操作,实际上都是使用相应的 int 类型作为运算类型
类加载机制
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)7 个阶段。其中验证、准备、解析 3 个部分统称为连接(Linking) 于初始化阶段,虚拟机规范则是严格规定了有且只有 5 种情况必须立即对类进行“初始化” (而加载、验证、准备自然需要在此之前开始)
垃圾回收器和内存分配策略
Java 中是值传递还是引用传递?
在运行栈中,基本类型和引用的处理是一样的,都是传 值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的 处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序 解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修 改的是引用对应的对象,而不是引用本身,即:修改的是堆中的数据。所以这个修改是可以 保持的了!
对象,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性 如果还是对象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传 递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个 对象引用),则可以修改这个节点下面的所有内容。
引用类型
对象引用类型分为强引用、软引用、弱引用和虚引用。 强引用:就是我们一般声明对象是时虚拟机生成的引用,强引用环境下,垃圾回收时需要严 格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收
软引用
软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机 会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟 机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟 机在发生 OutOfMemory 时,肯定是没有软引用存在的。
弱引用
弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾 回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。 强引用不用说,我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。 他们一般被作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。因为如果内 存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是 被使用在桌面应用系统的缓存。
基本垃圾回收算法
引用计数(Reference Counting):
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一 个计数。垃圾回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用的 问题。
可达性分析清理
标记-清除(Mark-Sweep):
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被 引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时, 会产生内存碎片
复制(Copying):
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃 圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只 处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理, 不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
标记-整理(Mark-Compact):
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分 两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除标记对 象,并未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标 记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
性能优化
一个 web 应用不是一个孤立的个体,它是一个系统的部分,系统中的每一部分都会影响整 个系统的性能
常用的性能评价/测试指标
响应时间
提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间。 常用操作的响应时间列表:
并发数
同一时刻,对服务器有实际交互的请求数。 和网站在线用户数的关联:1000 个同时在线用户数,可以估计并发数在 5%到 15%之间, 也就是同时并发数在 50~150 之间。
吞吐量
对单位时间内完成的工作量(请求)的量度
关系
系统吞吐量和系统并发数以及响应时间的关系: 理解为高速公路的通行状况: 吞吐量是每天通过收费站的车辆数目(可以换算成收费站收取的高速费), 并发数是高速公路上的正在行驶的车辆数目, 响应时间是车速。 车辆很少时,车速很快。但是收到的高速费也相应较少;随着高速公路上车辆数目的增多, 车速略受影响,但是收到的高速费增加很快; 随着车辆的继续增加,车速变得越来越慢,高速公路越来越堵,收费不增反降; 如果车流量继续增加,超过某个极限后,任务偶然因素都会导致高速全部瘫痪,车走不动, 当然后也收不着,而高速公路成了停车场(资源耗尽)。
常用的性能优化手段
避免过早优化
不应该把大量的时间耗费在小的性能改进上,过早考虑优化是所有噩梦的根源。 所以,我们应该编写清晰,直接,易读和易理解的代码,真正的优化应该留到以后,等到性 能分析表明优化措施有巨大的收益时再进行。
进行系统性能测试
所有的性能调优,都有应该建立在性能测试的基础上,直觉很重要,但是要用数据说话,可 以推测,但是要通过测试求证。
寻找系统瓶颈,分而治之,逐步优化
性能测试后,对整个请求经历的各个环节进行分析,排查出现性能瓶颈的地方,定位问题, 分析影响性能的的主要因素是什么?内存、磁盘 IO、网络、CPU,还是代码问题?架构设 计不足?或者确实是系统资源不足?
小结
由于文章篇幅原因,更多的细节知识点已经写不完了,我全部总结在下面这份【JVM与性能调优知识点】里面了,各位需要的话可以关注我的公众号前程有光免费领取
最后
欢迎关注公众号:前程有光,领取这份【JVM与性能调优知识点】+一线大厂Java面试题总结+各知识点学习思维导+一份300页pdf文档的Java核心知识点总结!