JVM学习笔记
1,JDK体系结构图
2,JVM工作流程
如果要运行Math.java文件,首先要使用javac命令编译Math.java文件,然后使用java命令运行Math类,由类装载子系统装载到内存模型中,在由字节码执行引擎执行相应代码
2.1,类加载过程
类装载子系统的作用
加载class文件,class文件在文件开头有特定的文件标识,加载的信息存放于方法区
加载过程
加载:通过一个类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的对象,作为方法区这个类的各种数据的访问入口
验证:
确保class文件的字节流中包含的信息符合当前虚拟机的要求,保证被加载的类的正确性,不会危害虚拟机自身安全
主要有四种验证:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备:为类变量分配内存并且设置该类变量的默认初始值,这里不包含final修饰的static,因为在编译的时候就会分配了,准备阶段会显示初始化,这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会分配到堆中
解析:将常量池内的符号引用转化为直接引用
初始化:执行类构造器方法< clinit >()方法,给静态变量赋值,执行静态代码块
成员变量:使用前,都经历过默认初始化赋值
- 类变量:准备阶段,给类变量默认赋值。初始化阶段,给类变量显式赋值即执行静态代码块
- 实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
局部变量:使用前,必须显式赋值,否则,编译不通过
类加载器分类
- 引导类加载器(C C++)
加载JVM自身需要的类
只能加载包名为java,javax,sun开头的类
- 扩展类加载器
加载相应类库
- 系统类加载器
加载系统类路径下的类库
双亲委派机制
java虚拟机对class文件采用的是按需加载的方式,而且加载某个类的class文件时,java虚拟机采用的是双亲委派模式,即把请求交给父类处理,它是一种任务委派模式
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,并且要委托到顶级父类,如果父类加载器可以完成任务就返回,否则在自己去加载
优势
- 避免类重复加载
- 保护程序安全
类是否相同取决于文件源和类加载器是否相同
主动使用和被动使用:
主动使用:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化一个类的子类
- java虚拟机启动时被标明为启动类的类
其他为被动使用,不会导致类的初始化
2.2,内存模型
虚拟机栈
虚拟机栈:用来存放局部变量,每一个线程运行时都会开辟一个对应的栈空间,每一个方法都会栈空间分配一个栈区(栈帧),栈帧用来存放不同方法的不同局部变量,栈帧使用栈(先进后出)这种数据结构,可以很好的解释代码的执行顺序,当线程结束,栈空间结束,方法调用完毕,栈帧回收
运行javap -c Math.class:反汇编,生成更容易阅读的指令文件,详细可查看JVM指令手册
栈帧:它包括局部变量表,操作数栈,动态链接,方法出口
局部变量表最基本的存储单元是slot(变量曹),变量曹存储编译时就已知的数据类型,并且long和double是占两个曹,使用起始索引访问
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
动态链接:每一个栈帧内部都包含了一个指向运行时常量池中该栈帧所属方法的引用,目的是支持当前方法的代码能够实现动态链接
java文件被编译为字节码文件时,所有的变量和方法引用都作为符号引用保存在常量池中
动态链接作用是将符号引用转换为直接引用
程序计数器
程序计数器:通俗的说是用来存储代码执行行号的,方便虚拟机的代码执行,每一个线程都会有程序计数器
特点:
- 是线程私有的
- 不会存在内存溢出
本地方法栈
本地方法栈:加载本地方法,如果一个线程使用本地方法,就分配一个本地方法栈
本地方法:用native关键字修饰,该方法实现是非java语言实现的
方法区
方法区:存储常量,静态变量,类信息,供所有线程使用(java1.8之前实现方式是永久代,之后是元空间)
对于每个加载的类型,JVM必须在方法区中存储以下类型信息
- 这个类型的完整有效名称
- 这个类型直接父类的完整有效名称
- 这个类型的修饰符
- 这个类型直接接口的一个有序列表
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
JVM必须保存所有的方法的以下信息,同域信息一样包括声明顺序
- 方法名称
- 方法的返回类型
- 方法的参数
- 方法的修饰符
- 方法的字节码,操作数栈,局部变量表及大小
- 异常表
方法区中包含了运行时常量池,让我们走进运行时常量池
首先了解一下常量池:class文件的一个结构,存储字面量,常量等
运行时常量池:是方法区的一部分,是常量池在运行时的一种表现形式,用于维护常量池以及支持动态链接
jdk1.8之后:无永久代。类型信息,字段,方法,常量保存在本地内存的元空间。字符串常量池,静态变量放到堆中
以下是演变过程
为什么这样演变呢?
官网介绍是为了融合JRockit和Hotspot两个虚拟机
个人理解:
- 永久代设置空间大小是很难确定的
- 对永久代调优很困难
StringTable:字符串常量池,为什么要调整它
- 放到方法区回收效率低,放到堆中更好回收
方法区垃圾回收行为
- 只有常量池中的常量没有被任何地方引用,就可以回收
- 对于类,条件苛刻,效果很难令人满意
堆
堆:供所有线程使用
new出来的对象默认进入Eden,并且有GC机制,用于回收堆的存储对象
2.3,对象的实例化内存布局与访问定位
JVM是如何通过栈帧中的对象引用访问到其内部的对象实例
2.4,字节码执行引擎
作用
将字节码指令编译为对应平台的本地机器指令
解释器
将字节码文件中的内容编译为对应平台的本地机器指令执行
当一条字节码指令被解释执行完成后,接着执行下一条指令即可
但效率低下
即时编译器
更高效的执行效率
HotSpot:采用解释器与即时编译器并存的架构,各自取长补短,效率很高
String
直接用双引号声明出来的String对象会直接存储在常量池中,而new存放到堆中
intern()的使用
public class Test {
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);// JDK 8false JDK 6 FALSE
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);// JDK 8true JDK 6 FALSE
}
}
这是一道面试题
垃圾回收算法简单介绍
引用计数算法
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加一,当引用失效时,引用计数器就减一,当引用计数器值为0时,对象A为垃圾对象,可以回收
可达性分析算法
Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
- 哪些对象可以作为 GC Root ?
-
局部变量所引用的对象
-
方法参数所引用的对象
-
方法区中类静态属性所引用的对象
-
方法区中常量引用的对象
-
本地方法栈中引用的对象
当成功区分出内存中存活对象和死亡对象后,GC接下来会进行垃圾回收,释放空间,常见的回收算法
标记-清除算法
是一种非常基础和常见的垃圾收集算法
当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后标记和清除
标记:从引用根节点开始遍历,标记所有被引用的对象
清除:对堆内存从头到尾进行线性的遍历,如果没有标记,则回收
- 缺点
效率不算高
在进行GC的时候,需要停止整个应用程序,导致用户体验差
这种方式清理出来的空间内存是不连续的,产生内存碎片,需要维护一个空闲列表
- 优点
比较基础和常见
复制算法
将活着的内存空间分为两块,每次只使用其中一块,在GC时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的、内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
没有标记和清除过程,实现简单运行高效
不会出现碎片现象
需要两倍的空间,空间浪费比较大
标记-压缩算法
从根节点开始标记所有被引用的对象,将所有对象压缩到内存一端,按顺序排放,清除边界外所有的空间
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
移动过程中,需要停止用户线程
分代收集算法
对于对象的生命周期不同,可以使用分代收集算法
增量收集算法
垃圾收集线程只收集一小片区域的内存空间,接着切换到用户程序线程,依次反复,直到垃圾收集完成
分区算法
将整个堆空间划分成连续的不同小区域,每一个小区间都独立使用,独立回收,可以控制一次回收多少个小区域