Skip to content

JVM

JVM 的主要组成部分及其作用?

JVM(Java Virtual Machine)是Java程序的运行环境,它是一个抽象的计算机,可以在物理计算机上运行Java字节码。JVM的主要组成部分及其作用可以归纳如下:

  1. 类加载器(Class Loader)

    作用:负责加载Java字节码文件,并将其转换为可以执行的类。

    细节:类加载器只管加载,只要符合文件结构就加载,至于能否运行,则由执行引擎负责。类加载器根据指定的全限定名称将class文件加载到JVM内存,然后再转化为class对象。

  2. 执行引擎(Execution Engine)

    作用:负责执行加载的字节码文件。通常会将字节码解释成机器码并执行,也有可能使用即时编译(JIT)技术将字节码直接编译成本地机器码执行。

    细节:执行引擎也叫解释器,负责解释命令,交由操作系统执行。它将字节码翻译成底层系统指令,再交由CPU去执行。

  3. 运行时数据区域(Runtime Data Areas)

    作用:存储Java程序运行时的数据,是JVM的内存。

    细节:

    ​ 堆(Heap):线程共享,用于存储绝大多数对象实例和数组。java堆可用-Xms和-Xmx进行内存控制

    ​ 栈(Stack):用于存储方法调用和局部变量。虚拟机栈中执行方法的时候,都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

    ​ 方法区(Method Area):线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JDK1.7把放在字符串常量池、静态变量移到堆中,JDK8开始使用Native Memory 来实现方法区。

    ​ 本地方法栈(Native Method Stack):与虚拟机栈类似,但为虚拟机使用的Native方法服务。执行每个本地方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

    ​ 程序计数器(Program Counter Register):指示Java虚拟机下一条需要执行的字节码指令。

  4. 垃圾回收器(Garbage Collector)

    ​ 作用:负责自动回收不再使用的对象内存空间,释放资源。

    ​ 细节:垃圾回收器是Java自动内存管理的重要部分,能够自动检测并回收不再使用的内存,防止内存泄漏。

  5. JIT编译器(Just-In-Time Compiler)

    ​ 作用:用于优化Java字节码的执行,将频繁执行的代码编译成高效的机器码,提升程序的执行速度。

    ​ 细节:JIT编译器是JVM中的一个重要组成部分,能够在运行时将Java字节码编译成机器码,提高程序的执行效率。

  6. 本地库接口(Native Interface)

    ​ 作用:与native libraries交互,是Java与其他编程语言交互的接口。

    ​ 细节:本地接口的作用是融合不同的语言为Java所用,使得Java程序可以调用其他语言编写的库。

  7. 直接内存 (Direct Memory)
    非虚拟机运行时数据区的一部分,不是 Java 虚拟机规范 中定义的内存区域。NIO 引入了一种基于 通道(Channel)与缓冲区的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。NIO避免了在 Java 堆和 Native 堆中来回复制数据,显著提高了性能。

程序计数器是什么?

程序计数器(Program Counter,简称PC)是计算机处理器中的一个重要寄存器,其作用主要如下:

  1. 基本功能

    程序计数器用于存放下一条指令所在单元的地址。在程序执行过程中,CPU通过程序计数器来确定下一条需要执行的指令的地址。

  2. 执行过程

    当执行一条指令时,CPU首先会根据程序计数器中存放的指令地址,将指令从内存中取出到指令寄存器中。

    执行完该指令后,程序计数器会根据指令的长度自动更新其值,指向下一条需要执行的指令的地址。

  3. 程序开始与复位

    在程序开始执行前,程序计数器的值会被设置为程序的第一条指令的地址。

    当计算机重启或复位时,程序计数器通常会恢复到零或某个预定义的起始地址。

  4. 线程与并发

    在多线程环境中,每个线程都有自己的程序计数器,用于记录当前线程执行的指令位置。这样,当线程切换时,可以准确地恢复到之前的执行状态。

  5. Java虚拟机(JVM)中的程序计数器

​ 在Java虚拟机中,程序计数器被称为PC寄存器(Program Counter Register)。可以看作是当前线程所执行的字节码的行号指示器。

​ 它是线程私有的,用于存储当前线程执行的字节码的行号指示器,即下一条需要执行的字节码指令的内存地址。

​ 当线程执行Java方法时,PC寄存器记录的是正在执行的虚拟机字节码指令的地址;当执行本地(Native)方法时,PC寄存器的值为空(Undefined)。

​ 是唯一在虚拟机规范中没有规定内存溢出情况的区域。

  1. 数字信息

    在ARM处理器中,R15被用作程序计数器(PC),它是一个32位的寄存器,可以寻址4GB的地址空间(2^32 = 4G)。

综上所述,程序计数器是计算机处理器中用于存储下一条指令地址的重要寄存器,它对于保证程序的连续执行、实现线程切换等功能起着关键作用。在Java虚拟机中,程序计数器以PC寄存器的形式存在,是线程私有的,用于记录当前线程执行的字节码指令的位置。

程序计数器为什么是私有的?

程序计数器(Program Counter, PC)之所以是私有的,特别是在Java虚拟机(JVM)和其他多线程环境中,主要原因可以归纳为以下几点:

  1. 线程独立性

    • 独立执行流:每个线程在JVM中都有自己独立的执行流,而程序计数器用于记录当前线程正在执行的字节码指令的地址。通过为每个线程分配一个私有的程序计数器,可以确保线程在执行过程中不会相互干扰,保持各自的独立性。

    • 并行执行:在多线程环境下,多个线程可以在不同的代码位置上并行执行。私有的程序计数器允许这些线程各自独立地追踪其执行流,从而实现高效的并行处理。

  2. 栈帧管理和方法调用跟踪

    • 执行流程追踪:程序计数器指向当前线程所执行的字节码的地址,这对于栈帧管理和方法调用跟踪至关重要。每个线程的PC寄存器更新能准确追踪其执行流程,确保方法调用的正确性和执行流程的稳定性。
  3. 线程安全和性能提升

    • 避免锁竞争:由于程序计数器是私有的,不需要在多线程之间进行同步操作,这避免了额外的锁竞争,从而提高了程序的执行效率和性能。

    • 内存安全性:私有的程序计数器不会让一个线程看到另一个线程的执行状态,这有助于防止潜在的数据竞争和并发问题,增强了程序的内存安全性。

  4. 上下文切换和恢复执行状态

    • 上下文切换:在多线程环境中,线程会频繁地进行上下文切换。程序计数器记录了当前线程执行的位置,当线程被切换回来时,能够准确地恢复到之前的执行状态,继续执行。

    • 状态恢复:每个线程都有自己的程序计数器,这意味着即使在线程被挂起或暂停后,它也能在恢复执行时从正确的位置继续执行,保证了程序的连续性和稳定性。

  5. 实现细节

    • 原生实现:在JVM中,程序计数器的实现通常是由JVM原生代码(如C++)完成的,而不是通过Java代码。这是因为程序计数器需要紧密地与底层硬件和操作系统交互,以确保高效的执行和准确的控制。

