我写的太垃圾,不写了,直接看这位大佬的了nyimac.gitee.io/2020/07/03/…
1、介绍
1.1什么是JVM
- 定义:
Java Virtual Machine(java 虚拟机)-java运行环境(java 二进制字节码的运行环境),java代码通过javac 编译成class字节码,这个class字节码使用java程序加载到java虚拟机,就可以运行了 - 好处:
- 一次编译,到处运行,跨平台(关键点就是JVM,正是JVM屏蔽了字节码和底层操作系统的差异,对外提供了一致的运行环境,JVM就是用解释的方法来执行二进制文件,来达到跨平台)
- 自动内存管理机制,垃圾回收功能
- 数组下标越界检查(下标越界会抛出异常,如果没有这个功能,就可能覆盖其他代码的内存,很严重)
- 多态(JVM内部是用虚方法调用的机制实现多态)
- 比较:
JVM JRE JDK
1.2学习JVM有什么用
- 面试
- 理解底层实现的原理
- 中高级程序员必备技能
1.3常见的JVM
此处参照学习HotSpot,其他的实现都大差不差
1.4学习路线
三大块:
- ClassLoader类加载
- 内存结构
- 执行引擎
一个类从java源代码编译成class二进制后,必须经过ClassLoader(类加载器),才能被加载到JVM里去运行,类都放在Method Area(方法区),类将来创建的实例对象都在Heap(堆中),而Heap(堆)里面的对象在调用方法时就会用到 JVM Stacks(虚拟机栈) PC Register(程序计数器) Native Method Stacks(本地方法栈)
方法执行时每行代码都有执行引擎中的解释器逐行进行执行,方法里的热点代码(被频繁调用的代码)会有一个JIT Compiler(即时编译器)它对热点代码要做一个编译(可以理解为一个优化后的执行)
GC(垃圾回收)它会对Heap(堆)里面不会在引用的对象,进行垃圾回收
本地方法接口,会有一些java代码不方便实现的功能,必须调用底层操作系统的功能,所以会和操作系统底层打交道,要记住本地方法接口来调用操作系统提供的功能方法
2、内存结构
2.1程序计数器
-
定义
Program Counter Register 程序计数器(寄存器) -
作用
程序计数器的作用就是记住下一条命令的执行地址,如0开始执行,经过解释器,解释成机器码,在交给CPU运行,然后解释器再去拿第二条命令,然后重复这个过程,总之程序计数器就是记住下条JVM命令的地址,如果没有这个计数器,它就不知道接下来执行哪条命令,这是程序计数器的基本作用- 为啥程序计数器是通过一个叫寄存器来实现的
程序计数器是java对物理硬件的屏蔽和抽象,物理上是通过寄存器实现的,寄存器可以说是整个CPU组件里面读取速度最快的一个单元,因为读取指令地址这个工作是非常频繁的,所以java虚拟机在设计的时候就把cpu的寄存器当作程序计数器,用它来存地址,将来读取地址
- 为啥程序计数器是通过一个叫寄存器来实现的
-
特点
- 不会存在内存溢出(程序计数器在Java虚拟机规范中唯一一个不会存在存在溢出的区)
- 是线程私有的,随着线程创建而创建,随着线程销毁而销毁 (java是支持多线程运行的,多个线程运行的时候cpu会有一个调度器组件,给线程分配时间片,比如给线程分配一个时间片,它在时间片内没有执行完,那就会把线程一的状态执行一个暂存,切换到线程二去,等线程二代码执行到一定程度,线程二的时间片用完了,在切换到线程一,继续执行线程一剩余的代码,就是时间片的概念,线程切换的过程中,需要记住下一条执行到哪里了,就需要用到程序计数器了,比如线程一执行到第九行代码,然后时间片用完,cpu切换到线程二执行,那这个时候程序计数器就会把下一条命令记录到程序计数器里面,而且这个程序计数器和线程是私有的,它是属于线程一的,相当于一个线程,一个程序计数器,因为各自执行的代码指定地址不一样的,所以每个线程都有自己的程序计数器)
2.2虚拟机栈
这里可以理解为,栈就是AK-47的弹夹,先放进去的子弹,最后发射,后放入发先发射 先进后出,后进先出
虚拟机栈干啥用的
java中每个线程运行的时候需要给每个线程划分空间,虚拟机栈就是线程运行需要的空间,一个线程运行需要一个虚拟机栈,将来多个线程,就会有多个虚拟机栈
每个栈内是由什么组成的
一个栈内可以看成由多个栈帧组成 一个栈帧就对应一个方法的调用,线程最终是为了执行代码,代码是由一个个的方法组成,所以在线程运行的时候,每个方法需要的内存就称之为一个栈帧,栈帧就是每个方法运行时需要的内存
方法运行时需要什么内存
- 参数
- 局部变量
- 返回地址 这些信息都是需要占内存的,所以方法执行时就需要预先把这些内存给分配好
栈和栈帧怎么联系起来的
比如调用一个定义方法时,就给这个方法划分一段栈帧空间,并且把它放到栈内,这个方法执行完了,把对应方法的栈帧出栈,释放这个方法占用的内存
2.2.1定义
Java Virtual Machine Stacks (Java虚拟机栈)
- 每个线程运行时所需要的内存,称之为虚拟机栈
- 每个线程由多个栈帧(Frame)组成,对应着每次方法调用时所占的用的内存
- 每个线程只能有一个活动栈帧(活动栈帧代表正在执行的方法)
2.2.2问题辨析
-
垃圾回收是否涉及栈内存?
不需要,因为栈内存无非就是一次次的方法调用所产生的栈帧内存,而栈帧内存在每一次方法调用结束后,都会被弹出栈,也就是被自动回收掉 -
栈内存的分配越大越好吗?
栈内存可以通过运行代码时,通过一个虚拟机参数来指定,栈内存越大反而会让线程变少(因为物理内存的大小是一定的,比如一个线程它使用的是栈内存,一个线程假设使用了1M内存,假设总共物理内存是500M,理论上可以有500个线程同时运行,但是如果对每个线程的栈内存设置了2M的内存,那么理论上只能同同时运行250个线程,栈内存划分大了只是为了能更多次的递归方法调用,而不是增强运行的效率) -
方法内部的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的,如果是局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全看一个线程变量是不是线程安全,其实就是要看它是多个线程对这个变量是共享的还是变量对每个线程是私有的,比如这个
这是不会影响线程安全的,因为变量X是一个方法内的局部变量,一个线程对应一个栈,线程内每一个方法调用,都会产生一个新的栈
如果变量是static类型,不加安全保护的话,就会产生线程安全问题
m3 结果返回,返回了就意味着其他的线程有可能会拿到这个对象,去并发并行的修改,一会造成线程安全的问题
2.2.3栈内存溢出
- 栈帧过多,导致栈内存溢出(方法递归调用,如果方法没有设置正确的结束条件,就会导致自己调用自己,每次调用都会产生一个栈帧,就会导致栈溢出)
没有正确结束语句递归,栈帧过多,导致栈内存溢出(java.lang.StackOverflowError)
栈内存设置小,这个数就会相对的变小
-Xss来设置栈内存
- 栈帧过大,导致栈内存溢出(这中情况不太容易出现,一个栈默认是1M内存,而一个栈帧里面的局部变量,比如int在4个字节,要好多好多xN)
2.2.4线程运行诊断
案例1:cpu占用过高 \
-
定位
用 top 命令定位哪个进程对cpu的占用过高
ps H -eo pid,tid,%cpu | grep 进程ID (用ps命令进一步定位是哪个线程引起的CPU占用过高)
32655进程下面又分了好多线程(第二列)
用JDK提供的工具看看哪些线程出现了问题
jstack 进程ID
根据上图得知32655进程下面的32665线程占用过高,就可以根据线程编号在java输出的信息找到它,但是这里的线程编号是十进制的,这款java输出的是十六进制的,所以要先把十进制转换为十六进制
案例2:程序运行很长时间没结果
重复上面步骤
2.3本地方法栈
JAVA虚拟机调用一些本地方法时,需要给这些本地方法提供的内存空间
2.4堆
堆空间参数:-Xmx
前面说的程序计数器,虚拟机栈,本地方法栈他们有个共同点就是都是线程私有的,而堆和方法区可以看成是共享的
2.4.1定义
Heap 堆
- 通过new关键字,创建对象都会使用使用堆内存 特点
- 他是线程共享的,堆对象中都需要考虑线程安全的问题
- 有垃圾回收机制
2.4.2堆内存溢出(java.lang.OutOfMemoryError:Java heap space)
堆里有垃圾回收机制,当对象不会被使用了,就可以成为垃圾被回收掉,占用的内存被释放掉,这样怎么可能还会出现堆内存耗尽呢?
- 如果不断的产生对象,而产生的新对象, 仍然有人使用它,这类对象就不会当作垃圾回收,达到一定的数量,就会导致堆内存耗尽,也就是堆内存溢出
内存溢出代码
2.4.3堆内存的诊断
体验工具的快乐jmap
在不同的时间节点查看堆空间的占用,new byte 用的是堆空间,占用10M,创建好对象输出2,休眠30秒,在把array的对象引用设置为null;意味着变量不会在引用刚才的byte数组对象,brte数组对象可以被垃圾回收了,调用gc方法进行一次回收,垃圾回收后打印3,这个时候在检测堆内存的变化情况
然后第一个时间点执行命令的时候,还没有new,堆空间占用了6M,总容易是Capacity 64M
第二个时间点执行的时候,byte数组已经创建出来了,堆空间占用了16M
第三个时间点执行的时候,垃圾回收之后内存占用 1.2M左右
体验工具的快乐jconsole
控制台输入 jconsole
案例
- 垃圾回收后,内存占用仍然很高 可以在控制台输入 jvisualvm jvm可视化工具
2.5方法区
2.5.1定义
方法区是所以java虚拟机线程共享的区域,存储了跟类结构相关的信息,成员变量,方法数据,成员方法以及构造器方法代码部分,运行时常量池....,方法区在虚拟器启动时创建,逻辑上是一个堆的组成部分
2.5.2组成
1.8以后永久代被废弃了, 方法去还是以个概念性的东西,它的实现变成了元空间,但是它不占用堆空间了,换句话书就是它已经不是JVM管理它了,被移除到本地内存,所谓的本地内存就是操作系统内存
2.5.3方法区内存溢出(java.lang.OutOfMemoryError:Metaspace)
这个很难溢出,因为他用的是系统内存,系统内存都是和物理内存有关,所以就要加一个JAVA虚拟机参数
'-XX:MaxMetaspaceSize=8M' 就是最大的元空间大小给设置成8M
2.5.4运行时常量池
常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类型,方法名,参数类型,字面量(字符串,
整数,布尔类型...)等信息
运行常量池,常量池是*.class文件当中的,当该类被加载,它的常量池信息就会放入运行时的常量池,
并把里的符号地址变为真是地址
可以拿到Class文件,利用jdk提供的工具,把clas文件在反编译一下,反编译后就可以勉强读懂
// 二进制字节码文件(基本类信息,常量池,类方法定义,包含了虚拟机指令)
public class JVMDemo2 {
public static void main(String[] args) {
System.out.println("Hi");
}
}
基本信息
常量池
类方法定义
他是常量池有什么关系呢,比如 0:getstatic #2 去常量池找对应的#2如上图,常量池#2 对应#22 #23一路找下去知道要执行什么代码了
2.5.5 StringTable特性
- 常量池的字符串仅是符号,第一次用到时才会变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是SpringBuilder(1.8) .apend
- 字符串常量拼接的原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8将这个字符串尝试放入串池,如果有则并不会放入,如果没有则会放入串池,会把串池中的对象返回
- 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份放入串池,则放入串池
2.6直接内存 此处略过
3.垃圾回收
3.1如果判断对象可以回收
有两种不同的算法,第一种是引用计数就是只要一个对象被其他变量所引用,据让这个对象计数加一,某个变量不在引用就减一,变成0的时候代表没有人引用它,就回收
3.1.1引用计数法
A对象引用B对象,B对象计数是1,反过来B对象同时也引用A对象,A对象计数也是1,但是没有谁在引用他们俩,是不是就代表他俩会被垃圾回收,答案是不行,因为各自的引用计数都是1,虽然这俩个变量都不会被引用了,但是他们的引用的计数不会被归零,导致这两个对象造成不能被垃圾回收,导致了内存上的泄露,java没有采用Python用的是这种
3.1.2可达性分析算法
-
java虚拟机中垃圾回收器采用可达性分析来探索所有存活对象
-
扫描堆中的对象,看是否能够沿着GC Root对象 为起点引用链找到该对象,找不到,表示可以回收
- 可达性分析算法要先确定一系列根对象,根对象就是肯定不能被当成垃圾回收的对象,就称之为跟对象,在垃圾回收之前,首先会对堆内存中的所有对象进行扫描,看每一个对象是不是被刚才提到的根对象做直接或者间接的引用,如果是就不能被回收,反正就会被回收 \
- 举个例子就是 夏天吃葡萄,葡萄洗了以后可以拿着葡萄的根,在葡萄根上的葡萄是不能被回收的对象,因为有根来引用他们,而与根断开,在盘子里的葡萄就会被作为垃圾给回收的对象
-
哪些对象可以作为Gc Root对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
3.1.3四种引用
实线=强引用
- 强引用
- 平时所有的引用都是强引用,比如说new一个对象,这个对象通过等号赋值运算符赋值给了一个变量,那么这个变量就强引用了这个对象,强引用的特点就是,只要沿着Gc Root的引用链可以找到它,那么它就不会被垃圾回收
- 如上图的位置,沿着C对象(根对象)沿着根对象能找到A1对象,A1对象是不能被回收的,B对象也强引用了A1对象,所以A1对象有两个Gc对象都引用了它,它是不能被垃圾回收,垃圾回收的时候它会被保留下来,只有B对象和C对象都不引用A1对象,A1才能被垃圾回收
- 软引用
- 没有被强引用,并且垃圾回收,内存不够,就会回收软件用
- 弱引用 \
- 如上图之要A2A3这两个对象没有被直接的强引用所引用,那么当垃圾回收时,他们都可能被当垃圾回收掉,好比C对象强引用到一个软引用对象,通过软引用对象又引用到了A2,这算是间接的软引用,同时又被一个B对象,强引用引用了,这中情况下垃圾回收,它是不会被回收的,如果B对象不强引用A2对象,只要满足条件就能被垃圾回收(当垃圾回收时,垃圾回收完了还发现内存不够,就会把软引用,所引用的对象释放掉),弱引用是只要垃圾回收,就被会被回收,
4. 虚引用
5. 终结器引用
2.垃圾回收算法
3.分代垃圾回收
4.垃圾回收器
5.垃圾回收调优