JVM学习笔记
- 一.学习前言
- 二.类加载子系统
- 三.运行时数据区概述即线程
-
- 1.运行时数据区结构
- 2.数据区与线程的关系
- 3.线程
- 4.后台线程
- 5.程序计数器(PC寄存器)
- 6.虚拟机栈
- 7.本地方法接口
- 8.本地方法栈
- 9.堆
- 10.方法区
- 11.对象的实例化与内存分配
- 12.直接内存
- 四.执行引擎
- 五.StringTable
- 六.垃圾收集器
一.学习前言
1.Java与JVM
Java是跨平台的语言,JVM是跨语言的平台(Scala,Jython,Groovy等)
JVM具有自动内存管理,自动垃圾回收功能
2.JVM整体结构
类装载器子系统将class文件加载到数据区形成一个大的class对象,过程为加载,链接,初始化;方法区和堆是多线程共享的,Java栈,本地方法栈,程序计数器是各个线程独有一份。执行引擎(编译为机器语言)中有解释器,JIT即时编译器(后端编译器)和垃圾回收器,JIT可以缓存代码指令,但缺点是启动的时候会产生卡顿,主流的虚拟机是二者结合起来系统使用。
3.JVM架构总结
由于跨平台性的设计,Java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器的,优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
4.一些主流虚拟机
HotSpot,JRockit(Oracle),J9(IBM),KVM(小型设备),Azul,Liquid(与硬件高度耦合,性能极高),Harmony(Apache,大量在Android SDK中使用),Microsoft JVM(版权问题),Taobao JVM(创新的off-heap技术,将生命周期较长的对象从heap内移到heap外的GCIH中,不受GC管理,降低GC频率,提高效率;GCIH中的对象还能在多个JAVA虚拟机进程中实现共享,硬件上严重依赖intel的CPU),Dalvik VM(非JAVA虚拟机规范,基于寄存器架构,执行dex文件(可以通过class文件转换而来),效率较高,可以直接使用大部分的JAVA API,Android 5.0以前所使用的虚拟机),Graal VM(HotSpot未来最大可能的替代品,Oracle号称可以运行所有语言),本次学习主要以HotSpot作为默认虚拟机
二.类加载子系统
1.内存结构概述
2.类加载器的角色
- class文件存在本地硬盘上,作为一个模板,最终这个模板在执行的时候要加载到JVM中来根据这个模板实例化出n个一摸一样的实例
- class文件加载到JVM中,被称为DNA元数据模板,放在方法区中
- 在class文件 -> JVM -> 最终成为元数据模板的过程中,需要一个运输工具,就是ClassLoader,扮演一个快递员的角色。
3.类加载过程
加载
- 通过一个类的全限定名获取此类的二进制流
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载class文件的方式
- 从本地系统中直接加载
- 通过网络获取,典型场景:web Applet
- 从zip压缩包中读取,成为日后jar,war的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景有:JSP应用
- 从专有数据库中提取class文件,比较少见
- 从加密文件中获取,典型的防class文件被反编译的保护措施
链接
-
验证(Verify)
- 目的在于确保class文件的字节流中包含的信息符合当前虚拟机的需求,保证被加载类的正确性,不会危害虚拟机自身安全,如class文件开头形式为CA FE BA BE(咖啡宝贝)
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
-
准备(Prepare)
- 为类变量分配内存并且设置该类变量的默认初始值,即零值,如:private static int a = 1;在该阶段a被赋予0,在初始化阶段才赋予1.
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化,即直接赋值。
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到堆中。
-
解析(Resolve)
- 将常量池内的符号引用转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄
- 解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等。对应常量池的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info等。
初始化
- 初始化阶段就是执行类构造方法<clinit>()的过程
- 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作的静态代码块中的语句合并而来,如果没有此操作则不会生成
- 构造器方法中指令按语句在源文件中出现的顺序执行。 注意:由于在初始化之前有 p r e p a r e 阶段,会将变量声明并赋默认值,所以在静态代码块中修改变量可以在声明的前面,但是不能在声明的前面使用变量,即非法的前向引用 \color{red}{注意:由于在初始化之前有prepare阶段,会将变量声明并赋默认值,所以在静态代码块中修改变量可以在声明的前面,但是不能在声明的前面使用变量,即非法的前向引用} 注意:由于在初始化之前有prepare阶段,会将变量声明并赋默认值,所以在静态代码块中修改变量可以在声明的前面,但是不能在声明的前面使用变量,即非法的前向引用
- <clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())
- 若该类具有父类,JVM会保证在子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
public class Test{
static class Father{
public static int A = 1;
static{
A = 2;
}
}
static class Son extends Father{
public static int B = A;
}
public static void main(String[] args){
System.out.println(Son.B);
}
}
首先加载Test类(父类Object已加载),执行main方法,加载Son类时先加载其父类Father,在<clinit>()中按照代码流程赋值A为2,在Son的<clinit>()中将B赋值为2
- 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁
public class DeadThreadTest{
public static void main(String[] args){
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread dead = new DeadThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread t1 = new Thread(r,"线程1");
Thread t1 = new Thread(r,"线程2");
t1.start();
t2.start();
}
}
class DeadThread(){
static{
if(true){
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while(true)
}
}
}
运行结果只有一个线程进行初始化该类,当该线程无法完成初始化之后另一个线程也会被阻塞
- 任何一个类声明之后,内部至少存在一个类的构造器(<init>(),即如果不显示的声明一个构造函数,JVM会默认生成一个无参的构造函数)
4.类加载器分类
- JVM支持两种类型的类加载器&