综上所述,程序计数器之所以是私有的,主要是出于线程独立性、栈帧管理和方法调用跟踪、线程安全和性能提升、上下文切换和恢复执行状态等方面的考虑。这些特点使得程序计数器在多线程环境中能够高效地支持并发执行和程序控制。

Java 虚拟机栈的作用?

Java 虚拟机栈(Java Virtual Machine Stack)是 Java 运行时环境的一个重要组成部分,用来描述Java方法的内存模型

每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期。

每个方法在执行时都会创建一个栈帧

每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程。

有两类异常:

  • 线程请求的栈深度大于虚拟机允许的深度抛出StackOverflowError
  • 如果 JVM 栈容量可以动态扩展,栈扩展无法申请足够内存抛出OutOfMemoryError,HotSpot不可动态扩展,只要线程创建成功就不存在此问题。

虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈(Java Virtual Machine Stack)和本地方法栈(Native Method Stack)之所以是私有的,主要原因可以归纳如下:

一、保证线程隔离性

  1. 局部变量隔离

    • 虚拟机栈为每个Java方法在执行时创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

    • 栈帧的创建和销毁过程与方法的调用和返回过程紧密相关,从方法调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。

    • 由于每个线程调用的方法可能不同,且每个方法都有其独立的局部变量和操作数,因此每个线程需要维护一个独立的虚拟机栈来确保这些局部变量和操作数的隔离性,防止被其他线程访问或修改。

  2. 线程独立性

    • 本地方法栈的作用与虚拟机栈类似,但它是为虚拟机使用到的Native方法(如C或C++编写的方法)服务的。

    • 类似地,每个线程在调用Native方法时,也需要一个独立的栈来存储与该方法相关的局部变量和状态信息,以保证线程间的独立性。

二、支持多线程并发执行

  1. 避免数据竞争

    • 在多线程环境中,如果虚拟机栈和本地方法栈不是私有的,那么不同线程可能会访问和修改同一个栈中的数据,导致数据竞争和并发问题。

    • 通过为每个线程分配一个私有的栈,可以避免这种数据竞争,提高程序的并发性能和稳定性。

  2. 简化同步机制

    • 私有的栈使得线程间的同步变得更加简单和高效。由于每个线程都有自己独立的栈,因此不需要在栈层面进行复杂的同步操作。

三、实现细节和性能考虑

  1. 栈帧大小固定

    • 在Java虚拟机中,栈帧的大小通常在编译时就确定了,因此栈的大小也是相对固定的(尽管有些虚拟机允许栈的动态扩展)。

    • 这种固定大小的栈设计有助于简化内存管理和提高性能,因为虚拟机可以更容易地预测和管理栈内存的使用情况。

  2. 内存分配和回收

    • 私有的栈还简化了内存的分配和回收过程。由于每个线程的栈都是独立的,因此当线程结束时,其对应的栈也会被自动销毁,释放占用的内存资源。

综上所述,虚拟机栈和本地方法栈之所以是私有的,主要是为了保证线程间的隔离性、支持多线程并发执行、简化同步机制以及提高性能和内存管理的效率。这种设计使得Java虚拟机能够高效地处理多线程程序中的方法调用和状态管理问题。

栈帧是什么?

栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态连接、方法返回值和异常分派

栈帧随着方法调用而创建,随着方法结束而销毁。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,包括以下部分:

  1. 局部变量表(Local Variable Table)

    • 用于存储方法参数和方法内的局部变量的变量值存储空间。包括各种 Java 虚拟机基本数据类型,对象引用(reference)和 returnAddress(类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用)。

    • 数据在局部变量表中以局部变量槽(Slot)来表示。

    • 局部变量表的内存空间在编译期完成分配,运行期不会改变大小,Class 文件中方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量,即变量槽的数量。

    • 变量槽的内存大小由具体虚拟机实现决定。虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。

    • byte、short、char在存储前被转换为int,boolean也被准换为int,0为false,1为true。

    • 其中64位长度的 long 和 double 类型数据会占用两个变量槽,其余类型占用一个变量槽。

    • 虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从 index0~(局部变量表最大容量-1)

    • 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。

    • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会被存放在index为0的slot处,其余的参数和方法体内部定义的局部变量表会复制到局部变量表中的slot上

    • 如果Java虚拟机栈容量可以动态扩展,当无法申请足够内存会抛出 OutOfMemoryError 异常,HotSpot 虚拟机栈不可以动态扩展,主要线程申请栈空间成功就不会出现OOM 异常,但是如果申请时就失败就会抛出 OOM。

  2. 操作数栈(Operand Stack,也常称为操作栈)

    • 它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。

    • 操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量。

    • 在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。

    • 当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

    • 一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全互相独立的。但大多数虚拟机的实现会有优化处理,让两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与其上面那个栈帧的部分局部变量表重叠在一起,不仅可以节约一部分空间,重要的是方法调用的时候可以直接共用一部分数据,无须进行额外的参数复制传递。

  1. 动态连接

    • 在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。

    • 每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。

    • 符号引用的一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。

  2. 方法返回

    • 当一个方法开始执行时,可能有两种方式退出该方法:正常完成出口,异常完成出口。

    • 正常完成出口是指方法正常完成并退出,没有抛出任何异常。如果当前方法正常完成,则根据当前方法的返回的字节码指令,有可能会有返回值传递给方法调用者,或者无返回值。

    • 异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。无论是Java虚拟机抛出的异常还是代码中使用throw指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。

    • 无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。

