Java JVM 底层机制,类加载器、堆、栈、方法区等详解

一、JRE、JDK、JVM之间的关系

JVM(Java Virtual Machine) : Java 虚拟机。它只认识 xxx.class 这种类型的文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心。
java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳。它会装载jvm.dll(windows下),这个动态连接库才是java虚拟机的实际操作处理所在。java.exe程序只负责查找和装载jvm.dll动态库,并调用它进行class文件执行处理。
“java”命令与“javac”命令其实都是launcher,负责启动JVM并把启动参数传给JVM而已。

JRE(Java Runtime Enviroment): Java运行时环境。面向Java程序的使用者,而不是开发者。JRE是运行Java程序所必须环境集合,包含JVM标准实现及Java核心库。它包括Java虚拟机、Java平台核心类和支持文件。它不包含开发工具(编译器、调试器等)。

JDK(Java Development Kit): Java开发工具包,它提供了Java的开发环境(编译器javac等工具,用于将java文件编译成class文件)和运行环境(包括JVM和Runtime辅助包,用于解析class文件使其运行)。如果安装了JDK,那么不仅拥有了Java开发环境,也拥有了运行Java程序的平台。实际上JDK=开发工具tools+JRE+标准类库。

二、JVM基本结构

在这里插入图片描述

1. 类装载器子系统

在JVM中负责加载.class文件。

2. 运行时数据区

2.1 方法区

作用:
当JVM使用类装载器定位class文件,并将其输入到内存中。会提取class文件的类型信息(包括名称、方法信息、字段信息),并将这些信息存储到方法区中。同时放入方法区中的还有该类型中的类静态变量(static修饰的,非static的叫做实例变量)、常量以及编译器编译后的代码等。

常量池:
在class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池。常量池指的是在编译期被确定,并被保存在已编译的class文件中的一些数据。除了包含代码中所定义的各种基本数据类型和对象型(String及数组)的常量值(final,在编译时确定,并且编译器会优化)还包含一些以文本形式出现的符号引用(类信息)。

运行时常量池:
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。

方法区是多线程共享的。也就是当虚拟机实例开始运行程序时,边运行边加载进class文件。不同的Class文件都会提取出不同类型信息存放在方法区中。同样,方法区中不再需要运行的类型信息会被垃圾回收线程丢弃掉。

2.2 堆内存

作用:
Java程序在运行时创建的所有类型对象和数组都存储在堆中。JVM会根据new指令在堆中开辟一个确定类型的对象内存空间。

  • 堆区中不存放基本数据和对象引用,只存放对象本身
  • 每个对象包含一个与之对应的class信息–class的目的是得到操作指令
  • 堆的优势时可以动态地分配内存大小,生存期也不必告诉编译器,Java垃圾回收器会自动收走不用的对象
  • 缺点时由于在运行时动态分配内存,存取速度较慢
  • 堆的大小是可扩展的,通过-Xms(设置堆内存初始大小)和-Xmx(设置堆内存最大值)控制。如果在堆中没有内存完成实例分配,并且也无法再扩展时,将会抛OutOfMemoryError异常
2.3 虚拟机栈

作用:
每启动一个线程,JVM都会为它分配一个Java栈,用于存放方法的局部变量,操作数以及异常数据等。当线程调用某个方法是,JVM会根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入Java栈中,方法执行完毕时,JVM会弹出该栈帧并释放掉。

-每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中 。
-每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
-栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

2.4 程序计数器

作用:
用于保存当前线程执行的内存地址。

由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先的状态,就要一个独立的计数器,记录之前中断的地方,可见程序计数器是线程私有的。

2.5 本地方法栈

和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。

Java官方对于本地方法的定义为methods written in a language other than the Java programming language,就是使用非Java语言实现的方法,但是通常我们指的一般为C或者C++,因此这个栈也有着C栈这一称号。

三、编译和运行过程

//MainApp.java
public class MainApp {
    public static void main(String[] args) {
        Animal animal = new Animal("Puppy");
        animal.printName();
    }
}
//Animal.java
public class Animal {
    public String name;

    public Animal(String name) {
        this.name = name;
    }

    public void printName() {
        System.out.println("Animal [" + name + "]");
    }
}

① 在编译好java程序得到MainApp.class文件后,在命令行上敲java AppMain。系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为AppMain.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。
② 然后JVM找到AppMain的主函数入口,开始执行main函数。
③ main函数的第一条命令是Animal animal = new Animal(“Puppy”);就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中。、
④ 加载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存,然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。最后会把堆内存中Animal的地址给栈中aninal。
⑤ 当使用animal.printName()的时候,JVM根据animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。
⑥ 开始运行printName()函数的字节码(可以把字节码理解为一条条的指令)。

在这里插入图片描述

四、类加载机制

