目录
JVM,Java Virtual Machine,Java虚拟机
虚拟机是指软件模拟的具有完整功能的、运行在一个完全隔离的环境中的完整计算机系统
常见的虚拟机:JVM、VMwave、Virtual Box
JVM和其他两个虚拟机的区别:
- VMware与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器
- JVM则是通过软件模拟Java字节码的指令集,JVM中则是主要保留了PC寄存器,其他的寄存器都进行了裁剪
JVM是一台被定制过的现实当中不存在的计算机
一个运行起来的Java进程就是一个JVM虚拟机,就需要从操作系统申请一大块内存
就会把这个内存划分成不同的区域,每个区域都有不同的作用
一、JVM运行时数据区
Java虚拟机运行时数据区是Java程序在运行过程中使用的内存区域,用于存储程序运行时所需要的数据
这些数据区包括了不同用途的内存区域,每个区域都有特定的作用和生命周期
注意它和Java内存模型(Java Memory Model,JMM)完全不同,属于完全不同的概念
其中堆和方法区都是线程共享的,Java虚拟机栈、本地方法栈、程序计数器是线程私有的
有自己的空间:别的线程也能访问;有自己私有的空间:别的线程无法访问
1、 堆(占据空间最大):存放程序中创建的对象
堆区分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定GC次数之后还存活的对象会放入老生代
垃圾回收时会将Endn中存活的对象放到一个未使用的Survivor中,并把当前的Endn和正在使用的Survivor清除掉
2、Java虚拟机栈:存放方法之间的调用关系,描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
Java虚拟机栈的生命周期和线程相同
- 局部变量表:存放编译器可知的8大基本数据类型、对象引用、存放方法参数和局部变量
- 操作栈:每个方法会生成一个先进后出的操作栈
- 动态链接:每个方法会生成一个先进后出的操作栈
- 方法返回地址:PC寄存器的地址
3、本地方法栈
本地方法栈和虚拟机栈类似,只不过Java虚拟机栈是给JVM使用的,而本地方法栈是给本地方法使用的(使用非Java语言编写的方法,如C、C++)
两种栈都是存放方法之间的调用关系
4、程序计数器:存放下一条要执行的指令的地址
5、方法区(元数据区):存储被虚拟机加载的信息、常量、静态变量、即时编译器编译后的代码等数据的(.class文件加载到内存之后就成了类对象,所以就是存放类对象)
运行常量池是方法区的一部分,存放字面量和字符引用
- 字面量:字符串(JDK8移动到堆中)、final常量、基本数据类型的值
- 符号引用:类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符
class Test{
public int r=100;//成员变量r处于堆上
public static int a=10;//静态变量(类属性)包含在类对象中,处在方法区(元数据区)
void main(){
Test t=new Test();//t是一个引用类型的局部变量,存放在栈的局部变量表中,存储一个对象的地址
//new出来的对象在堆上
}
}
二、JVM类加载
类加载过程
其中前五步是固定的顺序并且也是类加载的过程,其中中间的3步我们都属于连接,所以对于类加载来说总共分为一下几个步骤:加载、连接(验证、准备、解析)、初始化
1、加载(loading)
找到.class文件,打开文件,读取内容。将类的字节码数据从磁盘加载到内存中。这个过程是由类加载器完成的。在加载阶段,虚拟机需要完成一下三件事情:
- 通过类的全限定名获取类的二进制字节流
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2、验证(Verification)
确保类的字节码文件符合虚拟机规范,不会危害虚拟机的运行时环境
主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
3、准备(Perparation)
为类的静态变量分配内存并设置初始值(零值)
这里不包括final修饰的static变量,因为final在编译时就分配了初始值
4、解析(Resolution)
将常量池中的符号引用替换为直接引用的过程(初始化常量)
主要包括:类或接口的解析、字段解析、类方法解析、接口方法解析
5、初始化(Initialization)
Java虚拟机真正开始执行类中编写的Java程序代码,将主导权移交给应用程序
初始化阶段就是执行类构造器<clinit>()方法的过程,在初始化阶段,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步,确保类的初始化只会进行一次
针对类对象进行初始化:设置好类对象中需要的各个属性、static成员变量、静态代码块、以及还可能加载一下父类
双亲委派模型
双亲委派模型是和Java中多个类加载器(启动类加载器、扩展类加载器、应用程序类加载器)的运行规则,通过这个规则可以避免类的非安全性问题和类被重复加载的问题,但他也遇到了一些问题,比如JNDI和JDBC不能通过这个规则进行加载,它需要打破双亲委派模型的方式来加载
机制
如果一个类加载器收到了类加载的请求,它首先不会去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到需要的类)时,子加载器才会尝试自己去完成加载,如果能找到就返回对象,若直到应用程序类加载器都无法加载这个类,则抛出ClassNotFound异常
1)启动类加载器是由C++实现的,用于加载<JAVA_HOME>/jre/lib/rt.jar 和 resources.jar等jar包结构(即标准库的目录lib目录)
2)扩展类加载器用于加载<JAVA_HOME>/jre/lib/ext目录下的jre包(JDK中一些扩展库对应的目录,lib或ext目录)
3)应用类程序加载器用于加载classpath,也就是用户的所有类的(用户自定义的类和第三方库对应目录)
优点
1、避免重复加载类
比如A类和B类都有一个父类C类,那么当A启动时就会将C类加载起来,那么在B类进行加载时就不需要再重复加载C类了
2、安全性
当使用双亲委派模型时,用户就不能伪造一些不安全的系统类了。比如jre已经提供了String类在启动类加载时加载,那么用户若再自定义一个不安全的String类时,按照双亲委派模型就不会再加载用户自定义的那个不安全的String类了,这样就可以避免非安全的问题发生了
打破双亲委任模型
双亲委派模型虽然有其优点,但在某些也存在一些问题,比如Java中SPI(Service Provider Interface,服务提供接口)机制中的JDBC实现
tomcat中加载webapp时就是用的自定义的类加载器,就只能在webapp指定目录中查找,若在这里找不到就不会再去标准库啥的地方找了,直接抛异常
三、JVM中的垃圾回收机制
在C++中,动态分配内存管理允许程序在运行时分配和释放内存,相比静态内存管理(编译时确定内存分配大小),动态管理内存提供了更大的灵活性和控制能力。
C++中的动态内存管理通过new和delete关键字进行手动管理,同时智能指针提供了更安全和高效的方式来管理动态分配的内存,帮助开发者避免内存泄漏等问题。
相比C语言中的malloc分配内存,new不仅分配内存还进行了初始化。
Java也使用了new来进行动态内存管理,但相比之下Java给出了一个方案解决内存泄漏问题—垃圾回收机制(GC),即让JVM自行判定某个内存是否不再使用,若不再使用则回收,此时就不必程序员手动写代码回收。
上面讲了Java运行时内存的各个区域,对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。
并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。
因此讲内存分配何回收关注的是Java堆与方法区这两个区域。
Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些存活,哪些已经“死去”,判断对象是否已经”死去“有如下几种算法:
1、死亡对象的判断算法
1)引用计数算法
每个对象都有一个引用计数器,记录着有多少个引用指向该对象。当引用计数器为0时,表示该对象不再被任何引用指向,即认为该对象是死亡的,可以被垃圾回收器回收
实现简单,对垃圾对象的回收比较及时;但是难以处理循环引用,会导致引用计数器无法归零,从而造成内存泄漏
2)可达性分析算法
通过一组称为”GC Roots"的根对象作为起始点,从这些根对象开始往下搜索,可达的对象被认为是存活的,不可达的对象则是被判断为死亡,可以被回收
GC Roots:包括线程栈(本地变量表)、静态变量、常量池中的引用,它们保证了对象之间的可达性链条
能够有效处理循环引用的问题,不会导致内存泄漏;但是相对于计数算法,实现和运行时开销较大
在Java中,主流的垃圾回收器(如Serial, Parallel, CMS, G1等)通常使用可达性分析算法来判断对象的存活状态,这种算法能够更精确地确定不再使用的对象,从而安全有效地回收内存资源,提高程序性能和可靠性。
2、垃圾回收算法
1)标记-清除法
首先从根节点出发标记所有可达对象,再清除所有未标记对象(即不可达对象)
简单粗暴,适用于处理大对象和长时间存活的对象,但可能导致内存碎片化,影响内存分配效率
2)复制算法
将堆内存分为两块,每次只使用其中一块,当其中一块内存被占满时,将存活对象复制到另一块内存中,然后清除原内存中的所有对象
适用于处理短生命周期的对象,能够有效避免内存碎片化,但会消耗一定的内存空间
3)标记-整理法
类似于标记-清除法,但在清除阶段会将存活对象向一端移动,然后直接清除边界外的所有对象。这样可以保持存活对象的持续性,减少内存碎片化
适用于处理长时间存活对象和避免内存碎片化,但需要额外的移动存活对象的操作
4)分代算法
根据对象的存货周期将堆内存划分为不同的代,通常分为新生代和老生代。针对不同代使用不同的垃圾回收算法,如新生代使用复制算法,老生代使用标记-清除或标记-整理算法
充分利用对象的存活特性,提高垃圾回收效率,减少对整个内存的扫描次数
在实际应用中,Java的垃圾回收器通常会根据应用程序的特性和运行环境的不同,结合多种垃圾回收算法,采用分代收集和并发收集等技术,以达到最佳的垃圾回收效果。不同的垃圾回收器(如Serial, Parallel, CMS, G1等)会选择不同的算法组合和优化策略,以满足不同的性能和响应时间需求。