一般会把动态连接,方法返回地址与其他附加信息一起归为一类,称为栈帧信息。

  1. 附加信息

    虚拟机规范允许具体的虚拟机实现增加一些规范中没有描述的信息到栈帧之中,例如和调试相关的信息,这部分信息完全取决于不同的虚拟机实现。

堆的作用是什么?

  • 唯一目的是用来存放对象实例,几乎所有对象实例都在这分配。Java 虚拟机规范对堆的描述:所有对象实例以及数组都应当在堆上分配。但由于即时编译技术的进步(栈上分配,标量替换),Java 对象实例都分配在堆上已经不再绝对。
  • 所有线程共享。
  • 堆可以物理上不连续,逻辑上连续的内存区域
  • 是Java 垃圾收集器的管理区域
  • 基于分代理论的垃圾收集器对堆区域的划分,非Java虚拟机具体固有内存布局,更不是Java虚拟机规范对Java堆的进一步细致划分。
  • 堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
  • 通过 -Xmx 和 -Xms 设定 堆内存大小
  • 可抛出 OutOfMemoryError

堆栈的区别?

  1. 定义与功能

    • 堆:是Java程序中的动态内存区域,主要用于存储对象和数组。堆的大小在程序运行时可以动态变化,可以通过JVM的参数进行设置。堆中的对象需要通过new操作符来创建,并且会自动分配内存空间。

    • 栈:是Java程序中的方法调用内存区域,主要用于存储方法调用时的局部变量、参数和返回地址。栈的大小是固定的,每个方法调用都会创建一个新的栈帧(Stack Frame),并压入栈中。当方法执行完毕后,相应的栈帧会从栈中弹出。

  2. 管理方式

    • 堆:由JVM自动管理,程序员只需要通过new操作符创建对象即可,JVM会自动分配内存空间并在对象不再使用时进行垃圾回收。

    • 栈:由系统自动分配和释放空间,无需程序员手动管理。当一个方法被调用时,相应的栈帧会被压入栈中;当方法执行完毕后,栈帧会自动从栈中弹出,释放空间,具有(LIFO)的顺序特点。

  3. 存储内容

    • 堆:存储的是对象和数组,是动态分配的。

    • 栈:存储的是方法调用时的局部变量、参数和返回地址,是固定大小的的空间。

  4. 访问方式

    • 堆:堆内存中的对象需要通过new操作符创建后才能访问。

    • 栈:栈内存中的数据可以通过变量名直接访问。

  5. 生命周期

    • 堆:堆内存中的对象的生命周期由程序员控制,通过 GC 进行垃圾回收。

    • 栈:栈内存中的数据在方法调用结束时自动释放。

  6. 空间大小

    • 堆:空间较大,具体大小取决于JVM的设置和系统的有效虚拟内存。

    • 栈:空间较小。当剩余栈空间不足时,会抛出java.lang.StackOverflowError异常。

  7. 共享性

    • 堆:是所有线程共有的,多个线程可以访问同一个堆中的对象。

    • 栈:是线程私有的,每个线程都有自己的栈空间,互不影响。

  8. 内存分配效率

    • 堆:由JVM自动管理,分配速度相对较慢,但使用方便。

    • 栈:由系统自动分配和释放,分配速度快。

方法区的作用是什么?

方法区(Method Area)在Java虚拟机(JVM)中扮演着重要的角色,主要用于存储与类相关的信息。

  1. 存储类的结构信息

    • 类的完整名称

    • 父类的完整名称

    • 实现的接口列表

    • 字段信息(包括字段名、类型、修饰符等)

    • 方法名、方法参数、方法字节码等信息

  2. 存储常量池(Constant Pool)

    • 常量池用于存放编译时生成的字面量和符号引用。

    • 包含了类、接口、字段和方法的符号引用,以及字符串常量等。

  3. 存储静态变量

    • 静态变量即类级别的变量,被所有该类的实例对象共享。

    • 静态变量在方法区中分配内存,属于类的数据,而不是实例的数据。

  4. 存储即时编译器编译后的代码缓存

    • 存放已经被即时编译器(JIT)编译成机器码的方法代码。

    • 提高执行效率,减少后续执行时的重复编译。

  5. 作为线程共享的内存区域

    • 与Java堆一样,方法区是各个线程共享的内存区域。

    • 多个线程同时加载同一个类时,只有一个线程能加载该类,其他线程需要等待该线程加载完毕后直接使用该类。

  6. 大小可配置

    • 方法区的大小可以选择固定大小或者可扩展。

    • 如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误,如java.lang.OutOfMemoryError: PermGen space(JDK 7以前)或java.lang.OutOfMemoryError: Metaspace(JDK 8及以后)。

  7. 在JVM启动和关闭时的行为

    • 方法区在JVM启动时被创建。

    • 当JVM关闭时,方法区的内存会被释放。

  8. 内存区域

    • JDK7 把放在字符串常量池、静态变量等移到堆中。

    • JDK8 中改用在本地内存中实现的元空间代替,把 JDK 7 中永久代剩余内容(主要是类型信息)全部移到元空间,运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中。

    • 不需要连续内存和可选择固定大小/可扩展外,还可以不实现垃圾回收。

谈一谈常量池?

JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池

  • Class文件常量池

    在java代码的编译期间,java文件被编译为**.class**文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。 class文件中存在常量池(非运行时常量池),其在编译阶段就已经确定。常量池表(Constant Pool Table)用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

  • 运行时常量池(Runtime Constant Pool)

    方法区的一部分,所以也是全局共享的。运行时常量池的作用是存储Class文件常量池中的符号符号引用信息,在类的解析阶段还会将这些符号引用翻译出直接引用(直接指向实例对象的指针,内存地址),翻译出来的直接引用也是存储在运行时常量池中。Java 运行期间也可以将新的常量放入常量池中,可以抛出 OutOfMemoryError。

  • 全局字符串常量池

    字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用。这些被维护的引用所指的字符串实例,被称作被驻留的字符串或interned string或通常所说的”进入了字符串常量池的字符串。JDK1.7后,字符串常量池被移到了heap区。

