JVM——Java虚拟机
1. 内存结构
1.1 程序计数器
定义:Program Counter Register程序计数器(寄存器)
作用:记住下一条JVM指令的执行地址
特点:线程私有;不会内存溢出。
1.2 虚拟机栈
Java Virtual Machines Stacks(Java虚拟机栈)
- 栈-每个线程运行需要的内存空间,每个栈由多个栈帧(Frame)组成
- 栈帧-每个方法运行时需要的内存
- 每个线程只能有一个活动栈帧,对应当前正在执行的那个方法
问题辨析:
- 垃圾回收是否涉及栈内存?不需要,方法调用结束就会弹出栈
- 栈内存分配越大越好吗?-Xss size设置栈内存。物理内存一定,栈内存越大,线程数越少。
- 方法内的局部变量是否线程安全?局部变量安全,引用的对象未必。
1.2.1 栈内存溢出StackOverFlow
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
1.2.2 线程运行诊断
cpu占用过多
- 用top定位哪个进程堆cpu占用高
-
ps H -ef pid,tid,%cpu | grep 进程id(用ps命令进一步定位哪个线程引起cpu占用过高)
- jstack 进程id 可以根据线程id找到有问题的线程,进一步定位问题代码的源码行号
程序运行很长时间没有结果
1.3 本地方法栈
Native Method Stacks本地方法栈
1.4 堆
1.4.1 Heap堆
通过new关键字,创建对象都会使用堆内存
特点
- 线程共享,堆中对象都需要考虑线程安全问题
- 有垃圾回收机制
1.4.2 堆内存溢出
1.4.3 堆内存诊断
- jps工具
- 查看当前系统中有哪些java进程
- jmap工具
- 查看堆内存占用情况
- jconsole工具
- 图形界面的,多功能的检测工具,可以连续监测
- jvisualvm工具
1.5 方法区
1.5.1 Meathod Area方法区定义:
- 线程共享
- JVM启动时创建,逻辑上是堆的一部分
- 存储类相关结构,如运行时常量池、成员变量、方法数据、成员&构造方法、特殊方法(类构造器)
1.5.2 方法区内存溢出
1.8以前的永久代内存溢出
演示永久代内存溢出 java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize-8m
1.8以后的元空间内存溢出
演示元空间内存溢出 java.lang.OutOfMemoryError:Metaspace space
-XX:MaxMetaspaceSize-m
1.5.3 运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是*.class文件中的,当类被加载,它的常量池信息就会被放入运行时常量,并把里面的符号地址变为真实地址。
1.5.4 StringTable
// javap -v xxx.class查看字节码
// StringTable["a","b","ab"]
// 常量池中的信息会被加载到运行时常量池中,这时a b ab都是常量池中的符号,还没有编程java字符串对象
// idc #2会把a符号变为"a"对象
// idc #3会把b符号变为"b"对象
// idc #4会把ab符号变为"ab"对象
public static void main(String[] args){
String s1 = "a"; // 懒惰的
String s2 = "b";
// 字符串常量拼接,编译器优化为"ab",入池
String s3 = "ab";
// 变量拼接,运行期间StringBuilder创建,堆中
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString();
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期间确定为ab
String s6 = s4.intern();
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d");
String x1 = "cd";
String x3 = x2.intern();
// 如果调换【最后两行代码】的位置,调换后为true
System.out.println(x1 == x2);// false
System.out.println(x1 == x3);// true
}
特性
- 常量池中字符串仅是符号,第一次用到时才变为对象
- 利用串池机制,来避免重复创建字符串对象
- 字符串变量拼接原理时StringBuilder(1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串放入串池
- 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会放入串池,会把串池中的对象返回
- 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会复制一份放入串池,会把串池中的对象返回
1.5.5 StringTable位置
1.5.6 StringTable垃圾回收
1.5.7 StringTable性能调优
- 调整-XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
1.6 直接内存
Direct Memory
- 常见于NIO操作,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
2. 垃圾回收
2.1 如何判断对象可以回收
2.1.1 引用计数法
2.2.2 可达性分析算法
- Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
- 哪些对象可以作为GC Root?
2.2.3 四种引用
-
强引用
- 只有所有GC Root对象都不通过【强引用】该对象,该对象才能被垃圾回收
-
软引用
- 仅有软引用该对象时,在垃圾回收后,内存仍不足时会再次发出垃圾回收,回收软引用对象
- 可以配合引用队列来释放引用自身
private static final int _4MB = 4*1024*1024; public static void main(String[] args){ List<SoftReference<byte[]>> list = new ArrayList<>(); // 引用队列 ReferenceQueue<byte[]> queue = new ReferenceQueue<>(); for(int i=0;i<5;i++){ // 关联了引用队列,当软引用所关联的byte[]被回收,时,软引用会自己加入引用队列 SoftReference<byte[]> ref = new SoftRerence<>(new byte[_4MB],queue); System.out.println(ref.get()); list.add(ref); System.out.println(list.size()); } Reference<? extends byte[]> poll = queue.poll(); while(poll != null){ list.remove(poll); poll = queue.poll(); } for(SoftReference<byte[] reference:list){ System.out.println(reference.get()); } }
-
弱引用
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
- 可以配合引用队列来释放弱引用本身
-
虚引用
- 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
-
终结器引用
- 无需手动编码,但内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC才能回收被引用对象。
2.2 垃圾回收算法
2.2.1 标记清除Mark Sweep
- 速度快
- 会造成内存碎片
2.2.2 标记整理Mark Compact
- 速度慢
- 没有内存碎片
2.2.3 复制Copy
- 不会有内存碎片
- 可用内存空间减半
2.3 分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的年龄加1,并且交换from to
- minor gc会引发stop the world,暂停其他用户的线程,等待垃圾回收线程结束
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命15次(4bit)
- 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW时间更长。
相关VM参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx或-XX:MaxHeapSize=size |
新生代大小 | -Xmm或(-XX:NewSize=size+-XX:MaxNewSize=size) |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC前MinorGC | -XX:+ScavengeBeforeFullGC |
2.4 垃圾回收器
-
串行
-XX:UseSerialGC=Serial+SeralOld
- 单线程
- 堆内存较小,适合个人电脑
-
吞吐量优先
-XX:+UserParallelGC~-XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy -XX:GCTimeRatio=ratio -XX:MaxGCPauseMills=ms -XX:ParallelGCThreads=n
- 多线程
- 堆内存较大,多核cpu
- 单位时间内STW时间最短
-
响应时间优先
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads -XX:CMSInitiatingOccupancyFraction=percent -XX:+CMSScavengeBeforeRemark
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单次STW时间最短
-
G1
2.5 垃圾回收调优
3. 类加载与字节码计数
3.1 类文件结构
魔数 0-3字节,表示它是否是【class】类型的文件 ca fe ba be
版本 4-7字节,表示版本00 34(52),表示java8
常量池 8-9字节,表示常量池长度,00 35(35)表示常量池有#1~#34项。#0项不计入,也没有值
第#1项0a表示一个Method信息,00 06和00 15(21)表示引用常量池中#6和#21项来获得这个方法的【所属类】和【方法名】
ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
3.2 字节码指令
javap工具反编译class文件
3.2.1 图解方法执行流程
- 原始java代码
- 编译后的字节文件
- 常量池载入运行时常量池
- 方法字节码载入方法区
- main线程开始运行,分配栈帧内存
- 执行引擎开始执行字节码
多态的原理HSDB
3.3 编译期处理
3.4 类加载阶段
- 加载
- 将类的字节码载入方法区,内部采用C++的instanceKlass描述java类,它的重要field有:
- _java_mirror 即java类镜像,例如对String来说,就算String.class,作用是把Klass暴露给java使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
- 将类的字节码载入方法区,内部采用C++的instanceKlass描述java类,它的重要field有:
- 链接
- 验证:验证类是否符合JVM规范,安全性检查
- 准备:为static变量分配空间,准备默认值
- static变量在JDK7之前存储与instanceKlass末尾,JDK7后存储于_java_mirror末尾。
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型以及字符串常量,你们编译阶段值就确定了,赋值在准备阶段完成
- 如果static变量是final的,但属于引用类型,你们赋值也会在初始化阶段完成
- 解析:将常量池中的符号引用解析为直接引用
- 初始化
初始化即()v方法,虚拟机会保证整个类的构造方法的线程安全。类初始化是懒惰的,发生时机: - main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法
- 子类初始化,如果父类还没初始化,会引发父类初始化
- Class.forName()
- new会导致初始化
不会导致类初始化情况: - 访问类的static final静态常量(基本类型和字符串)不会触发初始化
- 类对象.class不会触发初始化
- 拆给年间该类的数组
- 类加载器的loadClass方法
- Class.forName()的参数2为法拉瑟时
3.5 类加载器
名称 | 加载类路径 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Application |
- 启动类加载器
- 扩展类加载器
- 双亲委派机制
所谓双亲委派,就是指调用类加载器的loadClass方法,查找类的规则。优先委派上级类加载器进行加载,如果上级加载器:- 能找到这个类,由上级加载,加载后该类也对夏季加载器可见
- 找不到这个类,则下级类加载器才有资格执行加载
- 线程上下文类加载器
- 自定义加载器
3.6 运行期优化
-
即时编译
- 分层编译
JVM将执行状态分成了5个层次- 0层,解释执行(Interpreter)
- 1层,使用C1即时编译器编译执行(不带profiling)
- 2层,使用C1即时编译器编译执行(带基本的profiling)
- 3层,使用C1即时编译器编译执行(带完全的profiling)
- 4层,使用C2即时编译器编译执行
- profiling是指在运行过程中收集一些程序执行状态的数据,如【方法的调用次数】、【循环的回边次数】等
JIT和解释器的区别: - 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT是将一些字节码编译为针对字节码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT会根据平台类型,生产平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采用解释执行的方式运行。对于小部分发的热点代码,我们可以将其编译成机器码。执行效率Interpreter<C1<C2,目标是发现热点代码(hotspot名称由来)
逃逸分析 ,C2编译器中会发现新建的对象是否逃逸,若无逃逸则优化字节码。使用-XX:-DoEscapeAnalysis关闭逃逸分析。
- 方法内联(Inlining)
如果发现热点方法,并且长度不长时,会进行内联。即把方法内部代码拷贝粘贴到调用者位置。还能进行常量折叠(constant folding)的优化。 - 字段优化
尽可能使用局部变量,而不是成员变量和静态变量。
- 分层编译
-
反射优化
4. java内存模型JMM
JMM(java memory model)定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。
4.1 原子性
4.2 可见性
4.3 有序性
详见并发编程