讲JVM优化前我们要先了解一下Java的一些底层
我们讲Java的底层分为:我们的产品应用程序,所运用的框架,再到我们的JavaAPI也就是我们的应用程序编程接口,最后到我们的Java虚拟机JVM
虚拟机也分为系统虚拟机和程序虚拟机
举几个例子 系统虚拟机有VMware和Oracle Vm VirtualBox
程序虚拟机有 JVM
由于我们的计算机CPU只能接受到机器指令,所以我们需要经过多次编译后才能使电脑运行我们的文件
我们要将高级语言(如Java,C++)转换为编译语言再转换为机器指令 最后进入到CPU
我们的JVM就是充当其中编译的跨语言平台 可以说Java不是最强大的语言但是JVM是最强大的平台
我自己总结了六点JVM的特点:①执行Java字节码指令 ②二进制 ③与硬件无交互
④一次编译到处运行 ⑤自动内存管理 ⑥自动垃圾回收功能
接着给大家说明一下JVM的运行方向
我们已经编译好的字节码文件会先进入到类加载器中加载接着会进入到我们重点的运行数据区
运行时数据区中又有 pc计数器、虚拟机栈、本地方法栈、堆和方法区等五个方面 从运行中方法区中出来后的数据将进入到执行引擎和本地库的接口 最后再进入到本地方法库
![](http://47.106.11.228/wp-content/uploads/2023/08/图片-22-1024x710.png)
类加载系统
我们现在来讲一下类加载系统
我们的字节码文件放入到 加载系统中会有一个加载过程也就是加载阶段到链接阶段再到初始化中
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
我们类加载器的中的加载器可以分为引导类、扩展类和系统类 他们的关系是依次向下传递的
引导类加载器:jre运行环境里的lib目录下的rt.jar
扩展类加载器:jre/lib/ext
系统类加载器:实现java.lang包下的抽象类classLoader
我们类加载器中的链接阶段也分为验证、准备和解析
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中
验证阶段就是对文件格式、字节码、符号引用、元数据的验证
准备阶段是为变量分配内存并且设置该类变量默认初始化
解析阶段是对常量池中的符号引用转换为直接引用
类加载器中的初始化阶段就是执行类构造器方法的过程
类加载系统是遵循双亲委派机制的 也就是说 我们的加载会先使用其最高级父类加载器加载
如最高级父类加载器加载不了将依次递归到其子类加载器中 类加载器遵循双亲委派机制的优势就是不会重复加载
运行时数据区
JVM中的运行时数据区是非常重要的一个部分
其中包含了五个重要内容我们来逐个分析一下
方法区、虚拟机栈、本地方法栈、pc计数寄存器、Java堆
其中 方法区和Java堆是线程共享的 pc计数寄存器、本地方法栈、虚拟机栈线程是线程私有的
也可以说 一个线程拥有独立的PC计数寄存器、本地方法栈和虚拟机栈
我们所说的JVM调优就发生在方法区和Java堆中
其中我们的优化Java堆占重要位置可优化95% 而5%集中在方法区中
程序计数寄存器
也可以理解为程序钩子
是用来存储下一条指令的地址的
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令
使用PC寄存器存储字节码指令地址有什么用呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么被设定为私有的?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack) ,早期也叫Java栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(stack Frame) ,对应着一次次的Java方法调用。
栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构
栈帧的作用有存储数据,部分过程结果,处理动态链接,方法返回值和异常分派。
栈帧包括:
函数的返回地址和参数。
临时变量。 包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
栈帧状态值:ebp (帧指针),指向当前的栈帧的底部;esp(栈指针) 始终指向栈帧的顶部;
栈帧从低到上依次是(从高地址到低地址的方向):
参数
返回地址
ebp
局部变量
esp
本地方法接口
简单地讲,一个Native Method是一个Java调用非Java代码的接囗
本地方法现状:
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
本地方法栈
用于管理本地方法的调用,这是和虚拟机栈唯一的区别
堆
Java 中的堆是 JVM 管理的最大的一块内存空间,主要用于存放Java类的实例对象 其被划分为两个不同的区域:新生代 ( Young )和老年代 ( Old ),其中新生代 ( Young ) 又被划分为:Eden、From Survivor和To Survivor三个区域
新生代占比3/1 老生代占比3/2
新生代 ( Young ) 的划分
新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,两个 Survivor 区域分别被命名为 from 和 to
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
默认情况下,Eden : from : to = 8 : 1 : 1
JVM 每次只使用 Eden 和其中的一块 Survivor 区域来为对象服务
无论什么时候,总是有一块 Survivor 区域是空闲着的
新生代实际可用的内存空间为 90% 的新生代空间
方法区
方法区,是用来存放有关 类、方法 信息的一块逻辑上的区域。也就说,人们想规划出一块区域,用来存储类 方法 相关信息。
除此之外,还用来存储常量、静态变量,以及一些代码缓存等数据
方法区(Method Area) 与Java堆一样,是各个线程共享的内存区域。
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
堆和栈的区别?
物理地址:堆对对象的物理地址分配是不连续的,所以他的性能要慢些;栈使用的是数据结构的栈,遵循先进后出的原则,所以分配物理地址是连续的,性能快些
存储的内容:堆中存储的是对象的实例,或数组等信息,关注数据的存储;栈中存放是局部变量表和操作数栈等,关注的是代码的执行
程序可见度:堆对于整个应用程序是可见的,共享的;栈是线程私有的,生命周期跟线程一致
JVM调优目标
使用较小的内存占用来获得较高的吞吐量或者较低的延迟
JVM的调优三个比较重要的指标
内存占用:程序正常运行需要的内存大小。
延迟:由于垃圾收集而引起的程序停顿时间。
吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。
JVM调优工具
①命令行工具:
jps:列出正在运行的虚拟机进程
jstat:显示数据 监控
jinfo:查看修改参数
jmap:获取dump日志查询finalize执行队列
jstack:生成线程快照
jconsole:可视化工具
②arthas
alibaba开源的诊断工具
③MAT工具
内存分析工具
GC
GC(Garbage Collection)是JVM的核心组件,它在JVM中以单独的线程(daemon thread)运行,作用于内存堆区域(Stack Space),扫描那些经过new关键字创建的无用的对象并清除以释放内存,必要时整理内存。
只作用于堆区域吗?
也会扫描方法区(永久代)
垃圾:无任何对象引用的对象
Java中的对象引用:
强引用、弱引用、软引用、虚引用
强引用:
当我们使用new进行创建的对象,就是属于强引用对象,当内存不足时,会抛出
,都不会回收这种对象
软引用:
软引用的对象只有在内存不足时才会被GC回收,内存充足时不会被回收;
弱引用:
弱引用的对象存活的时间很短,当一个对象偶尔使用,又能随时获取到,就可以设置为弱引用,弱引用还可以判断当前对象是否被标记为无用对象,当GC进行扫描回收时,只要对象是弱引用类型,不管内存是否充足都会进行回收。
虚引用:
对象如果是虚引用,任何时候都会被GC回收。他必须结合引用队列来使用,当JVM回收一个对象时,如果发现它还有虚引用,会在回收之前,将虚引用加入到队列中,那么程序就可以判断队列中是否有对象的虚引用,这样就可以跟踪到这个对象什么时候被回收。
常用的垃圾回收算法:
1.标记-清除 算法
是最基础的算法也是最好实现的,容易产生内存碎片
2.copying算法
将内存整理后 再将已使用的内存空间一次清理掉
Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低
3.标记-整理算法
它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存
4.分代收集算法:
分老年代和新生代收集
老年代使用标记-整理算法
新生代使用复制算法
如何判断对象有无死亡:
引用计数器:
为每个对象创建有引用计数器,当对象每被引用一次,则计数器+1,引用解除则—1,当计数器值为0时,GC就可以对该对象进行回收了。但是其有缺点:就是存在循环引用的问题,当两个对象互相引用形成环,计数器值则不为0,则不会会进行回收;当多线程对引用计数器进行增减时,可能导致计数器值不准确,还需要考虑并发问题。
可达性分析算法(GC ROOT):
算法中定义了一些GC ROOT对象,这些对象像树枝一样向外延伸,被引用到的对象就不会被GC回收,没有被引用到的对象就会被GC回收,将不被回收的对象进行标记
ROOT对象:
系统类加载器(bootStrap)加载的类、JVM常量池中引用的对象、JVM本地方法栈中引用的对象、JVM虚拟机栈中引用的对象等,这些对象都是强引用,不会被GC回收
内存泄漏与内存溢出
1.什么是内存泄漏
内存泄漏是指内存中存在不再被使用的对象或变量。申请了内存但是不释放
2.什么是内存溢出(OOM)
当分配的内存不够用时,产生内存溢出
3.内存泄漏的原因**
长生命周期的对象持有短生命周期对象的引用,即短生命周期的对象已经死亡或不再被使用,但因为长生命周期的对象持有短生命周期对象的引用,导致不能被GC回收,产生内存泄漏