直接内存是什么?

直接内存(也称为堆外内存或Direct Memory)是Java虚拟机(JVM)管理的一种非堆内存区域,不属于运行时数据区,也不是虚拟机规范定义的内存区域。

  1. 定义与特点

    • 直接内存是Java应用程序通过直接方式从操作系统中申请的内存。

    • 它不经过Java虚拟机的堆内存,而是直接由Java代码通过特定的方式(如java.nio.DirectByteBuffer)进行访问和操作。

    • 直接内存的分配和使用不受Java堆大小的限制,但受到本机总内存大小和处理器寻址空间的限制。

  2. 作用与优势

    • 直接内存的主要作用是为了提高某些操作的性能,如文件读写、网络通信等。

    • 由于直接内存是直接由操作系统管理,它可以减少Java堆和Native堆之间数据复制的开销(零拷贝),从而提高数据的传输效率。

    • 使用直接内存可以避免频繁的垃圾回收(GC)操作,减少GC对系统性能的影响。

  3. 访问方式

    • JDK 1.4引入了NIO(New I/O)类,其中包括基于通道(Channel)与缓冲区(Buffer)的I/O方式。

    • 通过NIO的DirectByteBuffer类,Java代码可以直接访问和操作直接内存。DirectByteBuffer作为直接内存的引用存储在Java堆中。

  4. 配置与管理

    • 直接内存的大小可以通过JVM参数-XX:MaxDirectMemorySize进行设置,该参数的大小限制对直接使用 Unsafe 类是无效的。如果不指定该参数,默认大小通常与堆的最大值(由-Xmx参数指定)相同。

    • 需要注意的是,在配置JVM参数时,应该考虑直接内存的大小,避免各个内存区域的总和超过物理内存限制,导致动态扩展时出现OOM(OutOfMemoryError)错误。

    • 直接内存导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果发现内存溢出后产生的 Dump 文件很小,而程序中又直接或间接使用了直接内存(典型的间接使用就是 NIO),那么就可以考虑检查直接内存方面的原因。

  5. 应用场景

    • 直接内存常用于需要高性能I/O操作的场景,如网络服务器、数据库连接池等。

    • 在处理大文件读写、网络通信等任务时,使用直接内存可以显著提高系统的吞吐量和响应速度。

内存溢出和内存泄漏的区别?

内存溢出(Out Of Memory,简称OOM)和内存泄漏(Memory Leak)在软件开发中是两个重要的概念,它们之间的区别可以从以下几个方面来阐述:

  1. 定义不同

    • 内存溢出:程序在申请内存时,没有足够的内存空间供其使用。

    • 内存泄漏:程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出。

  2. 产生原因不同

    • 内存溢出:通常是因为程序申请的内存超出了系统能够提供的范围,比如试图创建一个超大的数组或对象,超过了系统或虚拟机的限制。

    • 内存泄漏:通常是由于程序的设计问题导致的,比如忘记释放已经不再使用的内存,或者引用已不需要的对象,使得这部分内存无法被回收。

  3. 影响程度不同

    • 内存溢出:会导致程序立即崩溃或者抛出错误,影响较大。

    • 内存泄漏:一次小的内存泄漏可能不会立即影响程序运行,但是如果大量内存泄漏累积,最终会导致内存耗尽,影响系统的正常运行,甚至可能导致系统崩溃。

  4. 处理方式不同

    • 内存溢出:对于内存溢出的问题,通常需要检查程序是否有不必要的大内存申请,或者优化程序使得内存使用更加高效。有时候,重启电脑或软件后释放掉一部分内存又可以正常运行该软件一段时间。

    • 内存泄漏:对于内存泄漏的问题,首先需要找到程序中导致内存泄漏的部分,然后修复这些问题,比如及时释放不再使用的内存,或者取消对不再需要的对象的引用。

  5. 检测工具不同

    • 内存溢出:可以通过一些性能监控工具来预防内存溢出,如JProfiler、MAT等。

    • 内存泄漏:一些内存分析工具可以帮助检测内存泄漏,如Valgrind、LeakCanary等。

  6. 风险性

    • 内存泄漏由于具有隐蔽性、积累性的特征,不易被直接检测到,但它可以逐渐消耗系统资源,最终导致内存溢出,引发系统崩溃等严重后果。

    • 内存溢出虽然会立即导致程序崩溃或错误,但其根源往往与内存泄漏有关,因此解决内存泄漏问题是防止内存溢出的关键。

Java 内存溢出的场景有哪些?

  • 堆溢出 堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及最大堆容量后就会 OOM,例如在 while 死循环中一直 new 创建实例。堆 OOM 是实际应用中最常见的 OOM,处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要,分清到底是内存泄漏还是内存溢出。如果是内存泄漏,通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 关联才导致无法回收,一般可以准确定位到产生内存泄漏代码的具体位置。如果不是内存泄漏,即内存中对象都必须存活,应当检查 JVM 堆参数,与机器内存相比是否还有向上调整的空间。再从代码检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
  • 栈溢出 由于 HotSpot 不区分虚拟机和本地方法栈,栈容量只能由-Xss参数来设定,存在两种异常:StackOverflowError: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError,例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。OutOfMemoryError: 如果 JVM 栈可以动态扩展,当扩展无法申请到足够内存时会抛出 OutOfMemoryError。HotSpot 不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OOM,否则在线程运行时是不会因为扩展而导致溢出的。
  • 方法区溢出 方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用 JDK 反射或 CGLib 直接操作字节码在运行时生成大量的类。很多框架如 Spring、Hibernate 等对类增强时都会使用 CGLib 这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。JDK8 使用元空间取代永久代,HotSpot 提供了一些参数作为元空间防御措施,例如 -XX:MetaspaceSize指定元空间初始大小,达到该值会触发 GC 进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。

