一.初步了解JVM的基本
-
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。是运行Java代码的核心部分,主要负责将Java字节码翻译为机器语言,并且提供了运行时的环境。JVM作为Java平台的一部分,隐藏了操作系统和硬件的差异性,使Java代码可以在不同的系统上运行而无需修改。换句话解释就是Java是一种半编译-半解释的语言, 当我们编写和发布一个Java程序的时候,其实只要发布.class问价即可, 当JVM拿到.class文件的时候, 就知道该如何转换. 不同平台的jvm是存在差异的,不是同一个, 但是对Java层面上提供的内容是一致的, 当Windows上的jvm拿到.class文件就可以转化为Windows上能支持的可执行指令了; Linux上的jvm就可以转化成Linux上支持的可执行指令了
-
常见的虚拟机:JVM、VMwave、Virtual Box。
JVM 和其他两个虚拟机的区别:
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
- JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。JVM 是一台被定制过的现实当中不存在的计算机。
二.JVM中的内存区域划分.
什么叫做区域划分?
JVM本质上也是一个进程,他与需要从操作系统这里申请内存.申请到的这些内存空间,就支撑后续Java程序的执行.比如说,在Java中定义变量(就需要申请空间), 申请的空间就是从JVM刚申请的空间中的一部分 换句话说,JVM在这个过程中扮演了一个"二房东"的角色. 因此JVM申请的空间就需要合理的分配利用, 这样根据实际的用途来划分出的不同空间,这个过程就叫做内存区域划分.
- 堆(Heap):堆是JVM中最大的一块内存区域,几乎所有的对象实例都在这里分配内存。它是线程共享的,意味着所有线程都可以访问这个区域。堆也是垃圾回收发生的主要场所,JVM通过不同的垃圾回收算法管理堆内存,以优化内存的使用和回收不再使用的对象。可以通过启动参数如-Xmx来设置堆的最大大小。代码中new出来的对象/对象中的非静态成员变量,都是在这里
- 虚拟机栈/本地方法栈 :每个线程都有自己的虚拟机栈,用于存储局部变量、方法参数和调用上下文等。每当一个方法被调用时,一个新的栈帧就会被压入到调用线程的栈中。栈帧包含了方法的局部变量表、操作数栈等信息。当方法执行完毕,对应的栈帧就会被弹出。虚拟机栈是线程私有的,随线程而生,随线程而灭.换句话说就是主要的功能就是记录方法之间的调用关系
- 程序计数器(Program Counter Register):程序计数器是一个较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器通过改变程序计数器的值来选择下一条需要执行的字节码指令。程序计数器也是线程私有的,生命周期与线程紧密相关。每个线程都有一个独立的程序计数器,用于记录该线程当前正在执行的那条字节码指令。这个区域空间比较小,是用来专门存储下一条要执行Java指令的地址
- 元数据区:也叫做方法区 , 方法区是JVM中用于存储被加载类的元数据信息的地方,包括类的结构信息、运行时常量池、字段信息、方法信息等。在Java 8及以后的版本中,方法区被实现为元空间(Metaspace),使用的是进程的本地内存而非堆内存。这部分内存是由JVM自动管理的,主要用于加载类的信息。例如:咱们写的Java代码中,if, while, for 等各种逻辑运算 这些操作都会被转换成Java字节码, 这些字节码在程序运行的时候,就会被JVM加载到内存中,放到元数据区(方法区)当中,此时,当程序要如何执行,要做哪些事,就会按照元数据区里记录的代码以此执行.
注:
- [在这些内存区域划分之中, 堆和元数据区只有一份, 程序计数器和栈可能有多个, 因为每个线程都需要有自己的程序计数器和栈(每个线程都要有自己的执行流)]
- 由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是⼀个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存。
三.JVM中的类加载机制.
- JVM(Java虚拟机)的类加载机制是一个复杂而精密的过程,类加载指的是Java进程运行的时候, 需要把.class文件从硬盘读取到内存,并进行一系列解析的过程
类加载可以分为5个步骤(也可以说是3个,这个情况是把2.3.4合并成一个了)
- 1.加载. 这个步骤主要是把硬盘上的.class文件,找到并打开文件,读取到文件中的内容(此时的内容认为读到的是二进制的数据)
- 2.验证. 这个步骤主要是需要确保读到的内容,是合法的.class文件(字节码文件)格式
验证过程主要包含四个验证过程:- 文件格式验证
四个验证过程中,只有格式验证是建立在二进制字节流的基础上的。格式验证就是对文件是否是0xCAFEBABE开头、class文件版本等信息进行验证,确保其符合JVM虚拟机规范。 - 元数据验证
元数据验证是对源码语义分析的过程,验证的是子类继承的父类是否是final类;如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法;子父类中的字段、方法是否产生冲突等,这个过程把类、字段和方法看做组成类的一个个元数据,然后根据JVM规范,对这些元数据之间的关系进行验证。所以,元数据验证阶段并未深入到方法体内。 - 字节码验证
既然元数据验证并未深入到方法体内部,那么到了字节码验证过程,这一步就不可避免了。字节码主要是对方法体内部的代码的前后逻辑、关系的校验例如:字节码是否执行到了方法体以外、类型转换是否合理等。显然这是一个非常复杂的过程,无法完全保证字节码验证准确无遗漏的。而且,如果在字节码验证浪费了大量的资源,似乎也有些得不偿失 。 - 符号引用验证
符号引用的验证其实是发生在符号引用向直接引用转化的过程中,而这一过程发生在解析阶段。因为都是验证,所以一并在这讲。符号引用验证做的工作主要是验证字段、类方法以及接口方法的访问权限、根据类的全限定名是否能定位到该类等。
- 文件格式验证
-
- 准备. 这个步骤主要是==给类对象,申请内存空间.
注此时申请到的内存空间,里面的默认值全都是0(这个阶段中,类对象里的静态成员变量的值也就相当于是0了)
- 准备. 这个步骤主要是==给类对象,申请内存空间.
-
- 解析 主要是针对类中的字符串常量进行处理,==Java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程.
class Test{
private String s = "hello";
}
偏移量 :偏移量是一个相对的概念,可以理解为你相对与我的位置差了多少.
符号引用:在上述的伪代码当中,可以很明确的知道,s变量中相当于保存了"hello"字符串常量的地址, 但是在文件中不存在地址这样的概念, 因此就引入了偏移量这样的概念来表示hello存储的地方,此处文件中填充给s的"hello"的偏移量就可以认为是符号引用.
直接引用: 把.class文件加载到内存中.就会先把"hello"这个字符串加载到内存中.此时"hello"就有了真实的地址了,这个使用真实地址的方式称为直接引用.
-
- 初始化. 把类对象的各个部分的属性进行赋值填充==>触发对父类的加载,初始化静态成员,执行静态代码块.
四.双亲委派模型.
- 双亲委派模型是Java中类加载器的一种工作机制,它的核心思想是每个类加载器在尝试加载一个类时,会先委托其父加载器去执行这个任务,只有当父加载器无法完成这个任务时,子加载器才会尝试自己去加载该类。这种机制保证了Java平台的安全性和类的唯一性,防止了核心API被恶意篡改的风险。
- Java中的类加载器默认有三个(也可以自定义类加载器):
- BootstrapClassLoader :主要负责标准库的目录
- ExtensionClassLoader :主要负责扩展库的目录(Java语法规范里面描述了标准库中应该有的功能 ,但是实现JVM的厂商/组织,也会在标准库的基础之上扩充一些额外的功能,这些额外的功能部分就可能存储到扩展库的目录里面)
- ApplicationClassLoader :负责查找当前项目的代码目录,以及第三方库的目录.
双亲委派模型的工作过程:
- 从ApplicationClassLoader 作为入口,先开始工作
- ApplicationClassLoader 不会立即搜素自己负责的目录,而是会把搜素的任务交给自己的父亲.
- 然后,代码就进入到ExtensionClassLoader 的范畴中了, ExtensionClassLoader 也不会立即搜素自己负责的目录,也是会把搜素的任务交给自己的父亲.
- 再然后,代码就进入到了BootstrapClassLoader 的范畴之中了 ,BootstrapClassLoader 也不会立即搜素自己负责的目录,也是会把搜素的任务交给自己的父亲.
- 但是BootstrapClassLoader 发现自己==没有父亲,才会真正搜素负责的目录(标准库目录) ==.通过全限定类名,尝试在标准库目录中找到符合要求的.class文件. 如果找到了,接下来就直接进入到打开文件/读取文件等流程中; 如果没有找到,回到孩子这一辈的类加载器中,继续尝试加载.
- ExtensionClassLoader 收到父亲交回给他的任务之后,会进行搜素自己负责的目录,如果找到了,继续执行后面的流程; 如果没找到,就继续返回到孩子这一辈的类加载器中继续尝试加载
- ApplicationClassLoader 收到父亲交回给他的任务之后,也会进行搜索自己负责的目录,如果找到了,就继续执行后面的流程; 如果没找到,也是继续返回到孩子这一辈的类加载器中继续尝试加载,但是由于默认的情况下,ApplicationClassLoader 没有孩子了,此时就说明类加载失败了,就会抛出ClassNotFoundException异常.
注
- 上述的类加载器,存在的父子关系(不是面向对象中的,父类子类的关系) 而是类似于"二叉树", 有一个指针(引用)parent ,指向自己的"父"类加载器.
- 上述双亲委派模型的执行过程, 可以有效的避免你自己写的类,不小心和标准库中的类名重复,导致标准库的类功能失效