编程语言
首页 > 编程语言> > 工作中需要用到的Java知识(JVM上篇——JVM概念讲解)

工作中需要用到的Java知识(JVM上篇——JVM概念讲解)

作者:互联网

今天是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();
  }
}

当我们递归调用的时候操作不当就会造成栈溢出错误StackOverflowError。

到这里JVM基本的概念我们就讲过了,其中还有堆部分的内容没有讲,因为这部分内容较多,且涉及JVM调优部分,所以我这里单独用一篇文章进行描述。

标签:JRE,Java,String,JVM,上篇,方法,加载
来源: https://blog.csdn.net/weixin_49290171/article/details/122275865