虚拟机的类加载过程是怎样的?

JVM 把描述类的数据从 Class 文件到最终形成可以被虚拟机直接使用的 Java 类型的过程称为虚拟机的类加载机制。

Class 文件中描述的各类信息都需要加载到虚拟机后才能使用。

一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段

验证、解析和初始化三个部分称为连接。加载、验证、准备、初始化阶段的顺序是确定的,解析则可能在初始化之后再开始,这是为了支持 Java 的动态绑定。ß

  1. 加载(Loading)

    通过一个类的全限定名来获取定义此类的二进制字节流。这个二进制字节流可以从文件系统、网络等来源获取。

    将这个字节流代表的静态存储结构转化为方法区的运行时数据结构java.lang.Class

    在内存中(通常是Java堆)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

  2. 验证(Verification)确保 Class 文件的字节流符合虚拟机规范约束。防止载入有错误或恶意企图的字节流而导致系统受攻击

    验证主要包含四个阶段:

    • 文件格式验证:验证字节流文件是否符合Class文件格式规范。

    • 元数据验证:对元数据信息进行语义校验。

    • 字节码验证:对数据流和控制流进行分析,校验方法体,保证安全。

    • 符号引用验证:确保解析动作能正常执行。

验证重要但非必需,因为只有通过与否的区别,通过后对程序运行期没有任何影响。如果代码已被反复使用和验证过,在生产环境就可以考虑关闭(-Xverify:none)大部分验证缩短类加载时间。

  1. 准备(Preparation)

    • 正式为类变量(静态变量)分配内存并设置类变量初始值的阶段,都在方法区中进行分配,在JDK8及之后,类变量则会随着Class对象一起存放在Java堆中

    • 这里的初始值通常是默认值,如数值型变量的默认值为0,引用类型的默认值为null。

    • 常量值(被final修饰的静态变量)的初始化在这个阶段不会被执行,它们在编译阶段就已经赋值。

  2. 解析(Resolution)

    • 将常量池内的符号引用替换为直接引用的过程。

    • 主要针对四类符号进行解析:类或接口、字段、类的方法、接口方法。

    • 符号引用以一组符号描述引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存。

    • 直接引用是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄和虚拟机的内存布局相关,引用目标必须已在虚拟机的内存中存在。

  3. 初始化(Initialization)

    • 类初始化阶段是类加载过程的最后一步。

    • 直到该阶段 JVM 才开始执行类中编写的代码。

    • 准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源。初始化阶段就是初始化阶段就是执行类构造器**<clinit>()**方法的过程

    • <clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成。

  4. 使用(Using)

    • 类的初始化完成后,类就可以被虚拟机使用了。

    • 这包括创建类的实例、访问类的静态变量和静态方法等。

  5. 卸载(Unloading)

    • 当类不再被使用,并且没有引用指向它时,类加载器会卸载这个类。

    • 卸载类的过程是将类从虚拟机中移除,释放其占用的内存空间。

整个类加载过程是由虚拟机严格控制的,并且是按照上述顺序进行的。但是,需要注意的是,解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。

类加载的时机有哪些?

类的加载方式分为隐式加载与显式加载两种。隐式加载指的是程序在使用new等方式创建对象时,会隐式地调用类的加载器把对应的类加载到JVM中。显式加载指的是通过直接调class.forName()方法来把所需的类加载到JVM中。Java 中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java 动态扩展的语言特性就是依赖运行期动态加载和连接实现的。只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  • 遇到 new、getstatic、putstatic 或 invokestatic 字节码指令时,还未初始化。典型场景包括 new 实例化对象、读取或设置静态字段、调用静态方法。如果是final类型的静态常量,并且在编译时已经将该值放入常量池中的情况除外。
  • 对类反射调用时,还未初始化, 例如通过 Class.forName() 方法加载类时。
  • 初始化类时,父类还未初始化,则需要先触发父类的初始化。
  • 虚拟机启动时,会先初始化包含 main 方法的主类。
  • 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  • 当一个接口中定义了JDK 8新加入的默认方法时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

其余所有引用类型的方式都不会触发初始化,称为被动引用。

  1. 子类使用父类的静态字段时,只有父类被初始化。
  2. 通过数组定义使用类。
  3. 常量在编译期会存入调用类的常量池,不会初始化定义常量的类。

接口和类加载过程的区别,初始化类时如果父类没有初始化需要初始化父类,但接口初始化时不要求父接口初始化,只有在真正使用父接口时(如引用接口中定义的常量)才会初始化。

<cinit>()方法有什么特点?

<cinit>()方法通常称为类构造器或静态构造器。

  1. 静态性质

    • <cinit>()方法是由static关键字修饰的,因此它属于类本身,而不是类的实例。

    • <cinit>()方法在没有创建类的任何实例的情况下也可以被调用和执行。

  2. 初始化类变量

    • <cinit>()方法的主要职责是初始化类的静态变量(即类变量)。

    • 虚拟机的类加载过程中的初始化阶段就是执行类构造器<clinit>()方法的过程。

  3. 收集静态代码块

    • <clinit>()是Javac编译器的自动生成

    • 除了初始化静态变量外,<cinit>()方法还会收集类中的静态代码块(static {}块)中的语句。

    • 这些静态代码块和静态变量的初始化操作会按照它们在源代码中出现的顺序(从上到下)被<cinit>()方法执行。

    • 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

  4. 执行时机

    • 在类的第一个实例被创建之前,<cinit>()方法就已经被调用过了。

    • <clinit>()方法与类的<init>()方法不同,不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object

  5. 执行一次

    • <cinit>()方法只会在类被加载到JVM时执行一次。

    • 即使后续创建了类的多个实例,<cinit>()方法也不会被再次执行。

    • 接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

  6. 父类先执行

    • 如果一个类继承自另一个类,那么父类的<cinit>()方法会在子类的<cinit>()方法之前执行。

    • 这是因为子类可能会依赖于父类静态变量的初始化。

    • 接口中有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。

  7. 线程安全

    • <cinit>()方法的执行是线程安全的。

    • 在多线程环境下,只有一个线程会执行<cinit>()方法,其他线程会阻塞直到<cinit>()方法执行完成。

    • 如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

  8. 非必要生成

    • 如果一个类中没有静态代码块,也没有静态变量赋值操作,那么编译器可以不为该类生成<cinit>()方法。

