文章目录
1. 什么是JVM
1.1 定义
JVM全称是Java Virtual Machine,即Java虚拟机。本质上是一个运行在计算机上的程序,其职责是运行Java字节码文件。
1.2 作用
为了支持Java中的一次编译,到处运行的跨平台特性。
1.3 功能
-
解释和运行:对字节码文件中的指令,实时的解释成机器码让计算机执行。
-
内存管理:自动为对象、方法等分配内存,自动的垃圾回收机制,回收不再使用的对象。
-
即时编译:对热点代码进行优化,提高执行效率。
1.4 组成
-
类加载子系统:加载字节码信息到内存中
-
运行时数据区:管理JVM使用到的内存,分为两类,线程共享(堆、方法区),线程不共享(程序计数器、本地方法栈/虚拟机栈)
-
执行引擎:解释执行字节码指令,自动垃圾回收
-
本地接口:调用本地已经编译的方法,如虚拟机中提供的C/C++的方法。
1.5 常见JVM
Oracle的Hotspot, GraalVM, 龙井, openJ9等。
2. 字节码文件组成
-
基本信息:包括魔数、字节码文件对应的Java版本号、访问标识、父类和接口。
-
常量池:保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用。
-
字段:当前类或接口声明的字段信息。
-
方法:当前类或接口声明的方法信息字节码指令。
-
属性:类的属性,比如源码的文件名,内部类的列表等。
3. 运行时数据区
直接内存主要是NIO使用,由操作系统直接管理,不属于JVM内存。
3.1 程序计数器PC
每个线程会通过程序计数器记录当前要执行字节码的指令地址,主要有两个作用:
-
可以控制程序指令的进行,实现分支、跳转、异常等逻辑。
-
在多线程执行的情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到哪一句指令并继续解释执行。
3.2 栈
-
Java虚拟机栈
Java虚拟机栈采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈桢来保存,每个线程都会包含一个自己的虚拟机栈,它的生命周期和线程相同。
栈帧主要包含三部分:-
局部变量表:在方法执行过程中存放所有的局部变量。
-
操作数栈:虚拟机在执行指令过程中用来存放临时数据的一块区域。
-
帧数据:主要包含动态链接、方法出口、一场表等内容。
-
-
本地方法栈
Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是本地方法的栈帧。在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈桢,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。
3.3 堆
-
一般Java程序中堆内存是空间最大的一块内存区域,创建出来的对象都存于堆上。
-
栈上的局部变量表中,可以存放堆上对对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。
-
堆是垃圾回收的主要部分。
3.4 方法区
方法区是Java虚拟机规范中提出来的一个虚拟机概念,在Hotspot不同版本中会用永久代或元空间来实现。方法区主要存放的是基本信息,包括:
-
每一个加载的类的元信息(基础信息)
-
运行时常量池,保存了字节码文件中的常量池内容,避免常量内容重复创建减少内存开销。
-
字符串常量池,存储字符串的常量。
4. 哪些区域会出现内存溢出,会有什么现象
内存溢出指的是内存中某一块区域的使用超过了允许使用的最大值,导致使用时内存空间不足失败,虚拟机一般会抛出指定的错误。
在Java虚拟机中,只有程序计数器不会出现内除溢出的情况,因为每个线程的程序计数器只保存一个指令地址,是定长的。
-
堆内存溢出:是指在堆上分配的对象空间超过了堆的最大内存,堆的最大大小使用-Xmx参数进行设置。溢出后会抛出OutOfMemoryError。
-
栈内存溢出:所有栈空间的占用内存超过了最大值,最大值是用-Xss进行设置。溢出后会抛出StackOverflowError。
-
方法区内存溢出:方法区中存放的内容比如类的元信息超过了方法区内存的最大值,JDK7及之前方法去使用永久代(-XX:MaxPermSize=值)来实现,JDK8及之后使用元空间(-XX:MaxMetaspace=值)来实现。溢出都抛出OutOfMemoryError。
-
永久代溢出
-
元空间溢出
-
5. JVM在JDK6-8之间在内存区域上有什么不同
-
方法区实现
-
使用元空间代替永久代的原因
-
字符串常量池的位置
-
字符串常量池从方法区移动到堆的原因
6. 类的生命周期
6.1 加载阶段
-
第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。
-
类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到内存的方法区中,在方法区生成一个InstanceKlass对象,保存类的所有信息。
-
在堆中生成一份与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息。
6.2 连接阶段
-
验证:检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。
主要包含四部分:
-
文件格式验证
-
元信息验证
-
验证程序执行指令的语义
-
符号引用验证
-
-
准备
为静态变量分配内存并设置初值。final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。
-
解析
-
主要是将常量池中的符号引用替换为直接引用。符号引用就是在字节码文件中使用编号来访问常量池中的内容。
-
直接引用不再使用编号,而是使用内存中地址进行访问具体的数据。
-
6.3 初始化阶段
-
执行静态代码块中的代码,并为静态变量赋值。
-
执行字节码文件中clinit部分的字节码指令。
6.4 卸载阶段
判定一个类可以被卸载,需要同时满足三个条件:
-
此类所有实例对象都已经被回收,在堆中不存在任何该类实例对象以及子类对象。
-
加载该类的类加载器已经被回收。
-
该类对应的java.lang.Class对象没有在任何地方被引用。
7. 类加载器
类加载器负责在类加载过程中将字节码信息以流的方式获取并加载到内存中。
-
JDK8及之前类加载器由C++实现启动类加载器,如下:
-
JDK9之后由Java实现,如下
-
常见的类加载器
-
启动类加载器(Bootstrap Classloader):由Hotspot虚拟机提供的,JDK9之前使用C++编写,JDK9之后使用Java编写。
-
扩展类加载器(Extension Classloader):由JDK提供的,使用Java编写的类加载器,JDK9之后由于采用了模块化,改名为平台类加载器(Platform Classloader)。
-
应用类加载器(Application Classloader):JDK提供的,使用Java编写的类加载器,默认加载为应用程序classpath下的类。
-
自定义类加载器:允许用户自行实现类的加载逻辑,可以从网络、数据库等来源加载类的信息,自定义类加载器需要继承自Classloader抽象类,重写findClass方法。
-
8. 双亲委派机制
-
父类加载器:类加载器有层级关系,上一级称之为下一级的父类加载器。
-
定义:双亲委派机制指的是当一个类加载器接收到加载类的任务时,会向上交给父类加载器询问是否加载过,再由上而下进行加载,即向上询问,向下委派。
-
作用:保证类加载时的安全性,避免重复加载。
-
打破双亲委派机制:实现自定义类加载器,重写loadClass方法,将双亲委派机制的代码去除。
9. 如何判断对象有没有被引用
-
常见方法:引用计数器法,可达性分析法。
-
引用计数器:为每一个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。存在循环引用问题所以Java没有采用这种方法。
-
可达性分析法:Java使用可达性分析来判断对象可否被回收。可达性分析将对象分为垃圾回收的根对象(GC Root)和普通对象。
可达性分析算法是指如果从某个对象到GC Root对象是可达的,对象就不可被回收。最常见的是GC Root对象会引用栈上的局部变量和静态变量导致对象的不可回收。
10. 常见的垃圾回收算法
-
标记清除算法
-
算法描述:将所有存活的对象进行标记,Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活的对象进行标记,再从内存中删除没有被标记的对象。
-
优点:实现简单
-
缺点:存在内存碎片,需要检索两遍内存,分配速度慢(由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才今能获取合适的内存空间)。
-
-
标记复制算法
-
算法描述:将内存分成From区和To区,每次再对象分配阶段,只能使用其中一块空间(From空间),在垃圾回收时,将From中存活的对象复制到To区,将两块空间的名字互换。
-
优缺点
-
吞吐量高(算法只需要遍历一遍From区,并将存活的对象复制到To区,但是不如标记清除算法,因为标记清除算法不需要对象的移动)
-
不存在碎片化内存空间
-
内存使用效率低(每次只能让一半的内存空间来创建对象使用)。
-
-
-
标记整理算法(标记压缩算法)
-
算法描述:将所有存活的对象进行标记,遍历出所有存活的对象,将存活的对象移动到堆的一端,清理掉存活对象的内存空间。
-
优缺点
-
内存使用效率高:整个堆内存都可以使用,不会像标记复制算法那样只能使用半个堆内存。
-
不会发生碎片化
-
整理阶段的效率不高
-
-
-
分代垃圾回收算法
分代算法将整个内存区域划分为年轻代和老年代。
-
算法描述
-
分代回收时,创建出来的对象首先会被放入到Eden伊甸园区;
-
随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。
-
Minor GC会把需要的Eden中和From需要回收的对象回收,把没有回收的对象放入到To区。
-
接着,S0会变成To区,S1变成From区,当Eden区满时再往里放入对象,依然会发生Minor GC。
- PS:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC为加1.
-
如果Minor GC后对象的年龄达到阈值,对象就会被晋升至老年代。
-
当老年代中空间不足,无法放入新的对象时,现场时Minor GC,如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。
-
如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出OutOfMemory异常。
-
-
优点
-
可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。
-
新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法效率高、不会产生碎片,老年代可以选择标记清除算法和标记整理算法,由程序员来选择灵活度较高。
-
分代的设计中允许只回收新生代,如果能满足对象分配的要求就不需要对整个堆进行垃圾回收,由垃圾回收引起的停顿时间就会减少。
-
-
11. 垃圾回收器
-
串行垃圾回收器:是一个单线程的垃圾回收器,主要用于小型或者测试应用。它通过暂停应用程序来执行垃圾回收。
-
并行垃圾回收器:也被称为吞吐量垃圾回收器,它与 Serial 垃圾回收器类似,但是会利用多个线程来执行垃圾回收操作,以提高吞吐量。
-
并发垃圾回收器:使用多个线程来尽量减少垃圾回收时的停顿时间,适用于需要更短回收停顿时间的应用。
-
G1垃圾回收器:一种全新的、低停顿时间的垃圾回收器。它将 Java 堆划分为多个大小相等的区域,通过优先回收垃圾最多的区域来实现更短的回收停顿时间。
-
ZGC:一种低停顿时间的垃圾回收器,主要针对大内存和低延迟的应用。它通过使用读屏障和写屏障等技术来实现并发的垃圾回收,从而降低停顿时间。
12. JVM调优方案
1. 调整堆内存大小
通过-Xms和-Xmx参数设置初始堆大小和最大堆大小,根据应用程序的内存需求进行调整。但一般建议两者值设置相同,避免因频繁地进行堆内存缩放而消耗性能。
2. 选择合适的垃圾回收器
3. 调整新生代和老年代的比例
通过-XX:NewRatio参数调整新生代和老生代的比例,根据应用程序的对象生命周期进行优化。
4. 设置垃圾回收相关参数
-
通过 -XX:+UseConcMarkSweepGC、-XX:+UseG1GC 等参数选择不同的垃圾回收器。
-
使用 -XX:+UseParNewGC 开启并行新生代收集器。
5. 调整线程数
通过 -Xss 参数设置线程栈大小,避免线程数过多导致内存消耗过大。
6. 监控和分析GC日志
使用 -Xloggc 参数打印 GC 日志,结合工具如 VisualVM、JConsole 等进行分析。