虚拟机将描述类的数据从class文件加载到内存,并对数据进行校验
准备、解析和初始化
, 最终会形成可以被虚拟机使用的java类型,这就是一个虚拟机的类加载机制。
JVM主要包含三大核心部分:类加载器、运行时数据区和执行引擎。
一个类的生命周期:
在这里插入图片描述
在Java中,对于类有且仅有四种情况会对类进行初始化。

  • 使用new关键字实例化对象时,读取或设置一个类的静态字段时(除final修饰的static外),调用类的静态方法的时候,都只会初始化该静态字段或者静态方法所定义的类
  • 使用reflect包对类进行反射调用的时候,如果没有进行初始化,则先要初始化该类
  • 当初始化一个类的时候,如果其父类没有初始化过,则先要触发其父类初始化
  • 虚拟机启动的时候,会初始化一个有main方法的主类

1. 类加载的过程

1.1 加载

加载阶段主要完成三件事:
a. 通过一个类的全限定名来获取定义此类的二进制字节流,
b. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
c. 在java堆中生成一个代表此类的class对象,作为访问方法区这些数据的入口
这个加载过程主要是靠类加载器实现的,这可以由用户自定义类加载

1.2 验证

这个阶段目的在于确保class文件的字节流中包含的信息符合当虚拟要求,不会危害虚拟机自身安全。
主要包括四种验证:

  • 文件格式验证:基于字节流验证,验证字节流是否符合Class文件格式的规范,并且能被当前虚拟机处理。
  • 元数据验证: 基于方法区的存储结构验证,对字节码描述信息进行语义验证。
  • 字节码验证:基于方法区的存储结构验证,进行数据流和控制流的验证。
  • 符号引用验证:基于方法区的存储结构验证,发生在解析中,是否可以将符号引用成功解析为直接引用。
1.3 准备

仅仅为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即零值,这里不包含用final修饰的static,因为final在编译的时候就会分配了(编译器的优化),同时这里也不会为实例变量分配初始化。类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

1.4 解析

解析主要就是将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析。

1.4 初始化

初始化阶段依旧是初始化类变量和其他资源,这里将执行用户的static字段和静态语句块的赋值操作。这个过程就是执行类构造器< clinit >方法的过程。
< clinit >方法是由编译器收集类中所有类变量的赋值动作和静态语句块的语句生成的,类构造器< clinit >方法与实例构造器< init >方法不同,这里面不用显示的调用父类的< clinit >方法,父类的< clinit >方法会自动先执行于子类的< clinit >方法。即父类定义的静态语句块和静态字段都要优先子类的变量赋值操作。

2. 类加载器

2.1 类加载器的分类

-启动类加载器(Bootstrap ClassLoader):主要负责加载<JAVA_HOME>\lib目录中的’.'或是-Xbootclasspath参数指定的路径中的,并且可以被虚拟机识别(仅仅按照文件名识别的)的类库到虚拟机内存中。它加载的是System.getProperty(“sun.boot.class.path”)所指定的路径或jar。

  • 扩展类加载器(Extension ClassLoader):主要负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。它加载的是
    System.getProperty(“java.ext.dirs”)所指定的路径或jar。
  • 应用程序类加载器(Application ClassLoader): 也叫系统类加载器,主要负责加载ClassPath路径上的类库,如果应用程序没有自定义自己类加载器,则这个就是默认的类加载器。它加载的是System.getProperty(“java.class.path”)所指定的路径或jar。
2.2 类加载器的双亲委派模型

类加载器双亲委派模型的工作过程是:如果一个类加载器收到一个类加载的请求,它首先将这个请求委派给父类加载器去完成,每一个层次类加载器都是如此,则所有的类加载请求都会传送到顶层的启动类加载器,只有父加载器无法完成这个加载请求(即它的搜索范围中没有找到所要的类),子类才尝试加载。
使用双亲委派模型主要是两个原因:

  • 可以避免重复加载,当父类已经加载了,则就子类不需再次加载;
  • 安全因素,如果不用这种,则用户可以随意的自定义加载器来替代Java核心API,则就会带来安全隐患。
2.3 类加载实例的过程

当在命令行下执行:java HelloWorld(HelloWorld是含有main方法的类的Class文件),JVM会将HelloWorld.class加载到内存中,并在堆中形成一个Class的对象HelloWorld.class。
基本的加载流程如下:
1)寻找jre目录,寻找jvm.dll,并初始化JVM
2)产生一个Bootstrap Loader(启动类加载器)
3)Bootstrap Loader,该加载器会加载它指定路径下的Java核心API,并且再自动加载Extended Loader(标准扩展类加载器),Extended Loader会加载指定路径下的扩展JavaAPI,并将其父Loader设为BootstrapLoader
4)Bootstrap Loader也会同时自动加载AppClass Loader(系统类加载器),并将其父Loader设为ExtendedLoader
5)最后由AppClass Loader加载CLASSPATH目录下定义的类,HelloWorld类

参考文档:
https://www.jianshu.com/p/ae97b692614e?from=timeline
https://blog.csdn.net/heart_mine/article/details/79495032

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值