综上所述,<cinit>()方法是Java等编程语言中用于初始化类静态变量和静态代码块的重要机制,具有静态性、初始化类变量、收集静态代码块、执行时机确定、执行一次、父类先执行、线程安全和非必要生成等特点。

Class 对象是什么?

在程序运行期间,Java 运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。Class对象(也称为类对象或类字面量)是Java反射机制的核心。每个类都有一个与之关联的Class对象,它包含了关于该类(包括其方法、字段、构造函数等)的元数据信息。

Class 对象的特点:

  • Class类也是类的一种,其创建后的对象称作class对象;
  • 手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中
  • 每个通过关键字class标识的类,无论创建多少个实例对象,在内存中有且只有一个与之对应的Class对象来描述其类型信息;
  • Class类只有私有构造函数,因此对应Class对象只能有JVM创建和加载;
  • 某个类的Class对象是随着JVM加载该类过程中一起被加载到内存的。

获取 Class 对象的方式:

  • 类名.class ,该方式(字面量)获取对Class对象的引用时,不会自动地初始化该Class对象
  • 对象的 getClass方法。
  • Class.forName(类的全限定名),执行的过程中发现如果类Dog还没有被加载,那么JVM就会调用类加载器去加载该类,并返回加载后的Class对象。

Class对象的一些主要作用:

  1. 反射(Reflection)

    • 通过Class对象,我们可以在运行时获取类的信息,如类的名称、类的修饰符、类的父类、类实现的接口等。

    • 我们还可以动态地创建类的实例、调用类的方法、访问类的字段等。这种能力使得Java具有高度的灵活性和可扩展性。

  2. 类型信息

    Class对象可以表示任何类型(包括基本类型、数组类型、枚举类型、注解类型、类类型、接口类型以及它们的子类型)。通过instanceof运算符和.getClass()方法可以检查一个对象是否是某个特定类的实例。

  3. 类型转换

    在进行类型转换时,Class对象可以作为类型标记使用。例如,当使用Class.cast(Object obj)方法时,它会根据指定的Class对象来将给定的对象强制转换为相应的类型。

  4. 泛型信息

    在Java泛型中,Class对象可以用于表示泛型参数的类型。例如,在List<String>中,我们可以通过List.class获取到List的Class对象,但无法直接获取到泛型参数String的Class对象。不过,通过一些技巧(如使用类型令牌或类型擦除的桥接方法),我们仍然可以在运行时获取到泛型参数的类型信息。

  5. 类加载器(ClassLoader)

    Class对象与类加载器(ClassLoader)密切相关。类加载器负责在运行时加载类并生成相应的Class对象。通过Class.getClassLoader()方法可以获取到与该Class对象关联的类加载器。

  6. 注册和序列化

    在某些情况下,Class对象还用于注册或序列化类的信息。例如,在Java的序列化机制中,当一个对象被序列化时,它的类的Class对象也会被序列化,以便在反序列化时能够正确地恢复该对象。

  7. 注解处理

    Java中的注解(Annotation)是元数据的一种形式,可以用于为代码(包括类、方法、字段等)提供额外的信息。通过Class对象,我们可以获取到与类关联的注解信息,并对其进行处理。

总之,Class对象在Java中扮演着非常重要的角色,它使得Java具有高度的灵活性和可扩展性。通过反射机制,我们可以在运行时动态地获取和操作类的信息,从而实现各种复杂的功能。

什么是类加载器?

类加载器(Class Loader)是Java运行时环境(JRE)的一部分,**类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。**实现这个动作的代码被称为“类加载器”(ClassLoader)。虚拟机提供了3种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader) 启动类加载器主要加载的是JVM自身需要的类,该类加载使用 C++ 语言实现的,是虚拟机自身的一部分,不存在于 JVM 体系。负责将 <JAVA_HOME>/lib 路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
  2. 扩展类加载器(Extension ClassLoader) 扩展类加载器是指sun.misc.Launcher.ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载**<JAVA_HOME>/lib/ext** 目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。从 JDK9 开始从扩展类加载器更换为平台类加载器,负载加载一些扩展的系统类,比如 XML、加密、压缩相关的功能类等。
  3. 系统类加载器(System ClassLoader) 也叫应用类加载器(Application ClassLoader)是指sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath-D java.class.path指定路径下的类库,也就是classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

三种类加载器间的关系:

  • 启动类加载器,由C++实现,没有父类。
  • 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null,这个null会去调用启动类加载器。
  • 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
  • 自定义类加载器,父类加载器肯定为AppClassLoader。

除了以上三种,开发者还可以自定义类加载器,以满足一些特殊的需求,如代码热替换、模块化、插件化等。自定义类加载器通过继承 ClassLoader并重写 findClass()方法实现。

类加载器中findClass与loadClass的区别?

