JVM 是Java Virtual Machine (Java虚拟机) 的缩写,是一个可以执行Java字节码的虚拟机进程 。是Java实现跨平台的最核心的部分
先编译,后解释执行
Java源文件 --> 编译器 --> class文件(虚拟指令)–> Java虚拟机(JVM)–> 解释为指令执行
ClassFile {
u4 magic; 魔数
u2 minor_version; 副版本号
u2 major_version; 主版本号
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
前4个字节是固定的魔数,用于标识该文件是否为有效的Java字节码文件
编译就是将Java源文件转化为JVM认识的16进制的class文件格式
u2: 代表数据占两个字节
u4: 代表数据占四个字节
首先是加载,将编译好的class文件转为二进制流,将二进制流中类的信息存入方法区中,类对象存入堆中,然后是链接,链接包括三个阶段
第一个阶段是验证,验证加载的类的正确性,是否符合class文件规范,第二个阶段就是准备,在方法区中为静态变量分配空间,并设置初始值,将常量池中的符号引用转为直接引用,接下来是初始化,主要完成静态块执行以及静态变量的赋值
1. 装载
2. 链接
3. 初始化
为类的静态变量设置默认值、执行静态代码块。
类加载器是一个用来加载类文件的类。
在JVM中 不同的类加载器加载不同的类
启动类加载器(Bootstrap classLoader): 主要负责加载JAVA中的一些核心类库,主要是位于
扩展类加载器(Extension classLoader): 主要加载JAVA中的一些扩展类,位于
应用类加载器(System classLoader): 主要用于加载CLASSPATH路径下我们自己写的类,是扩展类加载器的子类。
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则继续向上委托,最终到达顶层的启动类加载器。如果父类加载器可以完成加载任务,就成功返回。如果父类加载器无法完成加载任务,子类加载器才会尝试自己去加载
自定义类加载器类,继承ClassLoader 类,重写loadClass方法
当 tomcat启动时,会创建几种类加载器:
Bootstrap 引导类加载器 加载 JVM启动所需的类,以及标准扩展类(位于 jre/lib/ext 下)
System 系统类加载器 加载 tomcat 启动的类,比如bootstrap.jar,通常在 catalina.bat 或者 catalina.sh 中指定。位于 CATALINA_HOME/bin下。
Common 通用类加载器
线程独享区
只用当前线程能访问的数据区域,线程之间不能共享
线程独享去随着线程的创建而创建,随着线程的销毁而被回收
线程共享区
所有的线程都可以访问
当线程被销毁的时候,共享区的数据不会被立即回收,需要等待达到垃圾回收的阈值之后才会进行回收
1.程序计数器
当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线程切换。这时,每个线程就需要一属于自己的计数器来记录下一条要运行的指令。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。
较小的内存空间、线程私有,记录当前线程所执行的字节码行号。
2.虚拟机栈
存放当前线程中所声明的变量,包括基本数据类型的数据和引用类型的数据的引用。
引用数据类型的引用存放在虚拟机栈中,而对象存放在堆内存中,引用数据类型占用四个字节存放地址
栈帧
每一个线程都会对应一个虚拟机栈,线程中的每个方法都会创建一个栈帧,存放局部变量,操作数栈,常量数据的引用,方法返回值的地址
虚拟机栈溢出异常
栈帧调用的深度太深,会出现虚拟机栈溢出异常StackOverflowError(SOF异常)。一般手动方法的调用是不会出现这个异常的,如果出现了这个异常,99%是由于递归造成的。
可以通过修改虚拟机栈的内存大小设置栈帧的最大深度,指令为:-Xss虚拟机栈内存大小
一般栈帧的深度达到3000~5000即可
太小:虚拟级栈容易溢出
太大:每个线程占据的内存太大,影响线程数量
3.本地方法栈
与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有强制要求,不同虚拟机实现方法不同。
4.堆
线程共享的(所有的线程共享一份). 存放对象的,new 的对象都存储在这个区域.还有就是常量池
5.方法区
在 java8 之后,我们把方法区称之为元空间(MetaSpace),方法区在逻辑上属于堆的一部分,但一些具体机制和堆有所区别,如:一些JVM 的方法区是可以不进行垃圾回收的,关闭 JVM 时才会释放方法区内存。所以方法区还有一个别名叫非堆,目的是和堆分开。方法区会存储类信息、静态变量、常量(JDK8 之后不存放字符串常量)、本地机器指令。
如果加载大量 class 文件,也会造成方法区内存溢出,如一个 tomcat 运行 20~30 个项目
执行引擎是Java虚拟机核心的组成部分之一。JVM将字节码装载到内存中,但是字节码并不能直接运行在操作系统之上。为了执行内存中的字节码文件指令,执行引擎(Execution Engine)就要将字节码指令解释/编译为对应平台上的本地机器指令。
执行引擎的翻译过程为两种:
目前市面上主流的JVM采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器相互协作,取长补短。
解释器每次解释都会将字节码文件解释为机器指令。整体效率较低,但当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。
即时编译器则会将字节码文件编译成机器指令,存在方法区,编译完成后直接执行本地机器指令即可。编译器吧代码编译成本地代码需要一定的执行时间,但编译为本地代码后执行效率高。
当Java虚拟机启动时,解释器首先发挥作用,不必等待及时编译器全部编译完成后再执行。随着时间的推移,编译器把越来越多的代码编译成本地代码,此时运行本地机器指令,获得更高的执行效率。
Java对象在内存中主要有以下几个部分:
对象头
MarkWord:一系列标记位(哈希码、分代年龄、锁标记状态等),在64位系统中占8个字节。
ClassPoint:对象对应的类信息的内存地址,在64位系统中占8字节。
Length:数组对象持有,表示数组长度,占4个字节。
实例数据
包含了对象的所有成员变量,大小由变量类型决定。
类型 | 字节 |
---|---|
byte | 1 字节 |
short | 2 字节 |
int | 4 字节 |
long | 8 字节 |
char | 2~3 字节 |
float | 4 字节 |
double | 8 字节 |
boolean | 1 字节 |
引用类型 | 8 字节 |
对齐填充
将对象大小填充为8字节的整数倍
如果对象只创建不回收,会造成堆内存溢出OutOfMemoryError(OOM)异常。
可以修改堆内存大小的指令
-Xms10M
初始堆内存大小为10M
-Xmx10M
最大堆内存大小为10M
老年代:
对象会优先分配到新生代内存中,每次GC后没有回收的对象年龄加一,年龄到15还没有被回收,对象会存放到老年代内存中;如果对象较大,超过新生代内存的一半,对象也会存放到老年代区域。
新生代
为了减少young去垃圾回收后的空间碎片,新生代有分为Eden区和连个Survivor区,且始终有一个Survivor区保持闲置。对象会先存放到Eden区当中,Eden区空间满了之后会进行young区的垃圾回收,之后将young区所有存活的对象复制到闲置的Survivor区中,并清空Eden区和正在使用的Survivor区。
新生代区域的垃圾回收称之为YoungGC,也叫MinorGC,Eden区满后会触发YoungGC
老年代区域的垃圾回收称之为OldGC,也叫MajorGC,OldGC非常浪费性能,所以我们的JVM调优要尽可能减少OldGC的次数,OldGC往往伴随着YoungGC。YoungGC+OldGC=FullGC
为了减少垃圾回收带来的空间碎片,空间碎片过多会频繁触发YoungGC。
为了减少Survivor区的空间碎片。
引用计数法:
如果操作对象,必须通过引用来进行。如果一个对象没有任何引用与之关联,则说明该对象基本不可能在其他地方被使用到。那么这个对象就成为可被回收的对象了。这种方法实现简单,效率高,但是无法解决循环引用的问题,因此Java中并没有采取这种方式(Python采用的引用计数法)。
可达性分析:
以一个GC Root 对象作为起点进行搜索,如果在GC Roots 和对象之间没有可达路径,则称该对象是不可达的。
GC ROOT 对象:
栈帧中本地变量表引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
标记——清除算法: 效率较低,有空间碎片。Old区使用的算法
复制算法: 空间碎片少,但会浪费空间。存活对象较少才会使用的算法。young区使用的算法。
标记——整理算法: 空间碎片少,效率较低。Old区使用的算法。
分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
垃圾收集器是对垃圾回收算法的实现。
垃圾收集器的执行效率= 吞吐量 / 停顿时间
吞吐量 = 用户代码执行时间 / (用户代码执行事件+停顿时间)
串行收集器: 只有一个垃圾回收线程,在垃圾回收时暂停用户代码线程,如 Serial 和 Serial Old收集器。
并行收集器: 吞吐量优先,多个垃圾收集器线程共同工作, 尽快完成垃圾收集。如 ParNew,Parallel Scanvenge, Parallel Old 收集器
并发收集器: 停顿时间优先,用户线程和垃圾回收线程一同工作,用户代码线程也会完全停止一小段时间,如 CMS,G1 收集器。
CMS(concurrent mark sweep,并发标记扫描)收集器是并发收集器,是基于标记 ——清理的算法进行垃圾回收,用于 OldGC
优点:并发收集、低停顿
缺点:会产生大量空间碎片,停顿时间虽然短但是不可控。
问:CMS 收集器为什么不进行并发的初始标记?
因为初始标记速度很快,不值得多开线程,开线程也是需要耗费资源的
G1(garbage first,垃圾优先)收集器是并发收集器,从 JDK1.7 开始支持,能进行OldGC 和 YoungGC。Old 区采用标记整理算法,Young 区采用复制算法。
G1 收集器没有固定的 Old、Young、Eden、Survivor 区,而是将内存分为一个个大小相等的 Region(格子,1Mb~32Mb)。每次垃圾回收后,Region 的用途可以发生改变,提高了内存的灵活性和利用率。
G1 收集器可以根据开发者设置的参数,停顿时间的期望值,优先筛选回收存活的对象比较少,垃圾对象比较大的区域 Region,可以把更多空余的空间释放出来。
ZGC 从 JDK11 开始支持,目前还是一个实验性版本,原理类似 G1。是目前收集效率最高的垃圾收集器,平均暂停时间为 0.05 毫秒。
优先让服务器自己来选择
-XX:+UseSerialGC
。 -XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseConcMarkSweepGC
、-XX:+UseG1GC
、-XX:+UseZGC
等。