JVM
https://www.processon.com/view/link/5eea141cf346fb1ae56a44e7
1.类加载子系统
1.类加载过程
一个类,一个.class文件。
1.加载:
加载成方法区的运行时数据结构。
加载方式:
本地系统,网络,数据库,压缩包,加密文件。最多的场景:动态代理技术。
2.链接
验证、准备、解析
3.初始化
类构造器方法<clinit>只针对类变量和静态代码块中的语句。它与类的构造器<init>是不同的。
虚拟机必须保证一个类的<clinit>方法在多线程下被同步加锁。()
2.类的加载器
1.启动类加载器
2.扩展类加载器
识别ext目录地下的class文件。
3.应用程序加载器(系统类加载器)
4.获取类加载器
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
3.双亲委派机制
接口是由引导类加载器加载的,而接口的具体的实现类是由线程的上下文类加载器加载的。
对类的加载器的引用
2.运行时数据区
jvm支持多线程
1.程序计数器
每个线程都有一份。
任何时间一个线程只有一个方法在执行,也就是当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;native方法则是undefined。
问题:
2.虚拟机栈
出现的背景:跨平台性,不同cpu架构不同,所以不能设计为基于寄存器。
优点:跨平台,指令集小,编译器容易实现。
缺点:性能下降,需要更多的指令。
生命周期:一个线程一个虚拟机栈,生命周期与线程一样。
作用:保存方法的局部变量、部分结果,并参与方法的调用和返回。
* 存储单位:栈帧
栈是运行时单位,堆是存储的单位。
栈帧:一个栈帧是一个方法。
当前正在执行的方法:当前方法,当前栈帧,当前类。
1.局部变量表
保存方法的局部变量。(vs 成员变量)
存储基本数据类型,或者引用数据类型在堆空间中的地址。
slot(变量槽):最基本的存储单元。32位以内的类型(包括returnAddress类型)占用一个槽,64位的类型(long,double)占用两个槽。
各种变量的赋值
按类型:①基本数据类型;②引用数据类型
按位置:
-
成员变量:(在使用前,都经历过默认初始化赋值)
-
类变量:
linking的prepare阶段:默认赋值 --> initial阶段:显示赋值(静态代码块中的赋值)。
-
实例变量:
随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值。
-
-
局部变量:(在使用前必须进行显示赋值!否则,编译不通过)
在java方法执行时,虚拟机使用局部变量表来完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根结点,只要被局部变量表中直接或间接引用的对象都不会被回收。
2.操作数栈(表达式栈)
operand
栈:可以用数组或链表来实现。
在每一个方法刚开始执行的时候,一个空的操作数栈就随之被创建出来。
代码追踪
如果被调用方法有返回值,该返回值会被压入到当前栈帧中的操作数栈中。
3.动态链接(指向运行时常量池的方法引用)
作用:将class文件常量池中的符号引用转换为调用方法的直接引用。
常量池的作用:提供一些符号和常量,便于指令的识别
静态链接和动态链接
动态类型语言和静态类型语言
方法重写的本质
虚方法
4.方法返回地址
存放PC寄存器的值。
3.本地方法栈
本地方法是C语言实现的。
本地方法栈管理本地方法的调用。具体做法是Native Method Stack中登记native方法,在执行引擎执行时加载。
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。甚至可以直接使用本地处理器中的寄存器。直接从本地内存中的堆中分配任意数量的内存。
4.堆
java虚拟机规范规定:堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享java堆,但还可以划分线程私有的缓冲区(TLAB)
“几乎”所有的对象实例都在这里分配内存。
对象提升(promotion)规则
TLAB
/**
* 测试堆空间常用的jvm参数:
* -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
* -XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
* 具体查看某个参数的指令: jps:查看当前运行中的进程
* jinfo -flag SurvivorRatio 进程id
*
* -Xms:初始堆空间内存 (默认为物理内存的1/64)
* -Xmx:最大堆空间内存(默认为物理内存的1/4)
* -Xmn:设置新生代的大小。(初始值及最大值)
* -XX:NewRatio:配置新生代与老年代在堆结构的占比
* -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
* -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
* -XX:+PrintGCDetails:输出详细的GC处理日志
* 打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
* -XX:HandlePromotionFailure:是否设置空间分配担保
*/
逃逸分析、栈上分配、同步省略、标量替换
同步省略:不会体现在字节码中,在加载到内存后才会发生。
标量替换
5.方法区
堆、栈、方法区的交互关系
元空间与永久代的区别
-
最大的区别是,元空间不在虚拟机设置的内存中,而是使用本地内存。更不容易出现oom。
-
存储结构也发生了一定的变化。
存储的内容
方法区用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
class文件中常量池的理解
constant pool table
运行时常量池的理解
runtime constant pool
字节码文件中的常量池加载到方法区后,就称为运行时常量池。
方法区演进细节
jdk8及之后,类型信息、字段、方法、常量保存在本地内存的元空间。但字符串常量池,静态变量仍在堆。
为什么要用元空间替换方法区?
6.对象的内存布局
对象的实例化
创建对象的方式
创建对象的步骤
对象的内存布局
对象访问定位
句柄访问
直接指针(HotSpot访问)
3.直接内存
不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域;
直接内存是在java堆外的,直接向系统申请的内存区间;
来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存;
通常,访问直接内存的速度会优于Java堆,即读写性能高;
也可能导致OOM异常;
缺点:
分配回收成本较高;
不受JVM内存回收管理
4.执行引擎
执行引擎的工作过程
什么是解释器,什么是JIT编译器?
机器语言,指令集,汇编语言
机器语言:0101
指令集:MOV, INC等指令,构成指令集。使用硬件实现对应的指令操作。
汇编语言:使用助记符(Mnemonics)代替指令操作码,用地址符号(Symbo1)或标号(Labe1)代替指令或操作数的地址。
【CPU只认指令码,汇编语言也需要翻译成机器指令码才能被计算机识别和执行】
高级语言:C,C++,java,等
JIT即时编译
5.StringTable
// jdk1.8
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char[] value;
}
// jdk1.9
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
}
字符串拼接操作
new String(“a”) + new String(“b”)创建了几个对象?
- StringBuilder.toString()没有在字符串常量池中存放字面量。
/** 题目:
* new String("ab")会创建几个对象?看字节码,就知道是两个。
* 一个对象是:new关键字在堆空间创建的
* 另一个对象是:字符串常量池中的对象"ab"。 字节码指令:ldc
*
*
* 思考:
* new String("a") + new String("b")呢?
* 对象1:new StringBuilder()
* 对象2: new String("a")
* 对象3: 常量池中的"a"
* 对象4: new String("b")
* 对象5: 常量池中的"b"
*
* 深入剖析: StringBuilder的toString():
* 对象6 :new String("ab")
* 强调一下,toString()的调用,在字符串常量池中,没有生成"ab"
*/
- 在jdk8中,字符串常量池和对象都放在堆中,为了节省空间。当运行s3.intern()时,字符串常量池中直接存放了一个指向堆空间中String(“11”)对象的地址引用。
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
String s2 = "1";
System.out.println(s == s2);//jdk6:false jdk7/8:false
String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")
//执行完上一行代码以后,字符串常量池中,是否存在"11"呢?
// 答案:不存在!!
s3.intern();//在字符串常量池中生成"11"。
// 如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。
// jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址
String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
System.out.println(s3 == s4);//jdk6:false jdk7/8:true
}
}
- 新创建一个字符串时,先在字符串常量池中寻找是否存在该字面量或者该字面量的引用。
public class StringIntern1 {
public static void main(String[] args) {
//StringIntern.java中练习的拓展:
String s3 = new String("1") + new String("1");//new String("11")
//执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
String s4 = "11";//在字符串常量池中生成对象"11"
String s5 = s3.intern();
System.out.println(s3 == s4);//false s3是对象应用,s4是字面量引用
System.out.println(s5 == s4);//true s5,s4都是字面量引用
}
}
例1:
public class StringExer1 {
public static void main(String[] args) {
// String x = "ab";
String s = new String("a") + new String("b");//new String("ab")
//在上一行代码执行完以后,字符串常量池中并没有"ab"
String s2 = s.intern();//jdk6中:在串池中创建一个字符串"ab"
//jdk8中:串池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回
System.out.println(s2 == "ab");//jdk6:true jdk8:true
System.out.println(s == "ab");//jdk6:false jdk8:true
}
}
例2:
public class StringExer2 {
public static void main(String[] args) {
String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成"ab"
// String s1 = new String("a") + new String("b");执行完以后,不会在字符串常量池中会生成"ab"
s1.intern();
String s2 = "ab";
System.out.println(s1 == s2); // jdk8: false;
}
}
总结:jdk8中,intern()方法总会往字符串常量池中放一个东西。字面量or引用,然后返回放的这个东西。
6.垃圾回收
标记阶段与清除阶段。
标记阶段
引用计数算法和可达性分析算法。
- 引用计数算法
优点:实现简单,判定效率高
缺点: 增加计数器存储开销。计数器加减时间开销,无法处理循环引用的问题。
- 可达性分析算法
finalization
三个状态:可触及,可复活,不可触及。
一个对象完成垃圾收集要经过2个标记过程。
GC roots
清除阶段
- 标记-清除算法(Mark-Sweep)
1.标记可达对象;
2.对堆内存中所有对象进行线性遍历,清除没有标记到的对象。
缺点:
效率不高,需要遍历整个堆空间;需要停止用户线程;产生内存碎片,需要维护空闲列表。【清除:将地址空间放在空闲列表中】
- 复制算法(Copying)
1.内存空间分成两份;
2.将存活对象复制到另一半空间中。
优点:
没有标记和清除过程,高效;不产生内存碎片。
缺点:
需要两倍的内存空间,内存利用率低;对于G1这种分成大量region的垃圾回收器,维护对象间的引用关系成本;使用场景在存活对象数量少(死亡对象多)的情况下适用。
- 标记-压缩算法(标记整理算法)(Mark-Compact)
1.标记可达对象。
2.整理内存空间,移动对象,清除不可达的对象。
优点:
不产生内存碎片;只需要维护内存可用空间起始地址,不需要维护空闲列表。
缺点:
效率相对较低。
- 三种算法对比,各有优势。
分代收集算法
针对不同生命周期的对象采用不同的收集方式,以提高回收效率。
新生代:生命周期短,存活率低,回收频繁。
老年代:区域大,对象生命周期长,回收频率不高。
增量收集算法
让垃圾收集线程和应用程序交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
缺点:
由于在垃圾回收过程中,间断性的执行了应用程序代码,所以能减少系统的停顿时间。但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
相关知识点
1.System.gc()
通过System.gc()或者Runtime.getRuntime().gc(),会显式触发Full GC。同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
【提醒jvm的垃圾回收器执行gc,但是不能确保一定会马上执行。可以通过调用System.runFinalization()方法,强制执行】
2.内存溢出与内存泄漏
内存溢出:没有空闲内存,并且垃圾收集器也无法提供更多内存。
内存泄漏:严格来说,对象不会再被程序用到,但是GC又回收不了,最终会导致oom;宽泛意义上,由于大量的不必要的生命周期过长的对象导致的oom,也可以叫memory leak。
3.STW
在GC过程中,产生的用户线程停顿,stw会使所有用户线程被暂停,没有任何响应。
why?
可达性分析算法需要在一个能确保一致性的快照中进行,否则如果出现分析过程中对象引用关系还在不断变化,则分析的准确性无法保证。
4.并行与并发
操作系统层面
并发:一个时间段内,才有多个进程同时运行。【在一个cpu时间片切换】
并行:每个时间点,都有多个进程同时运行。【多个cpu核心,进程互不影响】
垃圾回收器层面
5.安全点与安全区域
GC发生在安全点上,用户线程采用主动式中断到达安全点。
安全区域:指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。离开safe region时,需要检查jvm是否已经完成gc。
6.引用(强,软,弱,虚)
StrongRef:只要强引用关系存在,对象就不会被回收。
SoftRef:将要发生oom之前,将这些对象进行回收。回收后内存仍然不够,报oom。
WeakRef:对象只能存活到下一次垃圾回收时。
PhantomRef:不会会对象生存时间产生影响,也不能获得实例。唯一目的:在对象
垃圾回收器
1.GC评估性能指标
吞吐量和运行时间是矛盾的。
2.CMS
Concurrent-Mark-Sweep,采用标记-清除算法。
低延迟。
第一款真正意义上的并发收集器,实现了用户线程和垃圾回收线程的同时工作。
优点:
由于并发标记和并发清理两个耗时较长的过程都能够与用户线程并发执行,不会stw。所以整个回收过程是低停顿的。
缺点:
当堆内存使用率达到一定阈值的时候就要开始回收,因为在垃圾回收过程中,需要保证用户线程还有足够的使用空间。若无法满足此条件,会出现“Concurrent Mode Failure”失败,这时会启动后备预案,使用Serial Old收集器来进行老年代回收,导致停顿时间很长。
会产生内存碎片。
JDK14已被完全抛弃。
3.G1
Garbage First。全功能收集器。
为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。
官方目标:在延迟可控的情况下获得尽可能高的吞吐量。
名字:后台维护一个优先列表,每次根据收集时间,优先回收价值最大的Region。
JDK9成为默认的垃圾回收器。