文章目录
- 1. JDK,JRE,JVM分别是什么,它们之间有什么联系?
- 2. JVM内存区域划分
- 3. JVM类加载过程
- 4. 一个经典面试题
- 5. JVM 双亲委派模型
1. JDK,JRE,JVM分别是什么,它们之间有什么联系?
JDK: 是Java开发工具包,包含了编写,编译,调试和运行Java程序所需要的所有工具和组件。比如编译器Javac,JavaAPI,调试工具等,JDK是针对Java开发人员的,它包含了Jre,还有编译器和其他工具,可以用来编写和调试Java程序
JRE: 是Java运行时环境,包括了Java虚拟机和Java标准类库,用于在计算机中运行Java程序。
JVM: 是Java虚拟机,是Java程序的运行环境,负责将Java代码转化成为可以在计算机上运行的机器码,并且提供必要的条件支持。
2. JVM内存区域划分
JVM内存区域包括:程序计数器,栈区,堆区,方法区。
程序计数器: 内存中最小的区域,保存了下一条要执行的指令地址在哪。这里的指令就是使用JVM加载后的字节码文件,程序要想执行,那么此时就要使用JVM把字节码文件加载起来,放到内存中国,程序就会一条一条的把指令从内从中取出来,放到CPU上执行,那么此时就需要随时记住,当前执行到哪一条了。
我们知道CPU是并发执行程序的,CPU不是只给你一个进程提供服务的,要伺候所有的进程。因为操作系统是以线程为单位进行跳读执行的。每个线程都记录自己的执行位置。
还有就是我们的程序计数器,是在线程中存在的,并且每个线程只有一个程序计数器。
栈区: 用来存储局部变量和方法调用的信息
。方法调用的时候,每次调用一个新的方法,就会设计到一个入栈操作,每次执行完一个方法之后,都涉及到出栈操作。
看如下代码:
void A(){
B();
}
void B(){
C();
}
void C(){
}
如果不停的在栈区中存放方法的信息,那么就会造成栈溢出。那么此时就要联想到我们的递归方法,自己调用自己,那一个方法的信息不停的添加到栈区中,如果我们此时的递归条件,没有写正确,那么就会造成栈溢出。
在JVM中,栈的空间是比较小的,并且在JVM中可以配置栈空间的大小,但是一般也就是几M,或者几十M。这里的栈区其实就和我们在学习数据结构时学的基本一样。
我们要注意的是,这个栈区是在每个线程中存储一份的。
堆区: 一个进程只有一份,那么就是进程中的多个线程,共享我们这里的堆区
这个也是北村中空间占有最大的区域,我们一般写代码时,new出来的对象,都添加到对中,对象的成员变量自然也是在堆中了。
但是这里有这一句话,大家看对不对,内置类型的变量,在栈区上。引用类型的变量在堆区上。其实这个说法是错误的,正确的应该是局部变量,在栈区,成员变量和new出来的对象,在堆上
void func(){
String s = new String()l
}
此时这个操作在执行的时候,s和new String()是两个东西。
因为String 是引用类型,那么此时这里的s是一个引用类型的变量,但是它在一个方法中,方法一执行结束,那么此时这个s就从栈区消失了,那么此时的这个s就是一个局部变量。
这里的new String() 是一个对象的实体,是在堆上储存的
class Test{
String s = new String();
}
new Test();
这里的s是一个引用类型的变量,并且此时s是在Test类中的一个成员变量,那么此时的这个s就就是在堆区中的。
同样我们的new String() 这个对象主体,也是在堆区中的。
方法区: 方法区中,存放的是"类对象"
.class文件会被加载到内存中,也就被JVM构造成类对象(加载的称为"类加载")
这里的类加载,就是放到方法区中,那么类对象就是描述这个类长啥样。好比说,类的名字是什么,里面有哪些成员,有哪些方法,每个成员叫啥名字,是啥类型,public/private,每个方法叫啥名字,是啥类型,public。private,方法里面包含的指令。这里我们有没有想到Java的反射机制呢???😏😏😏
类对象中还有一个很重要的东西,就是静态成员。静态成员是使用static
关键字修饰的成员,也称为类属性
,而我们的普通成员,叫做实例属性
3. JVM类加载过程
类加载:其实就是设计一个运行是环境的一个重要的核心的功能。类加载就是把.class文件,加载到内存中,构建成类对象
。
类加载大致分为三个大步骤:
Loading环节:
先找到对应的.class文件,然后打开并读取.class文件,同时初步的生成一个类对象
在Loading的一个关键环节,.class文件到底是啥样的呢?
.class文件的格式正如上图,其中 u4
就是4个字节的unsigned int u2 就是2个字节的unsigned int ,cp_info/field_info 都是结构体。field_info表示的是类中的信息,method_info就表示的是方法中的信息
我们可以观察这个.class文件的格式,既可以看到 .class文件就把.java文件中的核心信息都表述进去了,只不过组织格式发生了变化。
Linking:
连接一般就是创建好几个实体之间的联系。
Verification: 表示的是校验过程,主要就是验证读到的内容是不是和规范中规定的格式,完全匹配。如果发现这里读到的数据格式不符合规范,就会类加载失败,并且抛出异常。
PreParation: 准备阶段是这个是为类中定义的变量,静态变量,被static修饰的变量,分配内存并设置变量初始化的阶段。
给静态变量分配内存,并且设置0值
Resoulation:表示的是解析。.class文件中,常量是集中放置的,每个常量有一个编号。.class文件的结构体里初始化情况下只是记录了编号。就需要根据编号找到对应的内容,填充到类对象中。
Initializing: 真正对类对象进行初始化,尤其是针对静态成员。
4. 一个经典面试题
大家看看下面的代码片段,判断输入结构是什么?
class A{
public A(){
System.out.println("执行A类中无参的构造方法");
}
{
System.out.println("执行A类中的构造方法块");
}
static{
System.out.println("执行A类中的静态代码块");
}
}
class B extends A{
public B(){
System.out.println("执行B类中无参的构造方法");
}
{
System.out.println("执行B类中的构造方法块");
}
static{
System.out.println("执行B类中的静态代码块");
}
}
public class TestDemo2 extends B {
public static void main(String[] args) {
new TestDemo();
new TestDemo();
}
}
这里有一个大的原则:
- 类加载阶段会进行 静态代码块的执行,要想创建实例,那么势必要先进行类加载
- 这里的静态代码块只是在类加载阶段执行一次
- 构造方法和构造代码块,每次实例化都会被执行,构造代码块在构造方法的前面执行。
- 父类执行在前,子类执行在后。
- 我们这里的main方法开始执行,然后调用TestDemo2的方法,因此要执行main方法,就需要先加载TestDemo2
- TestDemo2类继承自B,要加载TestDemo2,就要先加载B
- B继承自A,要加载B,就要先加载A
所以最后的执行结果就是:
执行A类中的静态代码块
执行B类中的静态代码块
执行A类中的构造方法块
执行A类中无参的构造方法
执行B类中的构造方法块
执行B类中无参的构造方法
执行A类中的构造方法块
执行A类中无参的构造方法
执行B类中的构造方法块
执行B类中无参的构造方法
5. JVM 双亲委派模型
其实这个JVM中的双亲委派模型在面试中是非常喜欢考的,但是这个东西是不重要的,这个东西其实也不是很难理解的,这个东西在我们日常编码的时候没有指导意义。即使它有一个高大上的名字。
其实这里的双亲委派模型就是一个类加载环节
这个环节是处于Loading状态的
双亲委派模型,描述的就是JVM中的一个类加载器,如何根据类的权限名,就如(java.lang.String)找到.class文件的过程。
这个类加载器
是JVM中提供了一个专门的对象,叫做类加载器,负责进行类加载,当然找文件的过程也是有类加载器来负责的。
这里的.class文件,可能放置的位置有很多,有的放到JDK目录中,有的放到项目目录中,还有在其他的特定位置。
默认的类的加载器,主要有3个
- BootStrapClassLoader:负责加载标准库中的类(String,ArrayList,random,Scanner等)
- ExtensionClassLoader: 负责加载JDK扩展的类
- ApplicationClassLoader:负责加载当前项目中的类
其实我们程序员也可以自定义类加载器,来加载其他目录中的类,就比如Tomcat就自定义了类加载器,用来专门加载webapps里面的.class文件
我们的双亲委派模型,就描述这个找目录的过程,也就是上述类加载器是如何配合的
考虑加载我们的java.util.String
- 程序启动,先进入ApplicationclassLoader类加载器
- ApplicationClassLoader就会检查下它的父加载器是否已经加载过了,如果没有,就调用父加载器extensionClassLoader
- ExtensionclassLoader也会检查下,它的父加载器是否加载过了,如果没有,就调用父加载器BootStrapClassloader
- BootStropClassLoader也会检查下,它的父加载器是否加载过了,但是自己没有父加载器,于是就扫描自己负责的目录
- java.lang.String这个类在标准库中内找到,直接由BootStrapClassLoader负责后序的加载过程,查询环节就结束了。
类加载器,加载我们自己写的Test类
首先前面还是一样的
- 程序启动,先进入ApplicationclassLoader类加载器
- ApplicationClassLoader就会检查下它的父加载器是否已经加载过了,如果没有,就调用父加载器extensionClassLoader
- ExtensionclassLoader也会检查下,它的父加载器是否加载过了,如果没有,就调用父加载器BootStrapClassloader也会检查下,它的父加载器是否加载过了,但是自己没有父加载器,于是就扫描自己负责的目录
- BootStropClassLoader也会检查下,它的父加载器是否加载过了,但是自己没有父加载器,于是就扫描自己负责的目录。但是还是没有找到,那么此时就回到子记载器中继续扫描。
- ExtensionClassLOader也扫描自己负责的目录,也没扫描到,回到子加载器继续扫描
- ApplicationClassLoader也扫描自己负责的目录,能找到Test类,于是进行后溪的加载,查找目录环节结束。