厦门网站设计开发网页公司,重新建设网站的请示,网站制图软件,校园文化建设Java虚拟机#xff08;JVM#xff09;是Java程序运行的核心组件#xff0c;它负责解释执行Java字节码#xff0c;并在各种平台上执行。JVM的设计使得Java具有跨平台性#xff0c;开发人员只需编写一次代码#xff0c;就可以在任何支持Java的系统上运行。我们刚开始学习Ja…Java虚拟机JVM是Java程序运行的核心组件它负责解释执行Java字节码并在各种平台上执行。JVM的设计使得Java具有跨平台性开发人员只需编写一次代码就可以在任何支持Java的系统上运行。我们刚开始学习Java时就下载了JDKJava开发工具包它提供了编译、调试和运行Java应用程序所需的工具和库。JDK包括了JREJava运行时环境JRE包含了Java虚拟机JVM。由于不同的CPU的指令集可能不同所以相同的代码可能不能在不同的系统上都正常运行而JVM就是为了解决这个问题Java会先通过javac把 .java 文件编译为 java字节码相当于Java自己的一套CPU指令然后再由具体系统平台上的的JVM不同系统上的JVM可能存在差异把上述字节码转化为对应的CPU能识别的机器指令。 1. JVM中的内存区域划分 JVM其实也是一个进程任务管理器中看到的java进程Java程序的执行时申请的内存就是JVM从系统这边申请到的内存JVM会先申请一块大的内存这块内存在给Java程序使用时又会根据实际的用图划分出不同的区域每个区域都有不同的作用。 Java堆Java Heap Java堆是Java虚拟机管理的最大一块内存区域用于存放对象实例数组类的成员变量。Java堆是所有线程共享的内存区域是垃圾回收的重点区域。 Java虚拟机栈Java Virtual Machine Stacks Java虚拟机栈也称为栈内存用于存储线程的方法调用、局部变量、部分结果等。每个方法在被调用时都会创建一个栈帧并入栈方法执行完毕后栈帧出栈。栈帧包括局部变量表、操作数栈、动态链接、方法返回地址等。 本地方法栈Native Method Stack 本地方法栈类似于Java虚拟机栈但是它为Native方法服务即JVM内部使用C、C等编写的本地方法。 程序计数器Program Counter Register 程序计数器是一块较小的内存区域用于记录当前线程下一条执行的字节码指令地址。在多线程环境下每个线程都有独立的程序计数器。 元数据区Metaspace/ 方法区Method Area元数据指的是一些辅助性质的描述性质的属性元数据区主要用于存储类的元数据信息例如类的结构、方法信息、字段信息等。一个程序有哪些类每个类中有哪些方法每个方法里要包含哪些指令都会记录在元数据区中即元数据区储存了Java代码编译后的Java字节码 注意一个JVM进程中堆和元数据区只有一个栈和程序计数器可能有多份每个线程都有一个自己的程序计数器和栈即每个线程都有自己的执行流。
public class Test{private int n;//堆区private static int m;//static修饰的变量为类属性储存在元数据区public static void main(String[] args) {Test t new Test();//t为局部变量储存在栈区类的实例储存在堆区}
} 2.JVM的类加载机制
2.1 类加载过程
JVM的类加载机制是指JVM在运行时将类的字节码加载到内存中并进行验证、准备、解析和初始化的过程。JVM的类加载机制主要包括以下几个步骤 加载Loading查找并加载类的字节码文件。这个过程可以通过类加载器来完成类加载器会根据类的全限定名在文件系统、网络或其他地方找到对应的字节码文件并将其读入内存。 验证Verification确保被加载的类的字节码是合法、符合JVM规范的。包括文件格式验证、元数据验证、字节码验证、符号引用验证等步骤。具体验证依据在Java虚拟机规范中有明确的格式说明 准备Preparation为类的静态变量分配内存并设置默认初始值这些变量所使用的内存都将在方法区中进行分配。 解析Resolution将类中的符号引用转换为直接引用这个过程可以在运行时进行也可以在编译时进行。 class Test{String s hello;
} 上面代码编译后hello会储存在常量池中s中相当于保存了“hello的字符串常量的地址但是代码没有运行时s和hello都在字节码文件中文件中没有地址这样的概念所以在代码运行前s中存储的是一个类似于”偏移量的概念记录了“hello”的相对位置就是这里的符号引用。 初始化Initialization对类进行初始化包括执行类构造器clinit()方法静态变量赋值等操作。在初始化阶段JVM会根据程序中对类的主动使用情况来触发初始化例如创建类的实例、访问类的静态成员、调用类的静态方法等。
2.2 双亲委派模型
在类加载过程中JVM采用了双亲委派模型即由多个不同层次的类加载器组成一个层次结构每个类加载器都有自己的责任范围当一个类需要加载时先由最顶层的类加载器尝试加载如果无法加载再交由下一层的类加载器依次类推直到最底层的类加载器。
JVM中进行类加载是由一个专门的模块“类加载器ClassLoader”完成的类加载器的作用是通过“全限定类名”带有包名的类名例如java.land.String可以类比为文件路径中的绝对路径查找 .class文件把 .class文件的数据转化为运行时需要的类对象并加载到JVM中。
JVM中默认提供了三种类加载器分别是 启动类加载器Bootstrap ClassLoader负责加载Java的核心类库如java.lang包下的类。它是JVM自身的一部分通常由C编写并不继承自java.lang.ClassLoader类。 扩展类加载器Extension ClassLoader负责加载Java的扩展类库位于jre/lib/ext目录下的类库。 应用程序类加载器Application ClassLoader也称为系统类加载器负责加载当前项目的代码目录以及第三方库的目录。
除了这三个默认的类加载器开发者也可以自定义类加载器来实现特定的类加载需求比如从网络中动态加载类、加密类加载等。自定义类加载器需要继承自java.lang.ClassLoader类并重写其中的findClass()方法来实现类的加载逻辑。
注意这三个类加载器之间存在父子关系上面的为父加载器下面的为子加载器即1是2的父亲2是3的父亲。
双亲委派流程当一个类加载器收到类加载请求时它首先将这个请求委托给它的父类加载器处理。如果父类加载器无法完成此加载请求子加载器才会尝试自己去加载。这个过程会一直递归下去直到启动类加载器。这样做的目的是保证Java核心API的稳定性防止用户自定义的类替换掉核心类库中的类。
1. 从Application ClassLoader作为入口
2. Application ClassLoader不会立刻搜索自己负责的目录会把任务交给父类加载器Extension ClassLoader。
3. Extension ClassLoader也不会立刻搜索自己负责的目录也会把任务交给父类加载器Bootstrap ClassLoader。
4.Bootstrap ClassLoader没有父类加载器就会搜索自己负责的目录查找需要的 .class文件如果找到了就直接进入打开文件/读文件等流程中如果没找到则把任务交给下一级类加载器Extension ClassLoader继续尝试寻找。
5.Extension ClassLoader 接受到任务此时就会在自己负责的目录中开始寻找如果找到了就直接进入打开文件/读文件等流程中如果没找到则同样把任务交给下一级类加载器Application ClassLoader继续尝试寻找。
6.Application ClassLoader 也会在自己负责的目录中开始寻找如果找到了就直接进入打开文件/读文件等流程中如果没找到也会尝试把任务交给下一级但是默认情况下Application ClassLoader没有下一级类加载器了于是就会类加载失败抛出ClassNotFoundException异常
上述流程就保证了类加载的顺序防止用户自定义的类替换掉核心类库中的类。例如用户自己定义了一个java.lang.String如果这个类先被加载了java核心库中的String类就不会被加载。
3. 垃圾回收机制GC
垃圾回收GC是自动内存管理的关键技术之一。它负责清理不再使用的对象释放内存空间。垃圾回收回收的是堆的内存 所以也可以说是回收对象。
3.1 识别垃圾
判定对象后续是否会继续使用不会继续使用的就会被视为垃圾如果一个对象没有任何引用指向它那么这个对象就无法被继续使用了也就会被视为垃圾。
3.1.1 引用计数
引用计数方法并没有在JVM中使用但是广泛运用在其他主流语言的垃圾回收机制中如PythonPHP。
引用计数是通过给每个对象安排一个额外的空间记录当前有几个引用指向该对象。每有一个引用指向该对象时就把值加一反之则减一当这个值为0时则视为垃圾当负责垃圾回收的扫描线程获取到这个对象的引用计数情况时发现为0就会释放这个对象的空间。
引用计数存在两个关键的问题
问题一要给每个对象安排计数器就会消耗额外的空间如果对象数量很多总的空间浪费也就很多。问题二可能产生循环引用问题例如两个对象互相引用但没有一个外部的引用指向它们此时这两个对象是无法被获取到的但他们的引用计数又不为0也就不会被释放。
3.1.2 可达性分析
JVM采用的就是可达性分析来识别对象是否是垃圾。
可达性分析采用的方法是 遍历所有变量JVM会遍历所有能够被直接或者间接访问到的对象能访问到的自然不是垃圾遍历一圈后不能访问到的就视为垃圾。
3.2 释放垃圾
找到垃圾以后就需要把垃圾对象所占的内存空间进行释放。
3.2.1 标记-清除算法
把标记为垃圾的对象直接释放。这种释放方式会导致内存碎片问题。
如图所示释放后会导致出现很多大大小小的内存碎片而内存申请都是一次申请一段连续的内存空间这就导致了部分内存碎片可能无法使用到也就导致了空间浪费。
3.2.2 复制算法
将内存分为两个相等的部分每次只使用其中一半。当这一半的内存用完后就将还在使用的对象复制到另一半然后再清除掉已经使用过的那一半内存中的所有对象。
这种方法避免了内存碎片但是总的可用空间变少了同时复制对象也会消耗时间。
3.2.3 标记-整理算法
将所有存活的对象向一端移动然后直接清理掉端边界以外的内存。 这样也可以避免内存碎片问题但是移动对象也要消耗时间。
3.2.4 分代回收算法
JVM中采用的是分代回收算法。给每个对象引入年龄的概念JVM中存在专门的线程负责周期性的扫描/释放对象如果一个对象被线程扫描了一次并且不是垃圾该对象的年龄就会1初始年龄为0。
JVM中会根据对象年龄的差异把整个内存分成两个大的部分新生代年龄较小的对象/ 老年代年龄较大的对象新生代又被划分为三个区域其中大的一部分区域为 伊甸区 剩下两块大小相同的区域叫做 生存区 或者 幸存区 。 新的对象都是从伊甸区中被创建的第一轮GC扫描后没有被清除的对象就会被通过复制算法移动到生存区即生存区相当于未被使用的那块内存生存区中的对象下次被GC扫描后存活的对象又会被通过复制算法移动到另一个生存区注意两个生存区完全是对等的每经历一次GC对象的年龄就会1如果某个对象在生存区中经过了若干轮GC任然没有被清除JVM就会认为这个对象的生命周期很长就会把这个对象移动到老年代老年代的对象也会被GC扫描只不过扫描的频率较低。老年代的对象被视为垃圾时会按照标记-整理算法释放内存。