今天是2022年的第二天,小伙伴们新年快乐啊!昨天摊了一天,今天来学习一下JVM。
在工作中JVM的使用比较少,相比之下面试中会更多的问到JVM的问题。但是作为一个Java程序员,JVM是我们必须要了解的部分。但是我看过不少JVM的帖子,对于一个新人来说很难理解其中的概念,这篇文章会以小白的心理去探究JVM的内容,如果有部分内容没有讲清楚也可以私聊一起探讨,希望这篇文章能够让大家更快的了解JVM的内容。
1.JDK、JRE、JVM的关系
JDK:Java Development ToolKit(Java开发工具包)是整个Java的核心,我们如果要开发Java程序就一定要安装JDK。
JRE:Java Runtime Environment(Java运行环境)包含在JDK中,可以说是JDK的一部分。他就是为了运行Java程序而存在的。那么可能会有人问到JVM不是用来运行JAVA程序的么?没错,JVM是用来运行JAVA程序的,但是JRE中还包括了Java的类库,JVM解析java文件的时候需要依靠JRE中的类库来进行解释,进而编译成为class文件。
注(简单了解):当我们下载JDK的时候,会有两个文件夹,一个JDK,一个JRE,其中JDK文件夹内部还有一个JRE。外面的JRE是用来运行我们编写的Java代码的;内部的JRE是用来运行Java内部工具的,比如javac.exe等工具。
JVM:Java Virtual Machine(Java 虚拟机)包含在JRE中,是JRE的一部分,Java的一大特性就是他的跨平台性,一次编译到处运行,依靠的就是JVM。电脑上安装了不同版本的JVM,运行时依靠本地的JVM编译,只要JVM支持你的电脑系统,你就可以在此电脑上运行Java代码。
注(简单了解):JVM是用C语言编写的。
2.Java文件的编译过程
这是学习JVM的主要部分,我先简单用一张图来描述,当我们选择编译Java文件的时候,系统是怎么运作的。
一般最基本的了解,就是知道编译后的java文件可以通过javac命令编译成class文件然后执行class文件,后续部分一般不会很了解,下面我会依次进行解释。
3.类加载器
在这里就需要提到一下反射,可能有些小伙伴还没有学习到,所以这里简单的介绍一下。
java反射
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
这里我们还要引入一个定义,那就是——类是一个模板,我们不管new出了多少个对象,通过getClass()方法得出的Class对象都是同一个。
public class Test{
public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();
System.out.println("test1对象的哈希值:" + test1.hashCode());
System.out.println("test2对象的哈希值:" + test2.hashCode());
Class<? extends Test> atest1 = test1.getClass();
Class<? extends Test> atest2 = test2.getClass();
System.out.println("test1对象反射的Class类的哈希值:" + atest1.hashCode());
System.out.println("test2对象反射的Class类的哈希值:" + atest2.hashCode());
}
}
有兴趣的同学可以执行一下如下代码,结果就是对象之间的哈希值不同,但是反射出的Class对象的哈希值是相同的。也就证明了我们说的类是模板的概念。
那么这个时候我们可以根据反射出的Class对象的getClassLoader()方法获取到这个类的类加载器并输出在控制台。
System.out.println("类加载器的地址:" + atest1.getClassLoader());
打印结果为
AppClassLoader代表了应用加载器,那么什么是应用加载器呢?那就要介绍一下类加载的概念了。
类加载器内的等级
1..启动类(根)加载器 BootstrapClassLoade(Java中获取不到这个加载器) -最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
2.扩展类加载器 ExtClassLoader -主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
3.应用程序加载器 AppClassLoader -面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
4.自定义类加载器 如果我们想自定义类加载器的话,我们继承ClassLoader类就可以了。
双亲委派机制
当我们调用Java自带的方法时,他们其实就是存在于JDK中的一些普通class类,比如String类是在java.lang.String里,那么我们是否可以自己创建一个java.lang.String类并重写String类中的方法来替代原有的String类呢?
代码如下
//首先我们要创建一个名称为java的包
//在这个包下创建一个名称为lang的包
//最后再创建一个名称为String的Java类
public class String {
public String toString() {
return "Hello World";
}
public static void main(String[] args) {
String str = new String();
str.toString();
}
}
此程序会报错
原因就是因为双亲委派机制,当根加载器中有String类,就不会加载我们编写的String类,但是根加载器中的String类中没有main方法,所以报错——找不到main方法。
类加载的步骤:
1.类加载器收到类加载的请求
2.将请求向上委托给父类加载器加载
3.调用findClass方法查找,查找不到抛出异常,让子类加载器加载
4.类加载,同时初始化类中静态的属性(给类中静态属性赋默认值)
5.执行静态代码块
6.分配内存空间,同时初始化非静态的属性(给非静态属性赋默认值)
7.如果声明属性的同时有显示的赋值,那么进行显示赋值把默认值覆盖
8.执行匿名代码块
9.执行构造器
10.返回内存地址
先执行父类后执行子类,依次为
1.父类静态代码块和静态变量初始化。
2.子类静态代码块和静态变量初始化。
3.父类的实例变量初始化。
4.父类的构造函数。
5.子类的实例变量初始化。
6.子类的构造函数。
4.本地方法栈、本地方法接口、本地方法库
我们查看Java自带方法的源码的时候,有时候发现Java会调用一个native关键字声明的抽象方法,也就是本地方法接口(JNI Java Native Implement)——JNI存在也是想要让Java程序能够调用C与C++。那么native关键字又代表着什么意思呢?
native关键字代表着此方法需要去调用底层C语言的方法库。
native关键字的方法会进入本地方法栈,记录native方法,最终调用本地方法接口执行本地方法库中的方法。
5.程序计数器
程序计数器(Program Counter Register),每一个线程都私有一个程序计数器,其实就是一个指针。
当我们多线程执行的时候,切换上下文的时候就可以通过此指针继续执行。
在一个线程中执行的时候,JVM通过读取程序计数器的值(一般为行号)来确定下一条需要执行的字节码指令,native关键字方法的计数器值为空。
6.方法区
方法区存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
例如static关键字声明的,final关键字声明的,Class类模板,常量池。
特点:
1.方法区是被所有线程共享的一个区域。
2.方法区的大小是非固定的,JVM可以根据应用需要动态调整,JVM也支持用户和程序指定方法区的初始大小。
3.方法区有垃圾回收机制,一些类不再被使用则变为垃圾,需要进行垃圾清理。
7.栈
栈是一种数据结构,用来存储8大基本数据类型、对象的引用、实例的方法。
我们常将栈与队列进行对比:
栈:先进后出,也就是说最后一个进入栈的会第一个出去。
队列:先进先出(First Input First Output) 最先进入队列的会第一个出去。
特点:
栈会优先加载main方法,当main方法(主线程)从栈中释放,说明程序结束。
对于栈来说,不存在垃圾回收机制。
栈的运行示意图如下
针对于上面的图来说,栈中存放的每个数据我们称为栈帧。每调用一个方法就会产生一个栈帧。
一个栈帧内有如下数据:
1.方法索引
2.输入输出参数
3.本地变量
4.Class的引用地址
5.父帧——指向调用此栈帧的另一个栈帧
6.子帧——指向此栈帧调用的栈帧
public class Test{
//methodA的父帧指向main方法
//methodA的子帧指向methodB方法
public void methodA(){
methodB();
}
//methodB的父帧指向methodA方法
public void methodB(){
System.out.println("Hello World");
}
//main方法最先被加载到栈中
public static void main(String[] args) {
methodA();
}
}
当我们递归调用的时候操作不当就会造成栈溢出错误*Error。
到这里JVM基本的概念我们就讲过了,其中还有堆部分的内容没有讲,因为这部分内容较多,且涉及JVM调优部分,所以我这里单独用一篇文章进行描述。