类加载器java.lang.ClassLoader 中的 findClass()loadClass() 方法都是用来加载 Java 类的,但是它们的实现方式和使用场景略有不同。

  1. 功能与作用

    • loadClass:这是ClassLoader类中的一个方法,用于加载一个类,并返回Class对象的引用。它首先会调用父类加载器(如果存在)来尝试加载类,如果父类加载器无法加载该类,则当前加载器会尝试自己加载。这是遵循Java的“双亲委派模型”的体现。

    • findClass:这个方法用于实现具体的类加载逻辑。当loadClass方法确定需要由当前类加载器来加载类时(即父类加载器加载失败),它会调用findClass方法来查找并加载类。因此,开发者通常会在自定义的类加载器中重写findClass方法以实现特定的加载逻辑。

  2. 调用方式

    • loadClass是一个非静态方法,需要通过ClassLoader的实例来调用。

    • findClass同样是一个非静态方法,但通常不需要直接由外部调用,而是由loadClass在内部调用。

  3. 异常处理

    • loadClass方法不会直接抛出ClassNotFoundException异常,而是当找不到类时返回null。因此,在使用loadClass方法时,需要检查返回的Class对象是否为null。

    • findClass方法在找不到类时会抛出ClassNotFoundException异常。

  4. 灵活性

    • loadClass方法提供了更多的灵活性,允许指定一个父类加载器,或者选择是否立即对类进行链接。

    • findClass则更侧重于实现具体的加载逻辑,为开发者提供了自定义类加载行为的机会。

  5. 双亲委派模型

    • loadClass方法遵循Java的“双亲委派模型”,即先让父类加载器尝试加载类,如果父类加载器无法加载,则再由当前加载器加载。

    • findClass方法则通常在这个双亲委派的过程中,当确定需要由当前加载器加载类时被调用。

总结来说,loadClass和findClass在Java类加载过程中分别扮演了不同的角色。loadClass是类加载的入口点,它遵循双亲委派模型,并在需要时调用findClass来实现具体的加载逻辑。而findClass则提供了自定义类加载行为的机会,允许开发者根据需求实现特定的加载逻辑。因此,一般来说,如果您需要在特定的环境中加载 Java 类,您可以实现一个 ClassLoader 子类,如果不想打破双亲委派模型,那么只需要重写findClass()方法即可;如果想打破双亲委派模型,那么就重写整个loadClass方法。

双亲委派模型是什么?

双亲委派模型是一种层次化的类加载模型。

工作原理

  1. 委派过程:当一个类加载器需要加载一个类时,它首先将加载请求委派给其父类加载器。这个过程会一直持续到顶层的启动类加载器。如果父类加载器能够成功加载该类,则直接返回;否则,子类加载器才会尝试自己去加载。

  2. 类加载器层次:Java中主要有三种类加载器:

    • 启动类加载器:加载JDK中lib目录下Java的核心类库,即$JAVA_HOME/lib目录。

    • 扩展类加载器:加载lib/ext目录下的类。

    • 应用程序类加载器:加载用户写的应用程序。

    所有加载请求最终都应该传送到启动类加载器

作用

  1. 隔离命名空间:每个类加载器都有自己的命名空间,相同的类文件在不同的类加载器中被视为不同的类。这样可以避免类的冲突和混乱。
  2. 安全性:通过双亲委派模型,可以确保核心Java类库只由启动类加载器加载,防止恶意代码替换核心类库,提高系统的安全性。
  3. 代码复用:当一个类已经被加载过后,它就会被缓存起来,下次需要加载时可以直接返回缓存的Class对象,提高了性能。

优点

  1. 避免重复加载类:通过双亲委派模型,相同的类只会被加载一次,避免了类的重复加载。
  2. 保护核心API:由于只有启动类加载器能够加载核心类库,因此可以确保Java的核心API不被篡改。

双亲委派模型是Java虚拟机中一种重要的类加载机制,它通过层次化的类加载器和逐级委派的加载方式,确保了Java核心类库的安全性和稳定性,同时避免了类的冲突和混乱。

有哪些破坏双亲委派模型的场景?

破坏双亲委派模型的场景在Java中确实存在,这通常是由于某些特殊需求或历史原因导致的。以下是几个破坏双亲委派模型的典型场景:

  1. 兼容1.2之前版本

    • 背景:双亲委派模型在JDK 1.2之后才被引入,但类加载器的概念和抽象类java.lang.ClassLoader在Java的第一个版本中就已经存在。

    • 问题:为了兼容已经存在的用户自定义类加载器的代码,Java设计者在引入双亲委派模型时不得不做出妥协。

    • 解决:在JDK 1.2之后的java.lang.ClassLoader中添加了一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。

  2. 基础类型回调用户代码

    • 背景:双亲委派模型很好地解决了各个类加载器协作时基础类型的一致性问题,但存在基础类型需要回调用户代码的情况。

    • 问题:例如,JNDI服务是一个典型的例子。JNDI的代码由启动类加载器完成加载,但JNDI服务需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(SPI)的代码。

    • 解决:这种情况下,双亲委派模型被“破坏”,因为启动类加载器需要加载本应由应用程序类加载器加载的代码。

  3. 自定义类加载器

    • 背景:在某些特殊场景下,如代码热替换、模块化、插件化等,开发者需要自定义类加载器以实现特定的加载策略。

    • 问题:自定义类加载器可能会不遵循双亲委派模型,直接加载类,从而破坏原有的类加载层次结构。Tomcat,应用的类加载器优先自行加载应用目录下的 class

    • 解决:开发者需要明确自定义类加载器的加载策略,并确保其不会对整个应用程序的类加载造成负面影响。

  4. OSGi(Open Service Gateway initiative)

    • 背景:OSGi是一个为Java平台定义的服务和模块化标准的规范,它允许在运行时动态地添加、更新和删除模块。

    • 问题:OSGi模块化的特性要求每个模块都有自己的类加载器,并且模块之间的类加载器应该是相互独立的。这与双亲委派模型存在冲突。

    • 解决:OSGi通过自定义的类加载器机制,实现了模块之间的类隔离和动态加载,从而打破了双亲委派模型。

  5. JDK 9 Platform ClassLoader

    • 背景:JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代。

    • 问题:当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,

    • 解决:如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。

如何判断两个Class类是否相等?

在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致,包括包名。
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。

常用的 JVM 调优的参数都有哪些?

