JVM的概括
JVM 的主要功能包括:
-
字节码执行:JVM 可以加载、解释和执行 Java 编译后的字节码文件(.class 文件),将字节码翻译成机器码并执行。
-
内存管理:JVM 负责 Java 程序的内存分配和回收。它包括堆内存、方法区、虚拟机栈、本地方法栈等内存区域的管理,以及垃圾收集器的工作。
-
垃圾回收:JVM 自动管理内存中不再使用的对象,通过垃圾收集器(Garbage Collector)定期回收这些对象,释放内存空间,防止内存泄漏。
-
即时编译:JVM 通过即时编译器(Just-In-Time Compiler,JIT)将字节码编译成本地机器码,以提高程序的执行效率。
-
线程管理:JVM 负责创建、管理和调度线程,包括 Java 线程和本地线程。
-
安全机制:JVM 提供安全管理器(Security Manager)来保护 Java 程序免受恶意代码攻击。
-
异常处理:JVM 提供异常处理机制来捕获和处理程序运行时的异常,保证程序的健壮性和可靠性。
-
类加载:JVM 负责加载 Java 类文件,并将其转换为运行时数据结构,包括类、接口、方法等,并进行链接和初始化操作。
-
性能监控和调优:JVM 提供了丰富的性能监控工具和调优参数,可以监控程序的运行状态,优化程序的性能和资源利用率。
-
多语言支持:除了 Java 语言之外,JVM 还支持运行其他编程语言的程序,如 Kotlin、Scala、Groovy 等,通过相应的编译器将其转换为字节码运行在 JVM 上。
字节码文件的组成
基本信息
常量池
Java 字节码文件中的常量池(Constant Pool)是一种特殊的数据结构,用于存储编译时生成的各种字面量和符号引用。常量池在类加载时被加载到内存中,并被存储在方法区(Method Area)中,它包含了以下内容:
-
字面量(Literal):包括整数、浮点数、字符、字符串等字面量值,如
int
、float
、char
、String
等。字面量可能会用在字段名或字符串中。 -
符号引用(Symbolic Reference):包括类和接口的全限定名、字段和方法的名称和描述符、方法句柄、方法类型等符号引用信息。
常量池的作用包括:
-
节省内存空间:常量池可以避免存储重复的字面量和符号引用,节省内存空间。对于相同的字面量或符号引用,只会在常量池中存储一份,其他的地方引用该字面量或符号引用时,只需要引用常量池中的索引即可。
-
提高性能:常量池中存储的字面量和符号引用可以被多个类或方法共享,避免了重复创建和加载。这样可以提高程序的运行效率和性能。
-
支持动态语言特性:常量池中存储的符号引用信息可以在运行时动态解析,支持 Java 的动态语言特性,如反射、动态代理等。
-
支持类的动态加载:在 Java 的类加载过程中,常量池中存储的符号引用信息可以被动态加载的类使用,支持类的动态加载和替换。
方法
Java 字节码文件中的方法(Method)是类文件的一个重要组成部分,它包含了类中定义的方法的相关信息,包括方法的名称、参数列表、返回类型、访问修饰符、异常信息、方法体等。
每个方法都以方法描述符(Method Descriptor)的形式存储在方法区(Method Area)中。方法描述符包含了方法的名称、参数列表和返回类型的描述符,用于唯一标识一个方法。
通过字节码指令分析下面三种“加一”的操作性能的高低 ?
int i=0,j=0,k=0,i++;j=j+ 1;k += 1;
i++和k += 1和相同
0: iconst_0 // 将常量0压入操作数栈
1: istore_1 // 将栈顶的值存储到局部变量表的索引为1的位置(i=0)
2: iinc 1, 1 // 局部变量表索引为1的变量自增1
j = j + 1:
0: iconst_0 // 将常量0压入操作数栈
1: istore_2 // 将栈顶的值存储到局部变量表的索引为2的位置(j=0)
2: iload_2 // 将局部变量表索引为2的变量压入操作数栈
3: iconst_1 // 将常量1压入操作数栈
4: iadd // 将栈顶的两个值相加,并将结果压入操作数栈
5: istore_2 // 将栈顶的值存储到局部变量表的索引为2的位置(j = j + 1)
工具
javap
是 JDK 中的一个命令行工具,用于反编译 Java 类文件,查看类的字节码信息。它可以将编译后的 Java 类文件反编译成可读性较高的字节码指令,从而帮助开发人员了解类的内部结构、方法、字段等信息。
基本用法:javap ClassName
Arthas(阿尔萨斯)是阿里巴巴开源的一款 Java 应用性能诊断工具,它提供了丰富的命令行工具和 Web 控制台,用于实时监控、诊断和调试 Java 应用程序。Arthas 可以帮助开发人员快速定位和解决应用程序中的性能问题、内存泄漏、线程问题等。
- 本地文件可以使用jclasslib工具查看,开发环境使用jiclasslib插件。
- 服务器上文件使用javap命令直接查看,也可以通过arthas的dump命令导出字节码文件再查看本地文件。还可以使用jad命令反编译出源代码
类的生命周期
-
加载(Loading): 加载阶段是指将类的字节码从磁盘或其他介质加载到 JVM 内存中的过程。在加载阶段,类加载器负责根据类的全限定名查找字节码文件,并将其读取到内存中。加载阶段的工作包括:
- 通过类的全限定名查找字节码文件。以二进制流的方式获取字节码信息。
- 创建类的 Class 对象,并将字节码加载到 JVM 内存中。
- 对字节码进行验证、准备和解析等操作。
-
链接(Linking): 链接阶段是指将类的字节码与 JVM 内部的运行时数据结构关联起来的过程。链接阶段分为三个阶段:
- 验证(Verification):验证字节码文件的结构、语义和符号引用是否符合 Java 虚拟机规范,防止恶意代码攻击。
- 准备(Preparation):为类的静态变量分配内存空间,并初始化默认值。
准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值。final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。在Java中,非静态的全局变量(成员变量)是在对象被创建时实例化的,它们存储在对象的堆内存中。而静态的全局变量(类变量)是在类被加载到方法区时被初始化的,它们存储在方法区中。(当类被加载到内存中时,非静态的成员变量并没有被初始化,它们只是类的组成部分之一。只有当类的对象被创建时,非静态的成员变量才会被实例化并分配内存空间,然后随着对象的生命周期一起存在。))
- 解析(Resolution):将类中的符号引用转换为直接引用(当符号引用引用其他类的方法时,并不会转化为直接引用,这是因为在解析阶段,虚拟机只负责解析当前类(被调用方法所在的类)中的符号引用,而不负责解析其他类中的符号引用。只有当前类中的方法被调用时,虚拟机才会进行解析,将方法的符号引用转换为直接引用,而被调用方法所在的其他类中的符号引用则不会在这个过程中被解析。因此,被调用方法所在的类中的符号引用会保持为符号引用,直到该方法被实际调用时,虚拟机才会进行解析,并将方法的符号引用转换为直接引用。这样做的好处是可以延迟解析的时机,节省系统资源,并且提高了解析的效率。),确保类与其他类和接口的关联关系正确。符号引用就是在字节码文件中使用编号来访问常量池中的内容。直接引用不在使用编号,而是使用内存中地址进行访问具体的数据。
- 验证(Verification):验证字节码文件的结构、语义和符号引用是否符合 Java 虚拟机规范,防止恶意代码攻击。
-
类被加载到内存中:当Java虚拟机(JVM)在运行时需要使用某个类时,会先将该类的class文件加载到内存中。类的加载过程包括加载、连接和初始化三个阶段。在加载阶段,类的class文件被加载到内存中,包括类的结构信息、静态变量和静态方法等。
-
类被实例化:类被实例化是指在内存中创建该类的对象实例。当使用
new
关键字或者其他方式创建对象时,会在堆内存中为该类分配内存空间,并调用类的构造方法对对象进行初始化,从而创建对象实例。
3.初始化(Initialization): 初始化阶段是类加载过程的最后阶段,它负责执行类的初始化代码,初始化类的静态变量和静态代码块。初始化阶段是在类首次被主动使用时触发的,例如创建类的实例、访问类的静态变量或静态方法等。
<clinit>
方法是 Java 中的一个特殊方法,它用于执行类的静态初始化代码块。在 Java 虚拟机加载类的过程中,如果类包含了静态变量初始化语句或静态代码块,编译器会将这些静态初始化代码合并到 <clinit>
方法中。
<clinit>
方法由编译器自动生成,它负责初始化类的静态成员变量,以及执行静态代码块中的代码。这个方法是线程安全的,会在多线程环境下保证只被执行一次,确保了类的静态初始化的安全性。
在 Java 虚拟机加载类时,会先加载父类,然后再加载子类。在加载类的过程中,如果发现类中包含了静态初始化代码,就会执行 <clinit>
方法。这个方法在类被加载时自动执行,不需要显式调用。
以下几种方式会导致类的初始化:
- 1.访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化
final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化。实际验证中没有初始化- 2.调用Class.forName(String className)。
- 3.new一个该类的对象时。
- 4.执行Main方法的当前类。
clinit指令在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的。
- 1.无静态代码块且无静态变量赋值语句。
- 2.有静态变量的声明,但是没有赋值语句
- 3.静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
- 4.数组的创建不会导致数组中元素的类进行初始化。
访问父类中存在的静态变量,不会初始化子类;访问子类的静态变量,会先初始化父类。
添加-XX:+TraceClassLoading 参数可以打印出加载并初始化的类
5.使用(Using): 使用阶段是指程序执行过程中使用类的过程。在使用阶段,程序可以创建类的实例、调用类的方法、访问类的字段等操作。
6.卸载(Unloading): 卸载阶段是指从 JVM 内存中卸载不再需要的类的过程。当类不再被引用,并且没有任何对象引用该类时,JVM 可以通过垃圾回收机制将其从内存中卸载,释放内存空间。
类加载器
1.类加载器的作用是什么?
类加载器(ClassLoader)负责在类加载过程中的字节码获取并加载到内存这一部分。通过加载字节码数据放入内存转换成byte[ ],接下来调用虚拟机底层方法将byte[ ]转换成方法区和堆中的数据。
类加载器的详细信息可以通过Arthas中classloader命令查看
启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器。默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等 。
通过启动类加载器去加载用户jar包::荐使用参数进行扩展,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展
扩展类加载器(Extension Class Loader)默认加载 Java安装目录/jre/lib/ext 下的类文件。
推荐,使用参数进行扩展使用参数进行扩展。使用 -Djava.ext.dirs=jar包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加上原始目录
应用程序类加载器(Application Class Loader)是JDK中提供的、使用Java编写的类加载器。默认加载为应用程序classpath下的类。
自定义类加载器允许用户自行实现类加载的逻辑,可以从网络、数据库等来源加载类信息。
自定义类加载器需要继承自ClassLoader抽象类,重写findClass方法。
- 启动类加载器(Bootstrap ClassLoader): 加载JDK核心类库。
- 扩展类加载器(Extension ClassLoader): 加载扩展类库(
JAVA_HOME/lib/ext
)。 - 应用程序类加载器(Application ClassLoader): 加载应用程序类路径(
CLASSPATH
)
类的双亲委派机制
1、当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载
2、 应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。
3、双亲委派机制的好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。
打破双亲委派机制
自定义类加载器
默认情况下,自定义类加载器的父类加载器是应用程序类加载器
对Java程序员来说,打破双亲委派机制的唯一方法就是实现自定义类加载器重写loadClass方法,将其中的双亲委派机制代码去掉。实现自定义类加载器重写findClass方法。
双亲委派机制是在 loadClass 方法中实现的,loadClass 调用了 findClass 方法来实现类的加载, findClass 方法中完成自定义类加载器的加载。
Tomcat自定义类加载器
common类加载器主要加载tomcat自身使用以及应用使用的ir包,默认配置在catalina.properties文件中common.loader="${catalina.basej/lib", "${catalina.basej/lib/*.jar"。
catalina类加载器主要加载tomcat自身使用的jar包,不让应用使用,默认配置在catalina.properties文件中。server/oader= 默认配置为空,为空时catalina加载器和common加载器是同一个。
shared类加载器主要加载应用使用的jar包,不让tomcat使用,默认配置在catalina.properties文件中。shared./oader=,默认配置为空,为空时shared加载器和common加载器是同一个。
默认这里打破了双亲委派机制,应用中的类如果没有加载过。会先从当前类加载器加载,然后再交给父类加载器通过双亲委派机制加载。
线程上下文类加载器
SPI(Service Provider Interface)机制是 Java 提供的一种服务提供者接口,用于实现模块之间的解耦和动态加载。SPI 主要由三个部分组成:服务接口、服务实现类和服务提供者配置文件。
具体来说,SPI 机制的工作原理如下:
-
定义服务接口:首先,定义一个服务接口,该接口定义了一组服务提供者需要实现的方法。
-
编写服务实现类:根据服务接口,编写多个服务实现类,实现接口定义的方法。这些服务实现类可以是不同的模块或者是不同的 jar 包提供的。
-
编写服务提供者配置文件:在
META-INF/services/
目录下创建一个以服务接口的全限定名命名的文件,文件内容是具体的服务实现类的全限定名,每行一个。这个文件的名称是固定的,即services
。 -
加载服务实现类:在程序运行时,通过 Java 的 SPI 机制,可以动态地加载并实例化服务提供者。Java 提供了
ServiceLoader
类来加载服务提供者,并提供了一些方法来获取服务实现类的实例。 -
动态注册和加载服务:在需要使用服务的地方,通过
ServiceLoader
类加载对应的服务接口,然后调用其方法。ServiceLoader
会根据配置文件中的信息,动态地实例化服务提供者,并返回对应的服务实现类的实例。
SPI 机制的优点是实现了模块之间的解耦和动态加载,使得系统更加灵活和可扩展。它允许程序在运行时根据需要加载服务实现类,无需在代码中显式指定具体的实现类,大大提高了系统的灵活性和可维护性。SPI 机制在 Java 中的典型应用包括 JDBC 数据库驱动加载、日志框架的扩展、XML 解析器的加载等。
JVM管理的内存(运行时数据区)
程序计数器
通常简称为 PC 寄存器,是计算机体系结构中的一种专用寄存器,用于存储当前线程正在执行的指令地址或者下一条将要执行的指令地址。在 Java 虚拟机中,每个线程都有一个独立的程序计数器,用于记录该线程当前执行的字节码指令位置。
-
存储当前指令地址:程序计数器存储了当前正在执行的字节码指令的地址。在多线程环境下,每个线程都有自己独立的程序计数器,因此可以保证线程之间的指令地址互相独立,不会相互影响。
-
指令跳转和控制流:程序计数器在执行分支、循环、方法调用等操作时起着重要作用。它记录了当前指令的地址,帮助虚拟机实现指令的跳转、分支和控制流等功能。
-
线程恢复:程序计数器在线程切换或者线程中断后能够快速恢复现场,使得线程可以从中断或者切换的位置继续执行。
-
不会导致 OutOfMemoryError(内存溢出错误):程序计数器是 Java 虚拟机中的一种轻量级寄存器,它不会导致 OutOfMemoryError 异常,因为它的内存空间是固定的,不会随着线程数量或者方法调用深度的增加而增加。程序员无需对程序计数器做任何处理。
栈
Java 虚拟机栈(Java Virtual Machine Stack)是 Java 虚拟机内存中的一块内存区域,用于存储线程的方法调用和局部变量信息。每个线程在创建时都会分配一个独立的虚拟机栈,用于存储该线程的方法调用栈帧(Stack Frame)。
-
方法调用和返回:虚拟机栈存储了线程执行方法的调用栈帧。当一个方法被调用时,虚拟机会为该方法分配一个新的栈帧,并将其压入虚拟机栈顶;当方法执行完成后,虚拟机会将该方法的栈帧弹出,恢复到调用该方法的上一个栈帧。
-
局部变量和操作数栈:每个栈帧包含了该方法的局部变量表和操作数栈。局部变量表用于存储方法的参数和局部变量,操作数栈用于存储方法执行过程中的临时数据和中间结果。
-
栈帧结构:栈帧通常由三部分组成:局部变量表、操作数栈和帧数据。局部变量表存储方法的参数和局部变量,操作数栈用于存储方法执行过程中的数据,帧数据用于存储一些额外的信息,例如动态链接、方法返回地址等。
-
线程私有:每个线程都有自己独立的虚拟机栈,栈内存的大小可以在启动时指定,并且可以动态调整。
-
栈深度限制:虚拟机栈的大小是有限的,栈帧的数量和深度受到限制。如果方法的调用深度超过了虚拟机栈的限制,就会抛出 StackOverflowError 异常;如果虚拟机栈无法扩展,就会抛出 OutOfMemoryError 异常。
栈帧结构
局部变量表
栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot),long和double类型占用两个槽,其他类型占用一个槽。
实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。
操作数栈
帧数据
动态链接
当符号引用引用其他类的方法时,并不会转化为直接引用。这是因为在解析阶段,虚拟机只负责解析当前类(被调用方法所在的类)中的符号引用,而不负责解析其他类中的符号引用。换句话说,只有当前类中的方法被调用时,虚拟机才会进行解析,将方法的符号引用转换为直接引用,而被调用方法所在的其他类中的符号引用则不会在这个过程中被解析。
所以图中的动态链接保存的是符号引用到运行时常量池之间的映射关系。
因此,被调用方法所在的类中的符号引用会保持为符号引用,直到该方法被实际调用时,虚拟机才会进行解析,并将方法的符号引用转换为直接引用。这样做的好处是可以延迟解析的时机,节省系统资源,并且提高了解析的效率。
方法出口
方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。
异常表
异常表(Exception Table)是编译后生成的 Java 字节码文件中的一部分,用于存储方法中可能抛出的异常信息以及对应的异常处理逻辑。异常表记录了每个可能抛出异常的代码块的起始位置、结束位置、异常处理器的地址等信息。
-
起始位置(Start PC):指定了可能抛出异常的代码块的起始位置(字节码指令的偏移量)。
-
结束位置(End PC):指定了可能抛出异常的代码块的结束位置(字节码指令的偏移量)。
-
处理器位置(Handler PC):指定了异常处理器的位置(字节码指令的偏移量)。如果在起始位置到结束位置范围内抛出了异常,虚拟机会跳转到指定的异常处理器进行异常处理。
-
异常类型(Catch Type):指定了要捕获的异常类型。如果该字段的值为 0,则表示要捕获所有异常(即 catch (Exception e));否则,该字段的值是对应异常类型的常量池索引。
异常表的作用是帮助虚拟机在方法执行过程中处理异常。当方法执行过程中抛出异常时,虚拟机会根据异常表中的信息决定如何处理异常:首先,虚拟机会遍历异常表,查找与当前抛出异常匹配的异常处理器;然后,虚拟机会跳转到对应的异常处理器位置,并执行异常处理逻辑。如果在异常表中找不到匹配的异常处理器,则异常会传递给调用者,并继续向上层方法传递,直至找到合适的异常处理器或者抛出未捕获异常。
本地方法栈
本地方法栈(Native Method Stack)是 Java 虚拟机中的一块内存区域,用于存储执行本地方法(Native Method)的相关信息。与虚拟机栈类似,每个线程在执行本地方法时都会分配一个独立的本地方法栈。本地方法栈的主要作用和特点包括:
-
执行本地方法:本地方法栈用于存储执行本地方法时的相关信息,包括方法的参数、局部变量、返回值等。本地方法是用本地语言(如 C、C++)编写的方法,在 Java 中通过 JNI(Java Native Interface)调用执行。
-
与虚拟机栈的关系:本地方法栈与虚拟机栈类似,但是它们存储的信息不同。虚拟机栈存储 Java 方法的调用信息和局部变量,而本地方法栈存储本地方法的调用信息和局部变量。
-
线程私有:每个线程都有自己独立的本地方法栈,用于执行本地方法时存储相关信息。本地方法栈的大小可以在启动时指定,并且可以动态调整。
-
不会导致 StackOverflowError:本地方法栈是 Java 虚拟机中的一种轻量级栈,它不会导致 StackOverflowError 异常,因为它的内存空间是固定的,不会随着线程数量或者方法调用深度的增加而增加。
栈溢出
堆
Java 虚拟机中的堆(Heap)是用于存储对象实例的一块内存区域,是 Java 程序中最主要的内存区域之一。堆是所有线程共享的,用于存放所有通过 new
关键字创建的对象实例和数组对象。
-
对象存储:堆用于存储所有的对象实例。当程序调用
new
关键字创建对象时,对象将被分配在堆中,并返回对象的引用。 -
动态分配:堆是动态分配的,它的大小可以在程序运行时根据需要动态调整。Java 虚拟机会根据堆的使用情况自动进行垃圾回收,并在需要时扩展或收缩堆的大小。
-
垃圾回收:Java 堆是垃圾回收的主要区域之一。在堆中分配的对象在不再被引用时,将会被垃圾回收器自动回收,释放内存空间。
-
分代结构:Java 堆通常分为新生代(Young Generation)、老年代(Old Generation)和永久代(PermGen)等不同的区域。不同代的对象具有不同的生命周期和特点,可以采用不同的垃圾回收算法和策略。
-
内存管理:Java 堆的大小可以通过启动参数
-Xmx
和-Xms
来指定,分别表示堆的最大和初始大小。Java 堆中还会根据对象的大小采用不同的分配策略,如对象的分配可以通过指针碰撞(Bump the Pointer)或者空闲列表(Free List)等方式进行。
Java服务端程序开发时,建议将-Xmx和-Xms设置为相同的值,这样在程序启动之后可使用的总内存就是最大内存,而无需向java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况。
方法区
方法区(Method Area)是 Java 虚拟机规范中定义的一块内存区域,用于存储类的结构信息、常量、静态变量、即时编译器编译后的代码等数据。在 Java 虚拟机规范中,方法区被描述为堆的一个逻辑部分,但在实际实现中,方法区通常是独立于堆的一块内存区域。
-
存储类的结构信息:方法区存储了每个类的结构信息,包括类的名称、访问修饰符、父类、接口列表、字段信息、方法信息等。
-
存储常量池:方法区存储了类的常量池(运行时常量池和字符串常量池),包括编译期生成的字面量、符号引用等。常量池中的信息用于支持类加载过程中的解析操作,例如将符号引用转换为直接引用。
-
存储静态变量:方法区存储了类的静态变量,这些变量在类的生命周期中只会被初始化一次,并且在类的所有实例中共享。
-
存储即时编译器编译后的代码:方法区存储了即时编译器(Just-In-Time Compiler,JIT)编译后的本地机器代码(Native Code),用于提高程序的执行效率。
-
垃圾回收:方法区中的数据是不断变化的,因此也需要进行垃圾回收。在一些早期的 Java 虚拟机实现中,方法区的垃圾回收由 Full GC 负责,但是在现代的 Java 虚拟机中,方法区通常会采用不同的垃圾回收算法和策略,例如采用分代垃圾回收算法,将方法区划分为新生代和老年代。
需要注意的是,方法区是线程共享的内存区域,在程序启动时被创建,并且在虚拟机退出时被销毁。方法区的大小可以通过启动参数 -XX:MaxMetaspaceSize
来指定,而在较早版本的 Java 虚拟机中,方法区的大小可以通过 -XX:MaxPermSize
来指定。
字符串常量池
常量池中的字符串仅是符号,第一次用到时才变为对象
如果没有变量参与,都是字符串直接相加,编译之后就是拼接之后的结果,会复用串池中的字符串如果有变量参与,每一行拼接的代码,都会在内存中创建新的字符串,浪费内存。
字符串拼接的时候,如果有变量:
JDK8以前:系统底层会自动创建一个StringBuilder对象,然后再调用其append方法完成拼接拼接后,再调用其toString方法转换为String类型,而toString方法的底层是直接new了一个字符串对象。
JDK8版本:系统会预估要字符串拼接之后的总大小,把要拼接的内容都放在数组中,此时也是产生一个新的字符串。
去·····
在 JDK 1.6 和 JDK 1.8 中,intern()
方法的行为有一些微妙的差异。
JDK 1.6 中的 intern()
在 JDK 1.6 中,如果调用字符串对象的 intern()
方法,会首先检查字符串常量池中是否已经存在该字符串对象的引用。如果字符串常量池中已经存在该字符串对象,则返回常量池中的引用;如果字符串常量池中不存在该字符串对象,则将该字符串对象添加到字符串常量池中,并返回常量池中的引用。
JDK 1.8 中的 intern()
在 JDK 1.8 中,intern()
方法的行为有一些改变。JDK 1.8 对字符串常量池的实现进行了优化,将字符串常量池从永久代移动到了堆内存中。因此,在 JDK 1.8 中,调用字符串对象的 intern()
方法时,如果字符串常量池中已经存在该字符串对象的引用,则直接返回常量池中的引用;如果字符串常量池中不存在该字符串对象,则将该字符串对象的引用复制到字符串常量池中,并返回复制后的引用。
总结
在 JDK 1.8 中,由于字符串常量池的实现方式发生了变化,intern()
方法的行为也发生了变化。在 JDK 1.8 中,intern()
方法的性能通常会比 JDK 1.6 更好,因为它避免了频繁地将字符串对象复制到字符串常量池中。然而,对于某些特殊情况,如大量使用字符串的场景下,intern()
方法可能会导致字符串常量池中的引用过多,从而增加堆内存的占用。因此,在使用 intern()
方法时,需要根据具体情况进行评估和选择。
直接内存(Direct Memory)
![](https://i-blog.csdnimg.cn/blog_migrate/cb3023177e4146e905c136ea27a7fd1a.png)
- 直接内存并非 JVMS 定义的标准 Java 运行时内存。
- JDK1.4 加入了新的 NIO 机制,目的是防止 Java 堆 和 Native 堆之间往复的数据复制带来的性能损 耗,此后 NIO 可以使用 Native 的方式直接在 Native 堆分配内存。
- 直接内存区域是全局共享的内存区域。
- 直接内存区域可以进行自动内存管理(GC),但机制并不完善。
- 本机的 Native 堆(直接内存) 不受 JVM 堆内存大小限制。可能出现 OutOfMemoryError 异常。
垃圾回收
在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。我们称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收。
内存泄漏
指的是不再使用的对象在系统中未被回收(未被使用的内存无法被释放),内存泄漏的积累可能会导致内存溢出。内存泄漏通常发生在以下情况下:
-
未释放资源:程序中未正确释放使用过的资源,如打开的文件、数据库连接、网络连接等。如果这些资源未被关闭或释放,就会导致内存泄漏。
-
对象引用保留:对象被长时间持有引用,但实际上已经不再需要,这样垃圾回收器无法回收这些对象。这种情况常见于缓存、集合、监听器等场景。
-
静态集合:在静态集合中保存了大量对象引用,导致这些对象无法被垃圾回收器回收。
-
循环引用:两个或多个对象之间形成循环引用,即彼此持有对方的引用,使得这些对象无法被正常回收。
-
线程未关闭:未正确关闭线程或者线程池,使得线程一直处于活动状态,无法被回收。
内存泄漏对系统的影响主要体现在内存占用过高、系统性能下降、甚至导致系统崩溃等方面。为了避免内存泄漏问题,需要加强对程序的设计、编码和测试,尽量避免上述情况的发生。另外,使用内存监控工具进行内存分析和内存泄漏检测也是一种有效的手段。
线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存。
禁止在代码中使用system.gc(),System.gc()可能会引起FULLGC,在代码中尽量不要使用。使用DisableExplicitGc参数可以禁止使用System.gc()方法调用。
方法区回收
方法区中能回收的内容主要就是不再使用的类判定一个类可以被卸载。需要同时满足下面三个条件:
1、此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象
2、加载该类的类加载器已经被回收。
3、该类对应的 java.lang.Class 对象没有在任何地方被引用。
如果想要查看垃圾回收的信息,可以使用-verbose:gc参数。
一般不需要回收,JSP等技术会通过回收类加载器去回收方法区中的类
堆回收
Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。
引用计数法和可达性分析法
Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GCRoot)和普通对象,对象与对象之间存在引用关系。
Java 中的引用类型有五种,分别是强引用、软引用、弱引用、虚引用和 Phantom 引用。
-
强引用(Strong Reference):
- 强引用是 Java 中最常见的引用类型。
- 当一个对象被强引用引用着时,即使系统内存不足,垃圾回收器也不会回收该对象。
- 只有当强引用不再被使用(例如被赋值为 null)时,垃圾回收器才会回收该对象。
-
软引用(Soft Reference):
- 软引用用于描述那些并非必须保留在内存中的对象。
- 当系统内存不足时,垃圾回收器可能会回收被软引用引用着的对象。
- 软引用通常用于实现对内存敏感的缓存功能。
-
弱引用(Weak Reference):
- 弱引用也用于描述那些并非必须保留在内存中的对象。
- 弱引用的生命周期比软引用更短,即使系统内存不紧张,垃圾回收器也可能随时回收被弱引用引用着的对象。
- 弱引用通常用于实现缓存功能,但是比软引用的生命周期更短。
-
虚引用(Phantom Reference):
- 虚引用是 Java 中最弱的引用类型。
- 虚引用并不能通过 get() 方法获取被引用的对象,只是用于检测对象是否被垃圾回收器回收。
- 当一个对象被虚引用引用着时,垃圾回收器可能会在回收该对象之前,将该引用添加到一个 ReferenceQueue 中。
-
Phantom 引用(PhantomReference):
- Phantom 引用是虚引用的子类,也被称为幽灵引用。
- 当一个对象被 Phantom 引用引用着时,该对象的 finalize() 方法已经被调用过,且垃圾回收器已经准备好将其回收。
- 虚引用通常与 ReferenceQueue 配合使用,用于执行某些清理操作。
强引用(Strong Reference)
是Java中最普遍、也是默认的引用类型。当一个对象具有强引用时,即使内存空间不足时,垃圾回收器也不会回收该对象,而会抛出 OutOfMemoryError 异常。
在Java中,大部分的对象引用都是强引用。例如,通过 new
关键字创建的对象,或者通过赋值操作符 =
分配的对象引用都是强引用。当一个对象具有强引用时,只有当该引用被显式地置为 null,或者该引用超出了引用的作用域,才会被垃圾回收器回收。
软引用(Soft Reference)
是 Java 中一种相对较弱的引用类型,用于描述那些并非必须保留在内存中的对象。当 Java 虚拟机在进行垃圾回收时,如果发现一个对象只被软引用引用着,且系统内存不足时,就会将该对象回收。软引用通常用于实现对内存敏感的缓存功能。
虽然软引用可以提高内存使用效率,但过度地依赖软引用可能会导致内存泄漏的问题。因此,在使用软引用时,需要根据具体情况合理设置软引用对象的生命周期,并注意监控系统内存的使用情况,避免因为软引用导致的内存溢出问题。
弱引用(Weak Reference)
是 Java 中一种比较弱的对象引用类型,它允许被引用的对象被垃圾回收器回收,即使该对象还存在弱引用。
在 Java 中,弱引用主要由 java.lang.ref.WeakReference
类来表示,可以通过该类来创建弱引用对象。弱引用对象在被垃圾回收器回收时,会自动释放对原始对象的引用,因此无需手动释放。
弱引用在某些场景下非常有用,但需要注意的是,由于弱引用的特性,当对象被回收后,弱引用的引用值会被置为 null。因此,在使用弱引用时,需要谨慎处理弱引用为空的情况,以避免空指针异常。
WeakHashMap 内部是通过弱引用来管理
entry
的,将一对key, value
放入到 WeakHashMap 里并不能避免该key
值被GC回收,除非在 WeakHashMap 之外还有对该key
的强引用。WeakHashMap 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。
当threadlocal对象不再使用时,threadlocal =null,使用弱引用可以让对象被回收;因为仅有弱引用没有强引用的情况下,对象是可以被回收的。由于 value
是一个强引用,它会一直存在于内存中,直到这个 Entry
被移除。如果 ThreadLocal
的实例被垃圾回收器回收了,Entry
中的 key
将变为 null
,但 value
仍然会保留在内存中。这时,如果不手动移除这个 Entry
,可能会造成内存泄漏,因为这个 value
对象无法被GC回收。
弱引用并没有完全解决掉对象回收的问题,Entry对象和value值无法被回收,所以合理的做法是手动调用remove方法进行回收,然后再将threadlocal对象的强引用解除
ThreadLocal
ThreadLocal 是 Java 中一个很有用的工具类,它提供了线程局部变量的功能。每个线程都可以通过 ThreadLocal 创建一个独立的变量副本,这个变量对于该线程是局部的,在其他线程中不可见。
ThreadLocal 主要用于解决多线程环境下的变量访问冲突问题,它的特点包括:
-
线程隔离性:每个线程都拥有自己的变量副本,彼此独立,互不影响。
-
线程安全性:在多线程环境下使用 ThreadLocal 可以避免线程安全问题,不需要额外的同步措施,因为每个线程都操作自己的副本,不会发生竞态条件。
-
减少同步开销:相比于使用共享变量加锁的方式,ThreadLocal 可以减少线程间的竞争和同步开销,提高程序的并发性能。
-
适用于线程封闭模式:ThreadLocal 适用于线程封闭模式(Thread confinement),即将对象限制在单个线程中,避免对象被多个线程共享。
ThreadLocal 主要有以下几个常用方法:
get()
:获取当前线程的变量副本。set(T value)
:设置当前线程的变量副本。remove()
:移除当前线程的变量副本。initialValue()
:初始化变量的默认值,在调用get()
方法时,如果变量副本尚未创建,则会调用initialValue()
方法初始化。
ThreadLocal 常见的使用场景包括:
- 在 Web 应用中,存储当前用户的会话信息。
- 在数据库连接、事务管理等场景中,维护线程安全的数据库连接。
- 在日志打印中,记录每个线程的操作日志。
- 在线程池中,维护线程相关的上下文信息等。
需要注意的是,使用 ThreadLocal 时应当注意避免内存泄漏问题,及时清理不再需要的线程变量副本,以免造成内存浪费。
一句话理解ThreadLocal,threadlocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值,Entry(threadlocal,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
垃圾回收算法
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。
- 吞吐量数值越高,垃圾回收的效率就越高。
- 最大暂停时间越短,用户使用系统时受到的影响就越短。
- 不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
分代垃圾回收算法
分代垃圾回收算法是一种基于对象存活时间的垃圾回收策略,将堆内存划分为不同的代(Generation),并针对每一代使用不同的垃圾回收算法和策略。常见的分代垃圾回收算法通常将堆内存分为新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,或称元空间 Metaspace)等几个代。
通过分代垃圾回收算法,可以根据对象的存活时间和特性采用不同的垃圾回收策略,从而提高垃圾回收的效率和性能,降低系统的停顿时间,提高系统的吞吐量。
分代垃圾回收算法的主要思想是根据对象的存活时间来进行垃圾回收,因为大部分对象的生命周期很短暂,而只有少数对象会存活很长时间。通过将堆内存划分为不同的代,并针对每一代使用不同的垃圾回收算法和策略,可以提高垃圾回收的效率和性能。
具体来说,分代垃圾回收算法通常包括以下几个步骤:
-
对象分配:新创建的对象通常被分配到新生代中。新生代采用较快速的垃圾回收算法,如复制算法或标记-复制算法,以提高回收效率。每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
-
垃圾回收:新生代中的对象经过一段时间后,如果仍然存活,则会被晋升到老年代。老年代中的对象通常存活时间较长,采用更为稳定的垃圾回收算法,如标记-清除算法或标记-整理算法。如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。
-
Full GC:对整个堆内存进行垃圾回收的操作称为 Full GC(Full Garbage Collection)。Full GC 会对所有代进行垃圾回收,包括新生代、老年代和永久代等。Full GC 操作通常比增量式垃圾回收更为耗时,因此尽量减少 Full GC 的频率可以提高系统的性能。
分代GC算法将堆分成年轻代和老年代主要原因有:
- 1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。
- 2、新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。
- 3、分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(fullgc),STW时间就会减少。
其他回收算法
标记清除算法的核心思想分为两个阶段:
- 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
- 清除阶段,从内存中删除没有被标记也就是非存活对象。
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:1.碎片化问题2.分配速度慢
标记整理算法核心思想分为两个阶段:
- 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
- 整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。
优点:内存使用效率高,不会发生碎片化,在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
缺点:整理阶段的效率不高
复制算法的核心思想:
- 准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)
- 在垃圾回收GC阶段,将From中存活对象复制到To空间,然后将From空间直接清空。
- 将两块空间的From和To名字互换。
优点:吞吐量高,复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动,不会发生碎片化,复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
缺点:可用内存变小:内存使用效率低,每次只能让一半的内存空间来为创建对象使用
垃圾回收器
JDK9后默认G1垃圾回收器
G1(Garbage-First)垃圾回收器是 Java HotSpot 虚拟机中的一种全新的垃圾回收器,于 JDK 7 Update 4 引入,并在 JDK 9 中成为默认的垃圾回收器。G1 垃圾回收器旨在解决 CMS 垃圾回收器在内存碎片问题上的一些缺陷,并提供更加可预测的停顿时间、更高的吞吐量和更好的内存利用率。
-
分代收集:与传统的分代垃圾回收器类似,G1 将堆内存划分为多个代(包括 Eden、Survivor、Old、Humongous 等),但不同于传统的分代回收器,G1 不是严格地基于年轻代和老年代,而是通过区域化的方式管理内存。
-
Region-Based收集:G1 将整个堆内存划分为多个大小相等的区域(Region),每个区域可以是 Eden 区、Survivor 区、Old 区或 Humongous 区等。G1 围绕这些区域进行内存分配和垃圾回收,从而减少了内存碎片。Region的大小通过堆空间大小2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m指定(其中32m指定region大小为32M),Region size必须是2的指数幕,取值范围从1M到32M。
-
可预测的停顿时间:G1 垃圾回收器通过智能地使用并行性、并发性和回收优先级等技术,可以提供可预测的停顿时间。通过控制全局并发标记周期的长度,G1 垃圾回收器可以在可接受的时间内完成垃圾回收。G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。比如 -XX:MaxGCPauseMilis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region
-
增量式回收:G1 垃圾回收器采用增量式回收的方式来降低垃圾回收造成的停顿时间。在并发标记阶段,G1 垃圾回收器会在每个堆区域中执行一部分工作,而不是在整个堆上进行标记。
-
混合收集:G1 垃圾回收器支持混合收集(Mixed Collections),即在 Full GC 之外的并发阶段进行部分老年代区域的收集,从而降低 Full GC 的频率和停顿时间。
-
MixedGC有两种方式触发,一种是大对象直接入h,另一种走晋升。如果是走晋升,youngGC是MixedGC的前置,也就是g1应该优先younggc,毕竟mixedgc中的并发标记是依赖于younggc作初始标记的。younggc之后,正常系统绝大多数对象会被回收掉,满足晋升条件的对象晋升到老年代,这个时候才会去检测45%这个阈值,那很有可能由于youngc的对象释放,这个阈值已经不满足条件了。
年轻代回收(Young GC),回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数-XX:MaxGCPauseMiis=n(默认200)设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。
垃圾回收流程:
1、新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC。
2、标记出 Eden 和 Survivor 区域中的存活对象,
3、根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。
4、后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
5、当某个存活对象的年龄到达阈值(默认15),将被放入老年代。
6、部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。
7、多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时
(-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。
如果清理过程中发现没有足够的空Region存放转移的对象,会出现FullGC。单线程执行标记-整理算法此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。
年轻代回收原理
1、GC Root扫描,将所有的静态变量、局部变量扫描出来。
2、处理脏卡队列中的没有处理完的信息,更新记忆集的数据,此阶段完成后,记忆集中包含了所有老年代对当前Region的引用关系。
3、标记存活对象。记忆集中的对象会加入到GC Root对象集合中,在GC Root引|用链上的对象也会被标记为存活对象
4、根据设定的最大停顿时间,选择本次收集的区域,称之为回收集合Collection Set。
5、复制对象:将标记出来的对象复制到新的区中,将年龄加1,如果年龄到达15则晋升到老年代。老的区域内存直
接清空。
6、处理软、弱、虚、终结器引用,以及JNI中的弱引用。
老年代回收原理
JDK8默认垃圾回收器PS+PO
解决内存泄露-待补充
常见的JVM参数
栈上的数据存储
有点像字节对齐,但又不相同
堆上的数据存储
对象头中一般包含两个部分:
标记字(mark word),占用一个机器字,也就是8字节。
类型指针,占用一个机器字,也就是8个字节。
标记字段
在32位的Java虚拟机中,一个地址引用通常占用4个字节(32位),而在64位的Java虚拟机中,一个地址引用通常占用8个字节(64位)。
指针压缩
主要应用于64位的Java虚拟机环境下,因为64位系统下的对象引用通常会占用更多的内存空间(通常是8个字节),而在实际的应用中,很多对象的地址并不需要那么多的空间来表示。
指针压缩通过缩小对象引用的存储空间,可以减少Java堆的内存占用,从而提高应用程序的性能和降低内存占用。如果堆内存小于32GB,JVM默认会开启指针压缩,则指针只占用4个字节。
内存对齐,字段重排序
内存对齐(Memory Alignment)是一种优化技术,用于提高数据访问的效率和系统的性能。在计算机系统中,数据存储在内存中时,通常会按照一定的规则对齐到内存地址的某个边界上,而不是随意存放在内存中的任意位置。
提高数据访问效率:当数据按照一定的规则对齐到内存地址的边界上时,可以使得数据的访问更加高效。
减少内存访问次数:当数据按照对齐规则存储时,可以保证每次访问都是按照整数个字节进行访问的,而不需要额外的访问操作。
优化内存屏障的效果:在多线程编程中,内存对齐可以优化内存屏障的效果。内存屏障用于保证内存的一致性和可见性,而内存对齐可以使得数据的存储更加紧凑,从而减少内存屏障的开销。
- 子类继承自父类的属性,先记录父类的属性,属性的偏移量和父类是一致的。
- Java 虚拟机字段填充保证对象能被8字节整除
- 字段重排序,保证字段OFFSET能被类型长度整除
方法调用原理
方法调用的本质是通过字节码指令的执行,能在栈上创建栈帧,并执行调用方法中的字节码执行
静态绑定(Static Binding)
是指在编译时确定方法调用的具体实现。在静态绑定中,方法调用的目标是根据编译时的类型来确定的,而不是根据实际对象的类型。
静态绑定的优点是可以提高程序的执行效率,因为方法调用的目标在编译时已经确定,无需在运行时进行查找。然而,静态绑定的缺点是不具备动态性,无法根据实际对象的类型进行方法调用,因此无法实现多态和动态绑定的特性。
动态绑定(Dynamic Binding)
是指在运行时根据对象的实际类型确定方法调用的具体实现。动态绑定是面向对象编程中多态性的一种体现,它使得程序能够根据对象的实际类型来调用相应的方法,而不是根据变量的声明类型来确定方法调用。
动态绑定的实现通常是通过虚方法表(Virtual Method Table)来实现的。虚方法表是每个类(或者说是每个对象)中保存了方法地址的表,其中存储了该类中所有虚方法的地址。当程序调用一个虚方法时,实际上是通过对象的指针访问虚方法表,然后根据对象的实际类型找到对应方法的地址,最终调用具体的方法实现。
非虚方法(Non-virtual Method)和虚方法(Virtual Method)是面向对象编程中的两种方法调用方式,它们的区别主要体现在方法调用的动态性。
-
非虚方法:
- 非虚方法是指在编译时已经确定了具体调用的方法实现,无论方法调用时使用的是哪个对象,都会调用到该方法的具体实现。非虚方法通常包括final方法、static方法和private方法等,它们在编译时就已经确定了调用的目标。
- 非虚方法的调用是静态绑定的,即在编译时就确定了调用的具体实现,无法根据实际对象的类型进行动态调度。
-
虚方法:
- 虚方法是指在编译时无法确定具体调用的方法实现,而是需要在运行时根据对象的实际类型进行动态调度。当调用一个虚方法时,实际调用的是对象的实际类型所对应的方法实现,而不是编译时声明的类型。
- 虚方法的调用是动态绑定的,即在运行时根据对象的实际类型确定调用的具体实现,具有多态性。
在Java中,除了被声明为final、static、private的方法之外,其他方法默认都是虚方法。
异常捕获原理
在Java中,程序遇到异常时会向外抛出,此时可以使用try-catch捕获异常的方式将异常捕获并继续让程序按程序员设计好的方式运行。比如如下代码:在try代码块中如果抛出了Exception对象或者子类对象,则会进入catch分支。异常捕获机制的实现,需要借助于编译时生成的异常表。
异常表(Exception Table)是编译后生成的 Java 字节码文件中的一部分,用于存储方法中可能抛出的异常信息以及对应的异常处理逻辑。异常表记录了每个可能抛出异常的代码块的起始位置、结束位置、异常处理器的地址等信息。
-
起始位置(Start PC):指定了可能抛出异常的代码块的起始位置(字节码指令的偏移量)。
-
结束位置(End PC):指定了可能抛出异常的代码块的结束位置(字节码指令的偏移量)。
-
处理器位置(Handler PC):指定了异常处理器的位置(字节码指令的偏移量)。如果在起始位置到结束位置范围内抛出了异常,虚拟机会跳转到指定的异常处理器进行异常处理。
-
异常类型(Catch Type):指定了要捕获的异常类型。如果该字段的值为 0,则表示要捕获所有异常(即 catch (Exception e));否则,该字段的值是对应异常类型的常量池索引。
异常表的作用是帮助虚拟机在方法执行过程中处理异常。当方法执行过程中抛出异常时,虚拟机会根据异常表中的信息决定如何处理异常:首先,虚拟机会遍历异常表,查找与当前抛出异常匹配的异常处理器;然后,虚拟机会跳转到对应的异常处理器位置,并执行异常处理逻辑。如果在异常表中找不到匹配的异常处理器,则异常会传递给调用者,并继续向上层方法传递,直至找到合适的异常处理器或者抛出未捕获异常。
当一个异常发生时
- 1.JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理
- 2.如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。
- 3.如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目
- 4.如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作。
- 5.如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止。
- 6.如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行。
以上就是JVM处理异常的一些机制。
Java 基础 - 异常机制详解 | Java 全栈知识体系 (pdai.tech)
JIT即时编译器
在Java中,JT即时编译器是一项用来提升应用程序代码执行效率的技术。字节码指令被Java 虚拟机解释执行,如果有一些指令执行频率高,称之为热点代码,这些字节码指令则被IT即时编译器编译成机器码同时进行一些优化,最后保存在内存中,将来执行时直接读取就可以运行在计算机硬件上了。
C1即时编译器和C2即时编译器都有独立的线程去进行处理,内部会保存一个队列,队列中存放需要编译的任务般即时编译器是针对方法级别来进行优化的,当然也有对循环进行优化的设计。
方法内联
逃逸分析
标量替换(Scalar Replacement)是Java虚拟机的一种优化技术,用于将对象的成员变量(scalar)直接替换成相应的基本类型或者其他对象,从而避免了对象的创建和销毁过程,减少了内存分配和垃圾回收的开销。
简单八股
回答思路:JVM的定义,作用,功能,组成
回答思路:查看字节码文件常用工具,字节码文件的组成,深入学习JVM基础
回答思路:从现象到本质回答