JVM
2023-04-15 23:11:54 1 举报
AI智能生成
JVM思维导图,不包含GC
作者其他创作
大纲/内容
JVM
概念
解释器:输入的代码 -->[解释器 解释执行] -->执行结果
JIT编译器:输入的代码 -->[编译器 编译] -->编译后的代码 -->[执行] -->执行结果
流程
解释器interpreter
回边计数器 --> 就是计算循环体执行的次数
方法调用计数器
检测热点代码
逃逸分析、锁消除、锁膨胀、方法内联、空值检查消除、类型检测消除、公共子表达式消除
优化代码
概念
同步消除
标量替换
栈上分配
逃逸分析
参数
C1(Client Compiler)
C2(Server Compiler)
替代C2编译器
Graal编译器(JDK10)
分类
编译器JIT compiler
五、执行引擎
①检测new指令的参数是否能在常量池中定位到类的符号引用。
①指针碰撞(堆整齐)
②空闲列表(堆不整齐)
分配方式
对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况
问题描述
1. CAS+失败重试机制保证更新操作的原子性(CAS自旋)
2. 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),每个线程在Java堆中预先分配一小块内存
问题解决
并发环境下的对象创建问题
对象内存分配
值初始化
设置对象头
执行<init>
外框
对象的创建过程
1.MarkWord(8字节)
2.Klasspointer
3.Array Length
存储信息
占用更多内存,容易内存溢出
存储数据的空间相应变少,GC概率增大,频率增加
64位不开指针压缩的问题
在64位的虚拟机中,对象头的标记字段占64位,而类型指针又占64位。一个对象额外占用的字节就是16个字节。以Integer对象为例,它仅有一个int类型的私有字段,占4个字节。因此,每个Integer的额外开销至少400%,这也就是Java为什么要引入基本数据类型的原因之一。为了减少内存开销,64位Java虚拟机引入了压缩指针概念(对应虚拟机选项 -XX:+UseCompressedOops,默认开启),将堆中原本64位的Java对象指针压缩成32位的。这样一来,对象头的类型指针也会被压缩成32位,使得对象头大小从16字节降低为12字节。压缩指针不仅可以作用对象头的类型指针,还可以作用引用类型的字段,引用类型的数组
默认情况下,Java虚拟机中32位的指针可以寻址到2的32次方,也就是32GB的内存空间(超过32位会关闭压缩指针)。在对压缩指针解引用时,我们需要将其左移3位,再加上一个固定的偏移量,便可以寻址到32GB地址空间伪64位指针了。
原理
-XX:-UseCompressedOops
关闭指针压缩
指针压缩
对象头(Header)
对象真正存储的有效信息
实例数据(Instance Data)
对齐填充(Padding)
16字节(markword:8个字节+class pointer:4个字节+对齐补充:4个字节)
一个 Java 空对象Object o = new Object() 在内存占多少字节?
对象在内存的存储结构
2.大对象直接进入(可设置大小判断标准)
在新生代达到一定条件的对象会进入老年代
新生代与老年代的对象存储变换
创建对象
一、创建阶段
①系统至少维护着对象的一个强引用(Strong Reference)
②所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))
特征
强引用(Strong Reference)概念
import java.lang.ref.SoftReference;import java.util.ArrayList;import java.util.List;/** * 对象引用 * VM Args: -Xms10m -Xmx10m 堆内存 */public class ReferenceTest { // 定义List集合 private static List<Object> list = new ArrayList<Object>() // 软引用测试 private static void testSoftReference() { // 循环创建软引用对象 for (int i = 0; i < 10; i++) { byte[] buff = new byte[1024 * 1024]; SoftReference<byte[]> sr = new SoftReference<byte[]>(buff); list.add(sr); } System.gc(); //主动通知垃圾回收 for(int i=0; i < list.size(); i++){ Object obj = ((SoftReference) list.get(i)).get(); System.out.println(obj); } } public static void main(String[] args) { testSoftReference(); }}
软引用(Soft Reference)概念
private static void testWeakReference() { // 循环创建弱引用对象 for (int i = 0; i < 10; i++) { byte[] buff = new byte[1024 * 1024]; WeakReference<byte[]> sr = new WeakReference<byte[]>(buff); list.add(sr); } System.gc(); // 主动通知垃圾回收 for(int i=0; i < list.size(); i++){ Object obj = ((WeakReference) list.get(i)).get(); System.out.println(obj); }}
弱引用(Weak Reference)概念
虚引用(Phantom Reference)概念
强引用<--软引用<--弱引用<--虚引用
引用级别
二、应用阶段
不可见阶段的对象在虚拟机的对象根引用集合中再也找不到直接或者间接的强引用,最常见的就是线程或者函数中的临时变量。程序不在持有对象的强引用
由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象
Class
活着的线程
Thread
Java方法的local变量或参数
Stack Local
JNI方法(即一般说的Native方法)的local变量或参数
JNI Local
全局JNI引用
JNI Global
用于同步的监控对象
Monitor Used
GC(Garbage Collector) roots
三、不可见阶段
指对象不再被任何强引用持有,GC发现该对象已经不可达
弊端
引用计数法
可达性分析(图1246可达)
如何确定一个对象是垃圾?
四、不可达阶段
GC发现对象处于不可达阶段并且GC已经对该对象的内存空间重新分配做好准备,对象进程收集阶段。如果,该对象的finalize()函数被重写,则执行该函数
条件
若对象重新与引用链上的任何一个对象建立关联,即可达,就可以避免被垃圾收集器进行第二次回收时回收(任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行)
finalize()
1.会影响到JVM的对象以及分配回收速度
重写finalize的弊端(被try/catch/finally替代)
public class Finalize { private static Finalize save_hook = null;//类变量 public void isAlive() { System.out.println("我还活着"); } @Override public void finalize() { System.out.println("finalize方法被执行"); Finalize.save_hook = this; } public static void main(String[] args) throws InterruptedException { save_hook = new Finalize();//对象 //对象第一次拯救自己 save_hook = null; System.gc(); //暂停0.5秒等待他 Thread.sleep(500); if (save_hook != null) { save_hook.isAlive(); } else { System.out.println("好了,现在我死了"); } //对象第二次拯救自己 save_hook = null; System.gc(); //暂停0.5秒等待他 Thread.sleep(500); if (save_hook != null) { save_hook.isAlive(); } else { System.out.println("我终于死亡了"); } }}
重写finalize()
五、收集阶段
对象的finalize()函数执行完成后,对象仍处于不可达状态,该对象进程终结阶段
六、终结阶段
GC对该对象占用的内存空间进行回收或者再分配,该对象彻底消失
七、空间重分配阶段
对象的生命周期
从根节点开始,可以搜索到这个对象,即可以访问到这个对象
1. 可触及态(可达状态)
从根节点开始,无论如何都不能访问到这个对象,即这个对象的所有引用都被释放,没有任何变量引用该对象了,但是该对象有可能在finalize()方法中再次被引用,从而复活
2. 可复活态
对象的所有引用都被释放了,并且在对象的finalize()方法中没有复活
3. 不可触及态(不可达状态
对象的三种状态
主要存储在栈内存
局部变量(方法体中声明)
静态变量(修饰符有static):主要存储在堆内区(JDK1.8之后)内存,只有一份
实例变量(修饰符没有static):主要存储在堆内存
成员变量(方法体外声明)
变量
四、对象
在计算机中,CPU指令就是指挥机器工作的指令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是执行指令的过程,也就是计算机的工作过程
操作码表示要完成的操作
操作码
操作数表示参与运算的数据及其所在的单元地址(这个单元地址可以是寄存器、内存等
操作数
通常一条CPU指令包括两方面的内容
1. 计算机原理层面的字节码指令
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成
cafe baby 代表Java文件
0X34代表JDK8版本
.class文件
2.1 概念
编译执行字节码
2.2 JVM作用
ICONST_0 -0000 0011 - Ox03
ALOAD_0 -0010 1010 -Ox2a
POP -0101 0111 - Ox57
2.3 JVM中的助记符
1. 将局部变量加载到操作栈中(ILOAD、ALOAD)
2. 从操作栈顶存储到局部变量表中(ISTORE、ASTORE)
加载的是1~5的数(!CONST与BIPUSH的加载界限)
ICONST
BIPUSH
SIPUSH
LDC
2.4.1 加载或存储指令
2.4.2 指令运算
显式转换两种不同的数值类型 如I2L、D2F等
2.4.3 类型转换指令
NEW、NEWARRAY
创建对象
GETFIELD、PUTFIELD、GETSTATIC
访问属性
INSTANCEOF、CHECKCAST
检查实例类型指令
2.4.4 对象创建与访问指令
POP、POP2
出栈
DUP
复制栈顶元素并且入栈
2.4.5 操作栈管理指令
调用静态方法
invokeStatic
用于调用对象的实例方法,根据对象的实际类型进行分派(多态,final,非静态)
ArrayList<String> list2 = new ArrayList<>();list2.add("a");
invokeVirtual
用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用
List<String> list = new ArrayList<>();list.add("a");
invokeInterface
用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
invokeSpecial
lambda,或者反射
invokeDynamic
INVOKE
返回VOID类型
RETURN
2.4.6 方法调用与返回
Java文件-->词法解析-->语法解析-->语义分析-->生成字节码
词法解析
将词法解析得到的token信息流按照Java语法规则组装成一颗语法树
语法解析
语义分析
Java文件编译成字节码的编译过程
解释执行
JIT编译执行
两者混合
字节码加载
2.4 字节码指令集
2. Java虚拟机中的字节码指令(与CPU中指令类似)
一、字节码指令集(二进制展示)
1.1 加载Load
校验目标类是否合规
验证(Verification)
静态成员变量赋默认值(可能为null或0)
准备(Preparation)
类、方法,属性等符号引用(二进制字节码,例:java.lang.Object)解析为直接引用
解析(Resolution)
1.2 连接(Link)
执行类构造器clinit方法
1.3 初始化(initializing)
1. 类加载的生命周期
全盘负责
限制类在Java虚拟机中的唯一性例,防止恶意代码的加载,保证了JDK代码的安全性
目的
例子
沙箱安全机制
确保所有加载过的Class都将在内存中缓存
缓存机制
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载
定义
流程
打破原因
自定义类加载器:继承classLoader,重写findClass
实现
OSGi(开放服务网关协议,Open Service Gateway Initiative)
1. 一个web容器可能要部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,因此要保证每一个应用程序的类库都是独立、相互隔离的
2. web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离
3. 部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类库被加载进JVM
Tomcat为什么要打破双亲委派
打破双亲委派方式
Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问
CommonLoader
Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
CatalinaLoader
各个Webapp共享的类加载器,加载路径中的class对于所有 Webapp可见,但是对于Tomcat容器不可见;
SharedLoader
各个Webapp私有的类加载器,加载路径中的class只对当前 Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本;
WebappClassLoader
JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例, 并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
JasperLoader
如果是自己规定的文件夹下的文件,就不交给父类加载而是直接自己加载,不是这些类的还是使用双亲委派
系统类的加载
总结
Tomcat的类加载机制
不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用。
不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载它们即可。
Tomcat
自定义类加载器
实现方式
打破双亲委派
双亲委派机制
clazz.getClassLoader()
获取当前类的类加载器
Thread.currentThread().getContextClassLoader
获取当前线程上下文的类加载器
ClassLoader.getSystemClassLoader()
获取系统的类加载器
DriverManager.getCallerLoader()
获取调用者的类加载器
获取类加载器的四种方式
2. 类加载器(ClassLoader)
1.类 加载-->连接-->初始化
2.加载部分拓展 类加载器-->加载器内容-->双亲委派机制
3.连接部分拓展 连接-->验证-->准备(为静态变量分配内存并设置默认初始值)-->解析(将符号引用替换为直接引用)
答题思路
描述一下 JVM 加载 class 文件的原理机制
二、类加载
Eden(伊甸园):占堆8/10空间
From Survivor(幸存者Survivor0):占堆1/10空间
To Survivor(幸存者Survivor1):占堆1/10空间
1. 新生代
接收新生代存不下的对象
参数-XX:+HeapDumpOnOutOfMemoryError
2. 老年代
为什么有TLAB
避免并发环境线程创建修改对象不安全
启动关闭TLAB
-XX: +/-UseTLAB
设置TLAB占Eden空间大小
-XX:TLABWasteTargetPercent
3. TLAB(Thread Local Allocation Buffer)
实现
设计思想
永久代
JDK1.6及之前
堆内存
JDK1.7
字符串常量池的位置
只在字符串常量池
(1)String s = "XXX";
在堆和字符串常量池
(2)String s = new String("XXX");
(3)String s1 = new String("XXX");String s2 = s1.intern();
String s1 = new String("he") + new String("llo");String s2 = s1.intern();System.out.println(s1 == s2);//true
String s = new StringBuffer("计算机").append("软件").toString();System.out.println(s.intern() == s);//trueString s1 = new StringBuffer("ja").append("va").toString();System.out.println(s1.intern() == s1);//false
String s0 = "zhuge";String s1 = "zhuge";String s2 = "zhu" + "ge";System.out.println(s0 == s1);//trueSystem.out.println(so == s2);//true
String s0 = "zhuge";String s1 = new String("zhuge");String s2 = "zhu" + new String("ge");System.out.println(s0 == s1); //falseSystem.out.println(s0 == s2); //falseSystem.out.println(s1 == s2); //false
String s = "a" + "b" + "c";//就等价于String s = "abc";String a = "a";String b = "b";String c = "c";String s1 = a + b + c;System.out.println(s == s1); //false
面试题
常见三种字符串操作
4. 字符串常量池
基本类型的实例变量在堆上创建
class Text{int a=1;}
基本类型的局部变量在栈上创建
public void text(){int b=1;}
基本数据类型放在堆里和栈里的情景
参数
内存分区
1.1 堆内存
方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象
方法区指向堆
针对常量池的回收和类型的卸载
内存回收目的
1.8之前持久代(永久代)
元空间并不在虚拟机中,而是使用本地内存。
与永久代区别
存储被 JVM 加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据
1..8之后元空间
运行时常量池
为什么jdk7之后字符串常量池要放入堆中呢?
字符串常量池
常量池
a-该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
b-加载该类的 ClassLoader 已经被回收。
c-该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
类需要同时满足3个条件才能算是 “无用的类”
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
1.永久代的大小不好设置(类信息放在永久代的话,加载时不知道有多少个类),而且如果类加载过多很容易导致OOM
2.永久代的回收效率低
永久代被代替的原因
1.2 方法区
1. 线程共有
如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的对象
栈指向堆
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回
作用
局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用
作用
基本数据类型
访问对象的速度更快,节省了一次指针定位的时间开销
HotSpot使用的方式
reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的)时只会改变句柄中的实例数据指针,而reference本身不需要被修改
reference
returnAddress
其它
内容
局部变量表
两者汇编指令不一样
i++和++i的区别
操作数栈
图示
动态链接
记录方法结束时的出栈地址(正常执行结束时的返回地址或者由于报错结束时的异常地址)
返回值压入上层调用栈帧
异常信息抛给能够处理的栈帧
PC计数器指向方法调用后的下一条指令
退出三种方式
方法出口
栈帧
栈与栈帧的关系void a(){ b(); }void b(){ c(); }void c(){}
/*** 栈超出最大深度:StackOverflowError* VM args: -Xss128k**/public class StackOverflowTest { // 定义变量,记录栈的深度 private static int stackLength = 1; // 定义方法 public static void stackLeak(){ stackLength++; stackLeak(); } public static void main(String[] args) { try { // 调用方法 stackLeak(); } catch (Throwable e) { System.out.println("当前栈深度:" + stackLength); e.printStackTrace(); } }}
2.1.1 虚拟机栈(JVM Stack)
本地方法栈为虚拟机使用到的Native 方法服务
虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务
与虚拟机栈区别
2.1.2 本地方法栈(Native Stack)
分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器
指向虚拟机字节码指令的位置
唯一一个无OOM的区域
2.1.3 程序计数器
2.1 栈内存
2. 线程私有
保存方法帧和局部变量
栈
对象
堆
无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
线程私有
JVM的所有线程共享
线程共享与否
java.lang.StackOverFlowError
栈空间不足
java.lang.OutOfMemoryError
堆空间不足
异常错误不同
栈的空间大小远远小于堆的
空间大小
Java中堆和栈有什么区别?
(1)栈代表处理逻辑,堆代表存储数据
(2)堆和栈分离,让堆中的数据可以被多个栈共享,提供了数据交互的方式,堆的数据可以被多个栈访问,节约了存储空间
(3)栈只能向上增长的,限制了栈的存储能力,堆可以动态增长
(4)面向对象就是堆和栈的完美结合
为什么要把堆和栈区分出来?
特点
在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据
概念
和堆内存对比
显式调用Sysdtem.gc()强制执行FullGC进行回收
会
直接内存是否会被GC?
直接内存使用场景
优点
缺点
3. 直接内存(本地内存)
三、JVM内存结构(重要)
收藏
收藏
0 条评论
回复 删除
下一页