常用的JVM调优参数主要包括以下几个方面,以下将按照不同的类别进行分点表示和归纳:

  • -Xms<size>:设置JVM启动时的初始堆内存大小。例如,-Xms256m表示设置初始堆大小为256MB。

  • -Xmx<size>:设置JVM可以使用的最大堆内存大小。例如,-Xmx1024m表示设置最大堆大小为1024MB。

  • -Xmn<size>:设置年轻代内存大小。这个参数指定了年轻代的大小,影响垃圾回收的频率和效率。

  • -XX:NewRatio=<ratio>:设置年轻代与老年代的比例。默认为2,表示年轻代占整个堆内存的1/3。

  • -XX:+UseSerialGC:使用串行垃圾回收器。该参数在单核处理器系统上比较适用,回收时会暂停应用程序的执行。

  • -XX:+UseParallelGC:使用并行垃圾回收器。该参数在具有多个处理器的系统上比较适用,可以并行地进行垃圾回收。

  • -XX:+UseConcMarkSweepGC:使用并发标记-清除垃圾回收器。该参数在较大堆内存的情况下比较适用,可以并发地进行垃圾回收,减少暂停时间。

  • -XX:+UseG1GC:启用G1垃圾回收器,适合大型应用。G1是一种面向服务端的垃圾回收器,能够减少应用程序的停顿时间。

  • -XX:SurvivorRatio=<ratio>:设置Eden区与Survivor区的比例。默认为8,表示Eden区占整个年轻代的8/10。

  • -XX:MaxTenuringThreshold=<threshold>:设置对象从Eden区到Survivor区晋升的最大年龄。

  • -XX:+PrintGC:开启打印 gc 信息;

  • -XX:+PrintGCDetails:打印 gc 详细信息。

  • -XX:ParallelGCThreads=<threads>:设置垃圾回收的线程数。

  • -XX:ConcGCThreads=<threads>:设置并发垃圾回收的线程数。

  • -Xss<size>:设置每个线程的栈大小。例如,-Xss1m表示设置每个线程的堆栈大小为1MB。

  • -XX:MetaspaceSize=<size>:设置元空间的初始大小。

  • -XX:MaxMetaspaceSize=<size>:设置元空间的最大大小。

  • -XX:+TieredCompilation:启用分层编译,对热点代码进行更深层次的优化。

  • -XX:CompileThreshold=<n>:设置JIT编译的阈值,决定方法调用次数达到多少次后开始编译。

  • -XX:MaxGCPauseMillis=<time>:设置G1 GC的目标最大停顿时间。

JVM 有哪几种执行模式?

对于Java 是解释执行的这个说法其实不太准确。java的源代码首先通过javac编译器编译成字节码(bytecode),然后在运行时,通过JVM内嵌的解释器将字节码转换成最终的机器码,这时候看起来是解释执行的,但是常见的JVM(例如HotSpot)都提供了即时编译器(Just-In-Time Compiler,JIT),它能够在运行时将热点代码编译成机器码,然后缓存在codecache中,需要的时候直接去codecache中取即可,而不用再次编译。这种情况下这些热点代码就属于编译执行而不是解释执行。

JVM目前支持四种执行模式:

  • 解释模式:JVM启动时,指定-Xint参数,就是告诉JVM只进行解释执行,不对代码进行编译。这种模式抛弃了JIT可能带来的性能优势。毕竟解释器是逐条读入,逐条解释执行的。
  • 编译模式:JVM启动时,指定-Xcomp参数,告诉JVM关闭解释器,使用编译模式(或者叫最大优化级别),不进行解释执行。这种模式并不表示执行效率最高,它会导致JVM启动变得非常慢,同时有些JIT编译器的优化操作(如分支预测)并不能进行有效的优化。
  • 混合模式:解释和编译混合的一种模式,新版本的JDK(例如JDK8)默认采用的是混合模式(-Xmixed)。通常运行在server模式的JVM,会进行上万次调用以收集足够的信息进行高效的编译,client模式这个限制是1500次。Hotspot JVM内置了两个不同的JIT编译器,C1对应client模式,适用于对于启动速度敏感的应用(如java桌面应用);C2对应server模式,它的优化是为长时间运行的服务器端应用设计的。
  • AOT(Ahead-of-Time Compilation):将javac编译器编译后的字节码直接编译成机器代码,避免了JIT预热等各方面的开销。Oracle JDK 9就引入了实验性的AOT特性,并增加了新的jaotc工具。

怎么获取 Java 程序使用的内存和堆使用的百分比?

java
Runtime.freeMemory() \\方法返回剩余空间的字节数 
Runtime.totalMemory();\\方法总内存的字节数 
Runtime.maxMemory();\\ 返回最大内存的字节数

有哪些故障处理工具?

  • jps:虚拟机进程状况工具,功能和 ps 命令类似,可以列出正在运行的虚拟机进程,显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(LVMID)。LVMID 与操作系统的进程 ID(PID)一致,使用 Windows 的任务管理器或 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID,但如果同时启动了多个虚拟机进程,必须依赖 jps 命令。
  • jstat:虚拟机统计信息监视工具。用于监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据,在没有 GUI 界面的服务器上是运行期定位虚拟机性能问题的常用工具。参数含义:S0 和 S1 表示两个 Survivor,E 表示新生代,O 表示老年代,YGC 表示 Young GC 次数,YGCT 表示 Young GC 耗时,FGC 表示 Full GC 次数,FGCT 表示 Full GC 耗时,GCT 表示 GC 总耗时。
  • jinfo:Java 配置信息工具。实时查看和调整虚拟机各项参数,使用 jps 的 -v 参数可以查看虚拟机启动时显式指定的参数,但如果想知道未显式指定的参数值只能使用 jinfo 的 -flag 查询。
  • jmap:Java 内存映像工具。用于生成堆转储快照,还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率,当前使用的是哪种收集器等。和 jinfo 一样,部分功能在 Windows 受限,除了生成堆转储快照的 -dump 和查看每个类实例的 -histo 外,其余选项只能在 Linux 使用。
  • jhat:虚拟机堆转储快照分析工具。JDK 提供 jhat 与 jmap 搭配使用分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/Web 服务器,生成堆转储快照的分析结果后可以在浏览器查看。
  • jstack:Java 堆栈跟踪工具。用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是**定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。**线程出现停顿时通过 jstack 查看各个线程的调用堆栈,可以获知没有响应的线程在后台做什么或等什么资源。
  • jconsole 用于对 JVM 中的内存、线程和类等进行监控;
  • jvisualvm JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

基于 MIT 许可发布