JVM
组成结构:
类文件结构
魔术:指定文件的类型,方便JVM识别是.class文件
版本号:指定版本
常量池:多个相同的字符串只保留一份,节省空间
类加载过程
加载:从本地磁盘、运行时通过动态代理等技术获取字节码信息。在方法区和 堆(包括类的静态变量) 上创建类的信息。
连接(验证、准备、解析)
-
验证:验证字节码信息是否符合规范,比如:文件是否以
0xCAFEBABE
开头 -
准备:为静态变量(static)分配内存并设置初始值。
- 在准备阶段会为静态变量分配内存并赋初值,在初始化阶段才会将值修改。
- final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。
-
解析:将常量池中的符号引用替换成指向内存的直接引用。符号引用就是在字节码文件中使用编号来访问常量池中的内容。
初始化:执行静态代码块中的代码,为静态变量赋值。注意:他们的执行顺序按编写的顺序加载。
使用: 执行类中的方法
卸载:同时满足3个条件,就会进行类的卸载
以下几种方式会导致类的初始化:
- 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。因为在准备阶段就确定了值。不用初始化
- 调用
Class.forName(String className)
。 - new一个该类的对象时。
- 执行Main方法的当前类。
类加载器
作用:获取类和接口字节码数据到JVM内存
启动类加载器(Bootstrap): 默认加载Java安装目录/jre/lib
下的类文件,比如rt.jar,tools.jar,resources.jar等。
扩展类加载器(Extension): 默认加载Java安装目录/jre/lib/ext
下的类文件。
应用程序类加载器(Application): 应用程序类加载器会加载classpath
下的类文件,默认加载的是项目中的类以及通过maven引入的第三方jar包中的类。
双亲委派机制: 当一个类加载器去加载某个类的时候,会自底向上查找父类加载器是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下尝试进行加载。
面试题:
- 如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?
- 答:启动类加载器加载,根据双亲委派机制,它的优先级是最高的
- String类能覆盖吗,在自己的项目中去创建一个java.lang.String类,会被加载吗?
- 不会,因为向上寻找的过程中,启动类加载器发现自己加载过,会直接返回。
- 类的双亲委派机制是什么?有什么好处
- 好处:1. 避免恶意代码替换JDK中的核心类库。2. 避免一个类重复地被加载。
打破双亲委派机制
-
自定义类加载器。Tomcat通过这种方式实现应用之间类隔离
- 继承ClassLoader抽象类,重写
loadClass
方法,将双亲委派的那段代码去掉。然后编写从指定位置加载字节码,最后调用defineClass
方法,在方法区和堆区创建对象。
- 继承ClassLoader抽象类,重写
-
线程上下文类加载器。JDBC
两个自定义类加载器加载相同限定名的类,不会冲突吗?
- 不会冲突,在同一个Java虚拟机中,只有
相同类加载器+相同的类限定名
,才会被认为是同一个类。
JVM内存结构(运行时数据区)
线程不共享
-
程序计数器:记录下一条要执行的字节码指令地址。不会发生内存溢出
-
Java虚拟机栈:每个方法执行时,会创建一个栈帧放入到虚拟机栈中。每个线程都有自己的虚拟机栈。会发生内存溢出
- 栈帧包括:
- 局部变量表,局部变量表的作用是在运行过程中存放所有的局部变量
- 操作数栈,操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域
- 帧数据,帧数据主要包含动态链接、方法出口、异常表的引用
- 调整虚拟机栈的大小:
-Xss栈大小
- 栈帧包括:
-
本地方法栈:存储的是native本地方法的栈帧。在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。
线程共享
- 方法区:方法区是存放基础信息的位置。主要包含三部分内容:
- 类的元信息,保存了所有类的基本信息(字段、方法)
- 运行时常量池,保存了字节码文件中的常量池内容
- 字符串常量池,保存在代码中定义的常量字符串内容
- 堆:创建出来的对象都存在于堆上。 会发生内存溢出。三个重要的值:
used
:java虚拟机当前已使用的堆内存。total
:操作系统已经给java虚拟机分配的可用堆内存max
:操作系统可以给java虚拟机分配的最大堆内存- 调整堆的大小:
-Xmx值
(max的最大值)-Xms值
(初始的total)
方法区的实现:
-
JDK7及之前的版本将方法区存放在堆区域中的永久代空间。会发生内存溢出
-
JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,会发生内存溢出。
字符串常量池和运行时常量池有什么关系?
JDK7之前,运行时常量池 包含 字符串常量池。方法区的实现在堆区域中的永久代空间
JDK7,字符串常量池 被拿到了堆中,其他不变。
JDK8及之后,移除了永久代,方法区放到了直接内存中的元空间。字符串常量池还在堆中
字符串的intern方法
- 执行
intern
方法时,如果字符串常量池中已经存在了,则直接返回字符串常量池中的引用。 - 如果不存在,则会把字符串对象的引用放到字符串常量池中,并返回引用。
面试题:静态变量存储在哪里呢?
- JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代。
- JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代。
- 说白了,静态变量一直存储在堆中。
直接内存:
直接内存并不属于Java运行时的内存区域。会发生内存溢出
要创建直接内存上的数据,可以使用ByteBuffer
。
在 JDK 1.4 中引入了 NIO 机制,使用了直接内存,主要为了解决以下两个问题:
- 堆中的对象进行垃圾回收时,会阻塞所有线程,从而影响其他对象的创建和使用。在直接内存中的对象不会受垃圾回收影响。
- IO操作读文件时,需要先把文件读入到直接内存(缓冲区),再把数据复制到Java堆中。影响效率。现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。
垃圾回收
线程不共享的区域(虚拟机栈、程序计数器、本地方法栈)
- 都是伴随着线程的创建而创建,线程的销毁而销毁。
- 方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存
- 线程不共享的区域不需要垃圾回收器负责回收。
垃圾回收的两个区域:方法区、堆内存
方法区的回收(开发中很少出现,场景:热部署):主要回收不再使用的类(即进行类卸载操作),需要同时满足3个条件才可以
- 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
- 加载该类的类加载器已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用。
堆内存的回收
判断对象是否可以回收的2种方式:
- 引用计数法:每个对象都有一个引用计数器,当对象被引用后,引用计数+1,取消引用-1。引用计数为0,则表示该对象可以被回收。缺点:存在循环引用,所以JVM没有使用。
- 可达性分析法:对象被分为两类,根对象(线程对象、静态变量、监视器对象等)和普通对象。
- 可达性分析法就是从根对象出发,如果能跟着引用链到达某个对象,则该对象不可被回收。
常见的引用对象(引用方式)
-
强引用:默认就是强引用,即对象被局部变量、静态变量所引用。强引用的对象不会被回收掉。
-
软引用:
SoftReference类
实现。当一个对象只被软引用对象指向,并且内存空间不足时,进行垃圾回收,则会回收被软引用指向的对象。可以把软引用对象本身放到引用队列中,回收软引用对象。 -
弱引用:
WeakReference类
实现。不管内存空间够不够,在垃圾回收时,弱引用指向的对象都会被回收。弱引用对象本身也可以使用引用队列回收。 -
虚引用:
PhantomReference类
实现。作用:告诉直接内存,当前指向直接内存的对象不再使用,回收直接内存空间吧。 -
终结器引用:进行两次垃圾回收,第二次会把对象回收,不建议使用。
垃圾回收算法:
不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程(STW)
评价垃圾回收算法的指标:
- 吞吐量:吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)
- 最大暂停时间:在垃圾回收过程中的STW时间最大值
- 堆使用效率:
1、标记清除算法
根据可达性分析算法,将所有存活的对象进行标记
之后将所有没被标记的对象回收。
缺点:会产生大量的垃圾碎片
2、复制算法
将堆内存分为两个区域,from、to。
在创建对象的时候,只使用from区域。也就是将对象创建在from中。
进行垃圾回收时,将from区域所有存活的对象放到to中。
然后清空from区域,再将from区域和to区域互相换个名字。
缺点:堆内存空间利用低
3、标记整理算法
根据可达性分析算法,将所有存活的对象进行标记
之后将存活对象移动到堆的一端。然后清理掉他们的内存空间。
缺点:没有垃圾碎片,但整理的效率低
4、分代垃圾回收算法
将整个内存区域划分为新生代和老年代,他们会进行不同的垃圾回收算法
新生代又分为:伊甸园
、幸存区from
、幸存区to
创建的对象会被放到伊甸园,当伊甸园满了的时候,会触发 MInor GC。
MInor GC会把伊甸园和幸存区from存活的对象放到幸存区to中。
然后清除伊甸园和幸存区from中的对象。
之后幸存区from和幸存区to互换名字。
每次发生MInor GC时,都会记录存活对象的年龄。如果到达15,则会把对象放到老年代
如果老年代满了,首先会进行Minor GC,如果新生代还是无法放下,则会进行Full GC。
如果Full GC之后,老年代还放不下,则会爆出OOM
面试题:分代GC算法为什么将堆分成新生代和老年代?
- 可以通过调整新生代和老年代大小的比例,来适应不同的应用程序。
- 新生代和老年代可以使用不同的回收算法,更灵活。
垃圾回收器:
垃圾回收器是垃圾回收算法的具体实现。
单线程的垃圾回收器:
- Serial 回收新生代、采用复制算法
- SerialOld 回收老年代、采用标记-整理算法
- 缺点:单核CPU优异,多核CPU吞吐量不如其他垃圾回收器。
多线程的垃圾回收器:
- ParNew 回收新生代、采用复制算法
- CMS(Concurrent Mark Sweep) 回收老年代、采用标记-清除算法
- 会产生内存碎片
G1垃圾回收器
- JDK 9之后,默认的垃圾回收器
- 回收年轻代、老年代 采用复制算法
JIT即时编译器
参考:https://lisxpq12rl7.feishu.cn/wiki/ZaKnwhhhmiDu9ekUnRNcv2iNnof