最近复习一下虚拟机的知识:参考了JVM中的直接引用和符号引用 和 Java 类加载机制(阿里面试题),以及 JVM理解其实并不难!
JVM/JRE/JDK的区别
- JVM:就是 JAVA 虚拟机, 它只识别 .class 类型文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作
- JRE:英文名称( Java Runtime Environment ),Java 运行时环境。它主要包含两个部分:JVM 的标准实现和 Java 的一些基本类库
- JDK:java 开发工具包。JDK 是整个 Java 开发的核心,它集成了 JRE 和一些好用的小工具
这三者的关系:一层层的嵌套关系。JDK > JRE > JVM
JVM的内存模型
- 方法区:用于存储虚拟机加载的类信息,常量,静态变量等数据
- 堆:存放对象实例,所有的对象和数组都要在堆上分配。
- 栈:Java 方法执行的内存模型:存储局部变量表,操作数栈,动态链接,方法出口等信息。生命周期与线程相同
- 本地方法栈:作用与虚拟机栈类似,不同点本地方法栈为 native 方法执行服务
- 程序计数器:当前线程所执行的行号指示器。
垃圾回收算法有几种类型? 他们对应的优缺点又是什么
- 标记-清除算法:会产生大量的碎片,而导致频繁的回收
- 复制算法:需要浪费额外的内存作为复制区,当存活率较高时,复制算法效率会下降
- 标记-整理算法:算法复杂度大,执行步骤较多
- 分代收集算法:根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为新生代( Young Generation 和老年代( Tenured Generation ),永久代( Permanet Generation )。缺点:算法复杂度大,执行步骤较多
类的加载过程
类加载机制:加载、验证、准备、解析、初始化五个阶段
- 第一步:加载
查找并加载类的二进制数据。
加载是类加载过程的第一个阶段,虚拟机在这一阶段需要完成以下三件事情:
通过类的全限定名来获取其定义的二进制字节流
将字节流所代表的静态存储结构转化为方法区的运行时数据结构
在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口
总结:就是将 (磁盘上的class文件)byte数组 变成内存中的Class类型对象
- 第二步:验证
确保被加载的类的正确性。
这一阶段是确保 Class 文件的字节流中包含的信息符合当前虚拟机的规范,并且不会损害虚拟机自身的安全。
包含了四个验证动作:文件格式验证,元数据验证,字节码验证,符号引用验证。
- 第三步:准备
需要注意:静态变量是分配在方法区中的
为类的静态变量分配内存,并将其初始化为默认值。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.
第四步:解析
而解析阶段即是虚拟机将常量池内的符号引用替换为直接引用的过程
关于直接引用和符号引号的概念特地找了一下资料:
符号引用:符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中
在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,
因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的
,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用:
直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。
如果有了直接引用,那引用的目标必定已经被加载入内存中了。
给我的感觉,就是类似 现实中 国家纲领性政策文件 和 各地细节法规。 是一种总体概况 和 具体实现之间 的关系
- 第五步:初始化
为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化
- ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量 在前面的静态语句块可以赋值,但是不能访问
public class Test
{
static
{
i=0;
System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
}
static int i=1;
}
JVM 预定义的类加载器有哪几种?分别什么作用
- 启动类加载器,Bootstrap ClassLoader,加载JACA_HOME\lib,或者被-Xbootclasspath参数限定的类
- 扩展类加载器,Extension ClassLoader,加载\lib\ext,或者被java.ext.dirs系统变量指定的类
- 应用程序类加载器,Application ClassLoader,加载ClassPath中的类库
- 自定义类加载器,通过继承ClassLoader实现,一般是加载我们的自定义类
什么是双亲委派模式?有什么作用?
基本定义: 双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器没有找到所需的类时,子加载器才会尝试去加载该类。
作用
- 通过带有优先级的层级关可以避免类的重复加载;
- 保证 Java 程序安全稳定运行,Java 核心 API 定义类型不会被随意替换。