JVM面试问题详解

1.什么是JVM?

  1. JVM 指的是Java虚拟机,本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件,作用是为了支持跨平台特性。
  2. JVM的功能有三项:
    1. 解释执行字节码指令:JVM 会逐条解释 .class 文件中的字节码指令,将其实时转换为机器码并交由底层硬件执行。这一过程由 JVM 内部的解释器完成,确保 Java 代码能够在不同的平台上运行。
    2. 内存管理与垃圾回收:JVM 负责管理对象在内存中的分配和释放,包括为对象分配内存,以及通过垃圾回收机制(Garbage Collection, GC)自动清理不再使用的对象,以防止内存泄漏并优化内存使用效率。
    3. 即时编译(JIT)优化:为了提升性能,JVM 包含了即时编译器(Just-In-Time Compiler, JIT),它会在程序运行过程中识别“热点代码”,将这些频繁执行的字节码编译为本地机器码,避免反复解释执行,从而大幅提升执行效率。
  3. JVM组成分为类加载子系统运行时数据区执行引擎本地接口这四部分。
    1. 类加载子系统:在 Java 程序运行过程中,源代码首先被编译器编译为 .class 字节码文件,而类加载子系统负责通过类加载器将字节码文件加载到内存中。类加载过程包括加载、连接和初始化三个阶段,确保类可以被虚拟机正确使用。
    2. 运行时数据区:这是 JVM 在运行时所使用的内存,主要划分为以下几个区:
      • 堆(Heap):用于存储对象实例数组,是垃圾回收器关注的主要区域。
      • 栈(Stack):包括两部分:
        • 虚拟机栈每个线程都会创建一个虚拟机栈,用来存储方法调用局部变量操作数栈方法返回地址等信息。
        • 本地方法栈:与虚拟机栈类似,但专门用于本地方法的调用
      • 方法区(Method Area):用于存储类信息常量静态变量方法字节码,Java 8 之后,方法区的实现被移到堆内存中,称为 Metaspace(元空间)。(字节码文件)
      • 程序计数器(PC 寄存器)每个线程都有自己的程序计数器,用来指示下一条即将执行的字节码指令的位置。
    3. 执行引擎:负责将字节码指令转换为机器码,并执行。
      • 解释器:逐条解释字节码,将其转换为机器码执行。
      • 即时编译器(JIT):为了提升性能,JIT 编译器会将热点代码(即多次执行的代码片段)编译为本地机器码,以减少解释执行的开销。
      • 垃圾回收器(Garbage Collector):负责自动管理内存回收,对不再使用的对象进行清理,防止内存泄漏。
    4. 本地接口(Native Interface):JVM 提供了与其他编程语言(如 C 和 C++)进行交互的机制,即本地接口。在某些场景下,JVM 会调用这些本地方法来执行特定的功能。
  4. 常用的JVM是Oracle提供的Hotspot虚拟机,也可以选择GraalVM龙井OpenJ9等虚拟机。

2.了解过字节码文件的组成吗?

Java字节码文件由以下几部分组成:魔数(标识文件类型)、版本号常量池(存储类名、方法名等常量信息)、访问标志(类或接口的修饰符)、类索引和父类索引接口表字段表方法表(包含具体的字节码指令)、属性表(记录附加信息,如源码文件名、注解等)。这些部分共同定义了Java类文件的结构。

  1. 魔数 (Magic Number)
    每个Java字节码文件以固定的魔数 0xCAFEBABE 开头,用于标识该文件是一个Java字节码文件。
  2. 版本号 (Version Number)
    紧接着魔数的是字节码的版本信息,包括主版本号次版本号,这标识了该字节码文件使用的Java版本。
  3. 常量池 (Constant Pool)
    常量池存储了类文件中的所有常量,包括类名方法名字段名字符串字面量数值常量等。常量池是字节码文件中非常重要的部分。
  4. 访问标志 (Access Flags)
    访问标志用于说明类或接口的修饰符信息,比如这个类是否是 public,是否是 abstract,或者是否是 final 等。
  5. 类索引、父类索引 (Class Index, Superclass Index)
    类索引用于标识当前类,父类索引用于标识当前类的父类。如果该类是 java.lang.Object,父类索引为0
  6. 接口索引表 (Interfaces)
    如果类实现了某些接口,接口索引表会列出这些接口。每个接口通过一个索引指向常量池中的接口描述。
  7. 字段表 (Fields)
    字段表包含了类的所有字段的定义,每个字段对应的类型修饰符名字等信息都在字段表中。
  8. 方法表 (Methods)
    方法表包含类中所有方法的定义,包括方法名返回值类型参数类型方法修饰符等。每个方法对应一个 Code 属性,存储了具体的字节码指令。
  9. 属性表 (Attributes)
    属性表记录了与类、字段、方法相关的附加信息,例如源码文件名注解局部变量表异常表等。常见的属性有 Code、LineNumberTable 等。

3.说一下运行时数据区

运行时数据区指的是JVM所管理的内存区域,其中分成两大类:

  • 线程共享 方法区
    方法区:存放每一个加载的类的元信息、运行时常量池、字符串常量池。
    :存放创建出来的对象。
  • 线程不共享本地方法栈虚拟机栈程序计数器
    本地方法栈虚拟机栈都存放了线程中执行方法时需要使用的基础数据。
    程序计数器存放了当前线程执行的字节码指令在内存中的地址。
    直接内存主要是NIO使用,由操作系统直接管理,不属于JVM内存。
  1. 方法区 (Method Area)
    存储类结构信息(如类元数据、常量池、静态变量、即使是编译后的代码等),这是所有线程共享的区域。
  2. 堆 (Heap)
    堆存储所有的对象和数组,也是线程共享的区域。所有对象实例都在堆上分配内存,垃圾回收器会在堆上自动管理内存回收。
  3. Java栈 (Java Stack) 虚拟机栈
    每个线程都有自己的栈帧,存储局部变量、操作数栈、方法返回地址等信息。栈是线程私有的,每个方法调用都会创建一个新的栈帧。
  4. 本地方法栈 (Native Method Stack)
    该栈用于支持本地方法(通常是C/C++代码),也是线程私有的。
  5. 程序计数器 (Program Counter, PC寄存器)
    每个线程都有自己的PC寄存器,记录当前线程所执行的字节码指令的地址。如果当前执行的是本地方法,则该寄存器为空。
  6. 运行时常量池 (Runtime Constant Pool)
    这是方法区的一部分,存储了编译期生成的常量,还包括方法和字段的引用,供类加载时和运行时使用。

4.哪些区域会出现内存溢出,会有什么现象?

内存溢出: 指的是内存中某一块区域的使用量超过了允许使用的最大值,从而使用内存时因空间不足而失败,虚拟机一般会抛出指定的错误。
堆:溢出之后会抛出OutOfMemoryError,并提示是Java heap Space导致的。
栈:溢出之后会抛出StackOverflowError
方法区:溢出之后会抛出OutOfMemoryError,JDK7及之前提示永久代,JDK8及之后提示元空间。
直接内存:溢出之后会抛出OutOfMemoryError

5.JVM在JDK6-8之间在内存区域上有什么不同

  1. 永久代移除:JDK 8 移除了永久代,取而代之的是元空间,元空间使用本地内存,从而解决了永久代空间不足导致的 OOM 问题。
  2. 运行时常量池移到堆中:常量池在 JDK 8 中被移到了堆内存,从而减少了对专用区域的依赖。
  3. 字符串常量池优化:JDK 7 及以上版本中,字符串常量池已经移到了堆中,使得字符串池管理更加灵活。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

字符串常量池从方法区移动到堆的原因:

1、垃圾回收优化字符串常量池的回收逻辑和对象的回收逻辑类似,内存不足的情况下,如果字符串常量池中的常量不被使用就可以被回收;方法区中的类的元信息回收逻辑更复杂一些。移动到之后,就可以利用对象的垃圾回收器,对字符串常量池进行回收。
2、让方法区大小更可控:一般在项目中,类的元信息不会占用特别大的空间,所以会给方法区设置一个比较小的上限。如果字符串常量池在方法区中,会让方法区的空间大小变得不可控。
3、intern方法的优化:JDK6版本中intern () 方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中。JDK7及之后版本中由于字符串常量池在堆上,就可以进行优化:字符串保存在堆上,把字符串的引用放入字符串常量池,减少了复制的操作。
在 JDK 6 到 JDK 8 之间,JVM 的内存区域发生了一些重要变化,尤其是在**永久代(Permanent Generation)元空间(Metaspace)**的处理上。以下是它们之间的主要区别:

1. 永久代(Permanent Generation) vs 元空间(Metaspace)

  • JDK 6 和 JDK 7:
    • 使用 永久代(PermGen) 来存储类的元数据、常量池、方法、类静态变量等。
    • 永久代的大小是有限的,通过 -XX:PermSize-XX:MaxPermSize 控制。
    • 如果加载类过多或者方法区空间不够,会导致 java.lang.OutOfMemoryError: PermGen space 错误。
  • JDK 8:
    • 永久代被移除了,取而代之的是元空间(Metaspace)
    • 元空间使用本地内存(Native Memory)而非 JVM 堆内存,因此它的大小不再受到堆内存的限制。
    • 元空间的默认大小根据系统可用内存自动调整,但可以使用 -XX:MetaspaceSize-XX:MaxMetaspaceSize 来手动调整。
    • JDK 8 中再也不会出现 PermGen space 的 OOM 错误,而可能会出现与本地内存相关的错误。

2. 常量池的变化

  • JDK 6 和 JDK 7:
    • 运行时常量池(Runtime Constant Pool)在永久代中分配,主要用于存储类的字面量(如字符串常量)和符号引用(如类和方法的引用)。
    • 字符串常量池(String Constant Pool)也在永久代中,这意味着如果有大量字符串常量,可能会耗尽永久代的空间。
  • JDK 8:
    • 运行时常量池被移到了堆中,不再占用永久代(因为永久代已被移除)。
    • 字符串常量池在 JDK 7 中已经从永久代移到了堆中,JDK 8 继续沿用了这一变化。

3. 类加载的变化

  • JDK 6 和 JDK 7:
    • 类的元数据存储在永久代中,因此类的加载和卸载直接影响永久代的大小。如果类加载过多而永久代空间不足,可能会导致 OutOfMemoryError: PermGen space 错误。
  • JDK 8:
    • 类的元数据存储在元空间中,元空间使用本地内存,可以动态扩展。因此类加载不再受到堆内存的限制,减少了 OutOfMemoryError 的出现。

4. GC 对永久代和元空间的影响

  • JDK 6 和 JDK 7:
    • 因为永久代属于堆内存的一部分,Full GC 过程中会对永久代进行回收,特别是类的卸载和常量池的清理。
  • JDK 8:
    • 元空间不在堆内存中,因此它不受堆 GC 的直接影响。类的卸载和元数据的清理由 元空间管理。

这些变化使得 JVM 的内存管理更加高效和灵活,特别是在处理大量类加载时,大大减少了内存相关的问题。

6.类的生命周期

类的生命周期包括:加载链接(验证、准备、解析)、初始化使用卸载五个阶段。加载时类被加载到内存,链接时进行符号解析,初始化时执行静态初始化块,使用阶段是类的实例化和方法调用,最后当类不再被引用时会被卸载。

1. 加载 (Loading) 类加载子系统
JVM通过类加载器将类的字节码文件(.class文件)加载到内存中。加载过程中,JVM会通过类加载器查找并读取类文件的字节流。
2. 链接 (Linking)
链接阶段包括三步:

  • 验证 (Verification):确保类文件的字节码符合JVM的规范,保证不会破坏虚拟机的安全性。
  • 准备 (Preparation):为类的静态变量分配内存,并将其初始化为默认值(如int类型初始化为0,引用类型初始化为null)。
  • 解析 (Resolution):将常量池中的符号引用(如类名、方法名)解析为直接引用。
    3. 初始化 (Initialization)
    这是类的构造阶段,执行类中的静态初始化块静态变量的赋值操作。初始化只会在类第一次被主动使用时进行。
    4. 使用 (Using)
    类初始化后就可以使用,实例化对象调用类的方法等操作都在这个阶段进行。
    5. 卸载 (Unloading)
    当类不再被使用,且类加载器无法再引用它时,JVM的垃圾回收机制会将该类卸载出内存,释放占用的资源。

7.什么是类加载器?

类加载器(Class Loader) 是Java虚拟机(JVM)中的一个组件,用于在运行时动态加载类到内存中。它负责将字节码文件(.class文件)加载到JVM,并且将类的符号引用转换为实际的内存地址。
类加载机制是动态的,类是在需要时才会被加载。

类加载器的作用:

  1. 加载类:从指定的位置(如文件系统、网络、JAR文件等)查找类的字节码文件,并将其加载到内存中。
  2. 将类进行初始化:加载后的类会经过验证、准备、解析和初始化,最终供程序使用。
  3. 隔离性和灵活性:类加载器允许不同的类在不同的加载器中加载,提供了类的隔离性。不同的类加载器可以加载不同版本的同一个类。

类加载器的类型
1.启动类加载器(Bootstrap ClassLoader)加载核心类
2.扩展类加载器(Extension ClassLoader)加载扩展类
3.应用程序类加载器(Application ClassLoader)加载应用classpath中的类
4.自定义类 加载器重写findClass方法
*JDK9及之后扩展类加载器(Extension ClassLoader)变成了平台类加载器(Platform ClassLoader)

类加载器的类型:

  1. 启动类加载器 (Bootstrap ClassLoader)
    负责加载核心类库(如 rt.jar),这是由JVM实现的类加载器,用于加载JDK中提供的核心类,如 java.lang.*。
  2. 扩展类加载器 (Extension ClassLoader)
    负责加载扩展类库(位于<JAVA_HOME>/lib/ext目录下的类),它加载的是一些扩展的类库(如第三方JAR包)。
  3. 应用类加载器 (Application ClassLoader)
    负责加载用户类路径(classpath)上的类,这是默认的类加载器,用于加载应用程序的类。
    自定义类加载器:
    开发者可以通过继承 java.lang.ClassLoader 创建自定义类加载器,来控制类的加载方式,甚至可以从特殊的地方(如数据库、网络)加载类。

8.什么是双亲委派机制

双亲委派机制指的是:向上交给父类加载器查找是否加载过,再由顶向下进行加载。
双亲委派机制的作用保证类加载的安全性,避免重复加载。

在这里插入图片描述

双亲委派机制(Parent Delegation Model)是Java类加载机制中的一种重要原则。它规定当某个类加载器收到类加载请求时,首先将请求委派给它的父类加载器,逐级向上递交,直到顶层的启动类加载器。只有当父加载器无法找到该类时,子加载器才会尝试加载类。


双亲委派机制的工作流程:

  1. 当类加载器接到加载类的请求时,它不会自己去尝试加载这个类,而是将这个请求委派给父类加载器。
  2. 父类加载器继续向上委派,直到请求到达启动类加载器(Bootstrap ClassLoader)。
  3. 启动类加载器尝试加载该类,如果能够找到该类,类加载过程结束;如果找不到,则依次返回到子加载器。
  4. 当所有父类加载器都无法加载该类时,才由当前的类加载器尝试加载。

双亲委派机制的好处:

  1. 安全性:确保Java核心类库(如java.lang.String等)只能由顶层的启动类加载器加载,防止核心类库被恶意替换。
  2. 避免重复加载:由于类加载请求首先由父类加载器处理,防止同一个类被多个类加载器重复加载,确保类加载的统一性。

双亲委派机制示例:

假设加载 java.lang.String 类,工作过程如下:

  1. 应用类加载器(Application ClassLoader)收到加载java.lang.String的请求。
  2. 它把请求委派给扩展类加载器(Extension ClassLoader)。
  3. 扩展类加载器再把请求委派给启动类加载器(Bootstrap ClassLoader)。
  4. 启动类加载器检查核心类库,找到java.lang.String类并加载,返回结果,类加载成功。

如果加载一个用户自定义类,假设是com.example.MyClass

  1. 应用类加载器收到加载com.example.MyClass的请求。
  2. 它将请求委派给父类加载器(扩展类加载器)。
  3. 扩展类加载器再委派给启动类加载器
  4. 启动类加载器无法加载,向下返回给扩展类加载器,扩展类加载器也无法加载,最终返回给应用类加载器
  5. 应用类加载器尝试加载,找到该类,并完成加载。

简述示例:

“双亲委派机制是Java类加载机制的一种设计模式。每个类加载器在加载类时,优先将请求委派给父类加载器,逐级向上,直到启动类加载器。只有当父类加载器无法找到该类时,子类加载器才会尝试加载。这种机制确保了核心类库的安全性和类加载的统一性。”

9.如何打破双亲委派机制

双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会自底向上交给父类加载器查找是否加载过,再由顶向下进行加载。
双亲委派机制的作用:保证类加载的安全性,避免重复加载。

打破双亲委派机制的常见方式包括:

  1. 通过自定义类加载器重写 loadClass() 方法,让类加载器自己加载类;使用 OSGi 等模块化框架;
  2. 使用线程上下文类加载器优先加载类;
  3. 以及像Tomcat这样的应用服务器通过自定义类加载结构来实现类隔离。

这些方法可以在特定场景下绕过父类加载器的委派。
在这里插入图片描述

打破 双亲委派机制 通常意味着让某个类加载器在加载类时,不再遵循将请求委派给父类加载器的默认流程,而是直接自行加载类。虽然双亲委派机制有助于保护核心类库和避免类的重复加载,但在某些特定场景下,我们可能需要打破这个机制,比如加载特定版本的类或插件。

以下是几种常见的方式可以打破双亲委派机制:


1. 自定义类加载器

可以通过继承 ClassLoaderURLClassLoader,并重写 loadClass() 方法来实现自定义类加载逻辑。默认情况下,loadClass() 方法会首先委派给父类加载器,你可以通过重写这个方法,跳过委派步骤,直接加载类。

示例代码:
public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 打破双亲委派机制,直接加载指定类
        if (name.startsWith("com.myapp")) {
            return findClass(name);
        }
        // 否则遵循默认机制,委派给父类加载器
        return super.loadClass(name, resolve);
    }
}

在这个例子中,MyClassLoader直接加载类名以 com.myapp 开头的类,而不委派给父类加载器。


2. OSGi 和类加载框架

像 OSGi(开放服务网关协议)这样的模块化框架允许动态加载和管理Java包,并通过自定义类加载器来实现类隔离,打破双亲委派机制。OSGi 中的每个模块(Bundle)都有自己的类加载器,Bundle 的类加载器在加载类时不遵循传统的双亲委派机制,而是使用 OSGi 特定的加载逻辑。


3. SPI(Service Provider Interface)机制

Java的SPI机制允许服务的提供者在运行时提供不同的实现,而不需要严格遵守类的加载机制。通过SPI,你可以定义接口,并在类路径中的 META-INF/services 目录下指定不同的实现类,由 ServiceLoader 来动态加载服务。这种方式可以间接打破双亲委派机制,因为 ServiceLoader 可以加载类路径中动态提供的类,而不依赖类加载器的默认行为。


4. 线程上下文类加载器(Thread Context ClassLoader)

JVM提供了线程上下文类加载器,允许你在某个线程中设置特定的类加载器。这样,当该线程加载类时,会优先使用线程上下文类加载器,从而可以绕过默认的类加载顺序。

示例代码:
Thread.currentThread().setContextClassLoader(new MyClassLoader());

当线程中的代码需要加载类时,会首先使用 MyClassLoader,从而打破双亲委派机制。


5. 双亲委派机制的破坏性案例:Tomcat

Tomcat等应用服务器通常会通过自定义类加载器来加载Web应用中的类和依赖库。Tomcat的类加载器结构中,Web应用的类加载器不会将加载请求委派给系统类加载器,而是优先加载Web应用目录中的类和依赖库。这打破了双亲委派机制,确保Web应用能够使用自己版本的类和依赖,而不会受到系统类库的影响。


简述示例:

“打破双亲委派机制的常见方式包括:通过自定义类加载器重写 loadClass() 方法,让类加载器自己加载类;使用 OSGi 等模块化框架;使用线程上下文类加载器优先加载类;以及像Tomcat这样的应用服务器通过自定义类加载结构来实现类隔离。这些方法可以在特定场景下绕过父类加载器的委派。”


这样简要描述了打破双亲委派机制的几种常见方式,并说明了具体应用场景。

10.如何判断堆上的对象没有被引用

引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1,存在循环引用问题所以Java没有使用这种方法。

Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GC Root)普通对象

可达性分析算法指的是如果从某个到GC Root对象是可达的,对象就不可被回收
GC Root对象

  1. 线程Thread对象,引用线程栈帧中的方法参数、局部变量等。
  2. 系统类加载器加载的java.lang.Class对象,引用类中的静态变量。
  3. 监视器对象,用来保存同步锁synchronized关键字持有的对象。
  4. 本地方法调用时使用的全局对象

在Java中,判断堆上的对象是否没有被引用通常是通过 垃圾回收器(Garbage Collector, GC) 来完成的

堆上对象没有被引用的判断依据:

JVM使用以下几种算法和策略来判断对象是否不再被引用:


1. 引用计数法(Reference Counting)

这是最简单的一种算法,垃圾回收器为每个对象维护一个引用计数器,每当有一个地方引用该对象时,引用计数加1;当引用失效时,引用计数减1。如果某个对象的引用计数为0,则认为该对象不再被引用,可以被回收。

缺点:
  • 循环引用问题:如果两个对象互相引用,引用计数器永远不会为0,导致内存无法回收。因此Java的垃圾回收器没有使用这种算法。

2.可达性分析算法(Reachability Analysis)

Java垃圾回收器主要使用 可达性分析算法 来判断对象是否还在被引用。该算法从一组称为 GC Roots 的根对象开始,沿着引用链查找。能够从 GC Roots 到达的对象被认为是 可达的(reachable),即仍然被引用;无法到达的对象则被认为是不可达对象,表示没有被引用,可以进行回收。

GC Roots 主要包括以下几种:
  • 虚拟机栈中引用的对象(栈帧中的局部变量表)
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中JNI引用的对象

如果对象从GC Roots无法到达,就被认为没有被引用,可以被垃圾回收。


3. 四种引用类型(强引用、软引用、弱引用、虚引用)

Java提供了4种不同的引用类型,用于控制对象的生命周期和回收策略:

  • 强引用(Strong Reference):最常见的引用形式,new 关键字创建的对象默认是强引用。只要存在强引用,垃圾回收器不会回收该对象。
  • 软引用(Soft Reference):在内存不足时,垃圾回收器会回收软引用的对象,常用于内存敏感的缓存。
  • 弱引用(Weak Reference):只要垃圾回收器运行,不管内存是否充足,弱引用的对象都会被回收,常用于避免内存泄漏。
  • 虚引用(Phantom Reference):虚引用对象在任何时候都可能被回收,通常用于追踪对象被回收的时间。

4. Finalization机制

如果对象没有被引用但重写了 finalize() 方法,那么在第一次被判定为不可达时不会立即回收,而是把对象放入一个 F-Queue 队列中,等待 finalize() 方法执行完后再进行第二次GC。如果该对象在 finalize() 中重新引用了自己,则该对象会“复活”;否则它将被彻底回收。

注意:
  • finalize() 方法在Java 9中已被弃用,不推荐依赖它来进行资源回收或对象清理。

总结

Java垃圾回收器主要通过 可达性分析算法 来判断堆上的对象是否没有被引用。它从GC Roots开始遍历对象,无法被GC Roots到达的对象会被判定为不可达,从而被回收。此外,Java还提供了不同类型的引用(如软引用、弱引用)来控制对象的生命周期。

简述示例
“JVM通过可达性分析算法判断堆上的对象是否没有被引用。如果从GC Roots无法访问到某个对象,则该对象会被认为是不可达的,进而被垃圾回收。此外,Java中的强引用、软引用、弱引用和虚引用也影响对象的可回收性。”

11. JVM 中都有哪些引用类型

JVM中的引用类型包括:强引用(不会被回收)、软引用(内存不足时回收,内存敏感的缓存,图片缓存)、弱引用(下一次GC时回收,缓存场景对象池 图片缓存)和虚引用(用于跟踪对象回收)。它们分别适用于不同的内存管理需求。

在JVM中,引用类型指的是在Java内存管理中对对象引用的不同强度,决定了对象的生命周期和垃圾回收行为。Java提供了四种主要的引用类型,每种引用类型对应不同的垃圾回收策略:

1. 强引用(Strong Reference)

  • 概念:这是Java中最常见、默认的引用类型。当你用 new 关键字创建对象时,它就是强引用。
  • 特点:只要强引用存在,垃圾回收器绝不会回收被引用的对象。即使内存不足,也不会回收这些对象,容易导致 内存泄漏
  • 示例
Object obj = new Object();  // 这是一个强引用

2. 软引用(Soft Reference)

  • 概念:软引用在内存不足时,才会被垃圾回收器回收。它适合用于实现缓存机制,在内存足够时不回收,内存紧张时才进行回收。
  • 特点:当JVM内存不够用时,垃圾回收器会回收软引用指向的对象。它比强引用更灵活,不容易导致内存不足。
  • 使用场景通常用于内存敏感的缓存,比如图片缓存等
  • 示例
SoftReference<Object> softRef = new SoftReference<>(new Object());
  • 回收时机:JVM在内存快耗尽时才会回收软引用对象。
    3. 弱引用(Weak Reference)
  • 概念:弱引用对象比软引用对象更容易被垃圾回收。只要垃圾回收器运行时,无论内存是否充足,都会回收弱引用指向的对象。
  • 特点:弱引用对象只要没有强引用或软引用指向它,就会被回收。
  • 使用场景:适合用于避免内存泄漏的场景,比如在某些缓存场景中,不希望持有对象太长时间
  • 示例
WeakReference<Object> weakRef = new WeakReference<>(new Object());
  • 回收时机:在下一次垃圾回收时(即使内存足够),弱引用对象都会被回收。
    4. 虚引用(Phantom Reference)
  • 概念:虚引用是最弱的一种引用类型,几乎形同虚设。它不能通过虚引用访问对象的内容,唯一的作用是跟踪对象被垃圾回收的状态。
  • 特点:虚引用必须和 引用队列(ReferenceQueue)配合使用。当垃圾回收器准备回收某个对象时,会把虚引用放入关联的引用队列中,通知程序对象已经被回收。
  • 使用场景:常用于管理对象被回收时的后续操作,比如监控对象的回收,或者在对象被回收后进行一些清理工作。
  • 示例
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), referenceQueue);
  • 回收时机:虚引用对象任何时候都有可能被回收。
    总结:
  • 强引用:最常见,绝不会被回收,除非手动断开引用。
  • 软引用:在内存不足时会被回收,适合用于缓存。
  • 弱引用:在下一次GC时都会被回收,常用于防止内存泄漏。
  • 虚引用:无法直接访问对象,只用于对象回收时的通知机制。

软引用(Soft Reference)弱引用(Weak Reference) 都是 Java 提供的用于管理内存的引用类型,它们在一定条件下允许垃圾回收器回收对象。两者的主要区别在于对象回收的时机不同,因此适合的使用场景也有所不同。

软引用(Soft Reference):

  • 回收时机:当 JVM 内存不足时,垃圾回收器会回收软引用对象。在内存充足时,软引用对象可能会一直保留。
  • 典型使用场景
    1. 内存敏感的缓存:软引用非常适合用于实现缓存系统,特别是在需要最大限度地利用内存来存储对象的情况下。当内存不足时,缓存对象会被回收,以避免内存溢出(OutOfMemoryError)。例如:
      • 图像、文件等大型对象的缓存。
      • 数据库查询结果的缓存。
    2. 大型对象的管理:对于占用大量内存但访问频率不高的大型对象(如数据块、图片等),可以使用软引用存储,以在需要时快速获取对象,同时允许它们在内存不足时被回收。

弱引用(Weak Reference):

  • 回收时机:当垃圾回收器发现一个对象只有弱引用时,无论内存是否充足,都会立即回收该对象。
  • 典型使用场景
    1. 引用映射:弱引用常用于 弱引用映射(WeakHashMap,这种映射允许将某些对象作为键,当这些对象不再有强引用时,它们会自动从映射中删除,避免了潜在的内存泄漏。常见用例包括:
      • 缓存中使用了弱引用键的对象,比如 Session ID、临时数据存储。
    2. 监听器或回调机制:在事件监听器或回调函数中,可以使用弱引用,确保如果监听器对象不再被其他对象引用时,它能够被自动清除,避免长时间占用内存。

软引用 vs 弱引用:

特性软引用(Soft Reference)弱引用(Weak Reference)
回收时机内存不足时才会被回收。在下次 GC 时即会被回收。
典型场景内存敏感的缓存,大型对象的管理。弱引用映射(WeakHashMap)、监听器/回调机制。
存活时间可以在内存充足时较长时间存在。很短,GC 后通常即被回收。
适合对象需要缓存但希望在内存不足时释放的对象。不需要长期持有但仍可能有用的临时对象,或者做标记用。

总结:

  • 软引用:适用于内存敏感的缓存,确保对象尽可能长时间保留,只有在内存不足时回收。
  • 弱引用:适用于希望对象被尽快回收的场景,常用于弱引用映射(WeakHashMap监听器/回调等短暂持有对象的场景。

12.ThreadLocal中为什么要使用弱引用?

当threadlocal对象不再使用时,使用弱引用可以让对象被回收;因为仅有弱引用没有强引用的情况下,对象是可以被回收的.
在 ThreadLocal 的实现中,使用了 弱引用(Weak Reference) 主要是为了避免 内存泄漏,特别是在多线程环境下。ThreadLocal 的核心设计是为了给每个线程保存独立的变量副本,而使用弱引用可以帮助及时回收不再需要的 ThreadLocal 实例。
弱引用并没有完全解决掉对象回收的问题,Entry对象value值无法被回收,所以合理的做法是手动调用remove方法进行回收,然后再将threadlocal对象的强引用解除。

ThreadLocal 的工作原理
ThreadLocal 的每个线程都有一个 ThreadLocalMap,这是一个存储该线程相关的 ThreadLocal 对象的哈希表。这个 ThreadLocalMap 的键是 ThreadLocal 实例,而值是对应的线程局部变量。这意味着 ThreadLocal 变量的生命周期是与线程绑定的。

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("someValue");

对于每个线程,它会有一个独立的 ThreadLocalMap,其中保存着当前线程与它的 ThreadLocal 变量的键值对。
为什么 ThreadLocal 使用弱引用?
在 ThreadLocalMap 中,ThreadLocal 对象作为键,而这些键是用 弱引用 来存储的。使用弱引用的原因如下:

  1. 防止内存泄漏
    如果 ThreadLocal 对象是使用 强引用 存储的,即使线程已经不再需要这个 ThreadLocal,ThreadLocalMap 依然会保持对它的引用,导致垃圾回收器无法回收这些对象,最终造成内存泄漏。
    使用 弱引用 后,当外部代码不再持有对 ThreadLocal 的引用时,垃圾回收器可以及时回收这些 ThreadLocal 实例,避免内存泄漏。
  2. 及时清理无用的 ThreadLocal
    在多线程环境下,如果某个线程长期存在,而它关联的 ThreadLocal 已经不再使用,ThreadLocalMap 中的键(ThreadLocal 实例)如果是强引用,它们将无法被回收。而通过弱引用,在垃圾回收器运行时可以清理掉不再使用的 ThreadLocal 键,而线程局部变量可以通过弱引用自动清理。
    可能的内存泄漏风险:
    虽然 ThreadLocal 的键使用了弱引用,但它的值(线程局部变量)并没有使用弱引用存储。这就意味着,即使 ThreadLocal 实例被垃圾回收了,但如果不显式清除 ThreadLocal 的值,ThreadLocalMap 中的值依然会存在,可能会导致内存泄漏。因此,需要手动调用 remove() 方法来清除不再使用的线程局部变量。
threadLocal.remove();  // 手动清理,以避免内存泄漏

简述示例:
“ThreadLocal 使用弱引用的原因是为了防止内存泄漏。当 ThreadLocal 实例不再被引用时,垃圾回收器可以及时回收 ThreadLocal 对象。如果使用强引用,ThreadLocalMap 可能会保留对不再使用的 ThreadLocal 的引用,导致内存无法释放。”
总结

  • ThreadLocal 的键使用弱引用来避免内存泄漏。
  • 当 ThreadLocal 对象不再被外部引用时,垃圾回收器可以自动清除 ThreadLocalMap 中对应的键。
  • 但是,线程局部变量(值)并不会自动清除,因此在使用 ThreadLocal 后,应该主动调用 remove() 方法来避免内存泄漏。

13.有哪些常见的垃圾回收算法?

在这里插入图片描述

1. 标记-清除算法(Mark-Sweep)

  • 原理
  1. 标记阶段:从根对象(GC Roots)出发,标记所有可达的对象。
  2. 清除阶段:遍历整个堆,回收那些未被标记的对象(即不可达的对象)。
  • 优点
    • 简单直接,不需要移动对象。
  • 缺点
    • 由于直接清除未标记对象,会造成 内存碎片,后续分配新对象可能会因为碎片化的内存无法找到连续空间而导致性能下降。
  • 适用场景:常用于老年代的垃圾回收。

2. 标记-整理算法(Mark-Compact)

  • 原理
  1. 标记阶段:与标记-清除算法类似,标记所有可达的对象。
  2. 整理阶段:将所有存活的对象压缩到堆的一端,确保内存空间连续,然后清理掉堆另一端的垃圾对象。
  • 优点
    • 消除了内存碎片,保证了连续的内存空间可用。
  • 缺点
    • 整理阶段需要移动对象,开销较大,效率低于标记-清除算法。
  • 适用场景:主要用于 老年代 的垃圾回收,尤其是需要长时间运行的系统。

3. 复制算法(Copying or Scavenge)

  • 原理
  1. 将内存划分为两个相同大小的区域,每次只使用其中一个区域。
  2. 当活动区域的内存满了时,垃圾回收器会将存活的对象复制到另一个区域中。
  3. 复制完成后,清空原区域,切换到新区域继续分配对象。
  • 优点
    • 每次只需要处理存活的对象,回收效率高
    • 无内存碎片,对象总是被压缩到一边。
  • 缺点
    • 内存利用率低,因为一半的内存始终是空闲的。
    • 如果存活对象较多,复制开销较大。
  • 适用场景:由于大多数对象很快会变为垃圾,复制算法常用于 新生代 的垃圾回收(如Eden区和Survivor区)。

4. 分代收集算法(Generational Garbage Collection)

  • 原理
    基于对象的生命周期特点,Java虚拟机将堆内存划分为 新生代老年代,分别使用不同的垃圾回收算法。具体做法是:
  • 新生代:大部分新创建的对象会存放在新生代,使用复制算法进行回收,因为大部分对象生命周期短,容易成为垃圾。
  • 老年代:在新生代存活时间较长的对象会被移到老年代,使用标记-整理或标记-清除算法来进行回收,因为这些对象生命周期长。
  • 优点
    • 结合了多种回收算法的优点,提高了垃圾回收效率和内存利用率。
  • 缺点
    • 相对复杂,需要调整新生代和老年代的比例以适应不同应用的需求。
  • 适用场景:分代收集是现代Java虚拟机最常用的垃圾回收策略。

5. 增量垃圾收集算法(Incremental GC)

  • 原理
    该算法将一次完整的垃圾回收过程分成多个小任务,间歇性地执行这些小任务,以减少每次垃圾回收的停顿时间。
  • 优点
    • 减少应用程序的停顿时间,适合实时应用。
  • 缺点
    • 回收效率较低,系统开销较大。
  • 适用场景:适合对停顿敏感的应用程序,如实时系统。

6. 分区垃圾收集算法(Region-Based GC / G1 GC)

  • 原理
    分区收集将堆内存划分成多个小的区域(Region),每个Region可以充当新生代或老年代的一部分。垃圾回收时,根据每个Region的垃圾量动态选择回收的区域,而不是对整个堆进行回收。
  • 优点
    • 减少了全局垃圾回收带来的停顿时间。
    • 动态管理内存,适合大堆内存。
  • 缺点
    • 实现复杂,系统开销较大。
  • 适用场景:适合大内存应用,尤其是需要减少全局停顿的场景。G1垃圾回收器(Garbage First, G1 GC)就是基于这种算法。
    总结
  • 标记-清除算法:简单但会产生内存碎片,适用于老年代。
  • 标记-整理算法:整理内存空间,消除碎片,适用于老年代。
  • 复制算法:效率高、无碎片,适用于新生代。
  • 分代收集算法:结合多种算法,根据对象生命周期进行垃圾回收,是现代JVM常用的回收机制。
  • 增量垃圾收集算法:减少停顿时间,适合实时应用。
  • 分区垃圾收集算法:基于内存分区,动态管理内存,适合大内存应用,如G1 GC。
    通过这些算法,Java虚拟机能够有效管理堆内存,并平衡内存回收的效率和应用程序的性能。

14.有哪些常用的垃圾回收器?

在这里插入图片描述

在这里插入图片描述

垃圾回收器的组合关系虽然很多,但是针对几个特定的版本,比较好的组合选择如下:
JDK8及之前:
ParNew + CMS关注暂停时间)、Parallel Scavenge + Parallel Old (关注吞吐量)、 G1(JDK8之前不建议,较大堆并且关注暂停时间
JDK9之后:
G1(默认)
从JDK9之后,由于G1日趋成熟,JDK默认的垃圾回收器已经修改为G1,所以强烈建议在生产环境上使用G1。
如果对低延迟有较高的要求,可以使用Shenandoah或者ZGC。

  • Serial GC单线程、简单,适合小内存应用
  • Parallel GC多线程高吞吐量适合多核机器和批处理应用。
  • CMS GC并发回收,减少老年代停顿时间,适合响应时间敏感的应用
  • G1 GC分区回收,减少全局停顿时间,适合大内存、延迟敏感应用
  • ZGCShenandoah GC:专注于低延迟大堆内存适合对延迟和响应时间要求极高的场景

1. Serial GC(串行垃圾回收器)

  • 原理: 使用单线程进行垃圾回收,所有的垃圾回收过程都是串行进行的。在进行垃圾回收时,应用线程会暂停(Stop-The-World,STW)。
  • 特点
    • 新生代:采用 复制算法
    • 老年代:采用 标记-整理算法
    • 适用于单核CPU或小内存环境。
  • 优点
    • 实现简单,开销低。
  • 缺点
    • 由于是单线程执行垃圾回收,回收时会导致较长的停顿时间。
  • 适用场景:适合单核机器或者对停顿时间不敏感的小型应用。
  • 启动参数
-XX:+UseSerialGC

2. Parallel GC(并行垃圾回收器)

  • 原理: 使用多线程来处理垃圾回收,尤其是在新生代,能够同时利用多个CPU进行回收。回收时同样会暂停应用线程(STW)。
  • 特点
    • 新生代:采用 复制算法
    • 老年代:采用 标记-整理算法
    • 注重高吞吐量,希望通过缩短总回收时间来提升应用的整体性能。
  • 优点
    • 提高了回收效率,适合多核机器。
  • 缺点
    • 仍然会导致应用线程的暂停(STW),不适合对延迟敏感的应用。
  • 适用场景:适合对吞吐量有较高要求的场景,比如批处理应用、大数据分析等。
  • 启动参数

-XX:+UseParallelGC

---

### 3. Parallel Old GC(并行老年代垃圾回收器)

- **原理**: Parallel GC 的老年代版本,使用多线程对 **老年代** 进行垃圾回收。老年代的回收算法是 **标记-整理算法**。
- **特点**:
    - 与 Parallel GC 结合使用,能够在老年代回收时减少停顿时间。
- **适用场景**:适合长时间运行、需要处理大量数据的服务器端应用,特别是希望在吞吐量上有较高要求的场景。
- **启动参数**:

```java
-XX:+UseParallelOldGC

4. CMS GC(Concurrent Mark-Sweep)

  • 原理: CMS是一种并发的垃圾回收器,它专注于缩短老年代的垃圾回收停顿时间,适用于对响应时间要求高的应用。它的垃圾回收过程分为多个阶段,其中一些阶段可以和应用线程同时进行。
  • 特点
    • 新生代:使用 复制算法,但仍然是并行进行的。
    • 老年代:使用 标记-清除算法,回收过程中会产生 内存碎片
    • 专注于减少老年代的STW停顿。
  • 优点
    • 大部分垃圾回收阶段是与应用线程并发执行的,减少了全局的停顿时间。
  • 缺点
    • CMS算法在老年代会产生内存碎片,需要频繁的 Full GC 来整理内存空间。
    • 在低内存环境下可能会出现 “Concurrent Mode Failure”(并发模式失败),从而触发长时间的Full GC。
  • 适用场景:适合对响应时间敏感的应用,如Web服务器、需要频繁响应请求的场景。
  • 启动参数
-XX:+UseConcMarkSweepGC

5. G1 GC(Garbage-First)

  • 原理: G1 GC 是一种 面向区域 的垃圾回收器,它将堆内存划分为多个小的 Region,每个Region可以作为新生代或老年代的一部分。它通过优先回收 最多垃圾的区域 来实现高效的垃圾回收,并且可以并发执行部分垃圾回收任务。
  • 特点
    • 新生代老年代 都使用分区回收,采用 标记-整理算法,不会产生内存碎片。
    • 通过 预测停顿时间,来控制垃圾回收的执行频率和回收区域,以减少STW时间。
  • 优点
    • 减少了全局停顿时间,适合大内存、高吞吐量和延迟敏感的应用。
    • 在内存碎片管理上有显著优势,避免了CMS中的内存碎片问题。
  • 缺点
    • 实现复杂,调优难度较大。
  • 适用场景:适合大内存服务器、高性能要求的应用,如大数据处理、企业级应用、游戏服务器等。
  • 启动参数
-XX:+UseG1GC

6. ZGC(Z Garbage Collector)

  • 原理: ZGC 是一种低延迟的垃圾回收器,能够处理 非常大的堆内存(TB级别)。它的主要目标是将垃圾回收对应用线程的停顿时间控制在 10ms以内,并且几乎不会随着堆的大小增长而增加停顿时间。
  • 特点
    • 基于 分区和并发回收,使用着色指针(Colored Pointers)和读屏障(Read Barriers)技术来实现低延迟。
    • 支持极大的堆内存(最大支持TB级别)。
    • 并发性极强,几乎所有的垃圾回收工作都是在应用程序线程运行时并发执行的。
  • 优点
    • 极低的停顿时间,适合对延迟要求极高的应用。
    • 支持非常大的内存空间。
  • 缺点
    • 占用的内存资源较大。
  • 适用场景:适合延迟敏感且内存空间大的应用,例如实时交易系统、在线游戏等。
  • 启动参数
-XX:+UseZGC

7. Shenandoah GC

  • 原理: Shenandoah 是一种 低延迟 垃圾回收器,几乎所有的垃圾回收任务都与应用线程并发执行。其设计目标与ZGC相似,专注于大堆内存和低延迟的场景。
  • 特点
    • 使用 Region 划分堆内存。
    • 和ZGC一样,旨在减少GC带来的停顿时间。
  • 优点
    • 并发回收,大幅减少了停顿时间。
  • 缺点
    • 在大堆内存下表现较好,但实现较复杂。
  • 适用场景:适合需要极低延迟和大堆内存的场景。
  • 启动参数
-XX:+UseShenandoahGC

总结

每种垃圾回收器有其独特的设计目标,选择合适的垃圾回收器取决于应用的内存需求、吞吐量和响应时间要求。

15.如何解决内存泄漏问题?

内存泄漏 是指程序运行过程中,某些对象由于未正确释放而持续占用内存,尽管它们已经不再使用。随着时间的推移,内存泄漏会导致系统内存逐渐耗尽,最终导致程序崩溃或性能下降。解决内存泄漏问题的关键在于及时发现、预防和排除问题。

以下是常见的内存泄漏原因及其解决方案:


1. 合理使用 try-with-resources 或手动释放资源

  • 问题:未正确关闭或释放系统资源(如文件、数据库连接、网络连接等)会导致内存泄漏。
  • 解决方案:使用 try-with-resources 自动关闭资源,或者在 finally 块中显式关闭资源。
    // try-with-resources example
    try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
        // Read file
    } catch (IOException e) {
        e.printStackTrace();
    }
    
  • 确保手动管理资源时,在不再需要资源时调用关闭方法(如 close()release() 等)。

2. 静态集合类导致的内存泄漏

  • 问题:当集合(如 HashMap, ArrayList, Set)为 static 并且对象不断添加进集合时,即使对象不再使用,它们仍然会保持在集合中,导致内存无法回收。
  • 解决方案:定期清理集合,移除不再使用的对象,或者使用弱引用(WeakHashMap)。
    // 使用 WeakHashMap 代替普通的 HashMap
    WeakHashMap<Key, Value> map = new WeakHashMap<>();
    

3. 避免内部类或匿名类持有外部类引用

  • 问题:非静态的内部类和匿名类会默认持有外部类的引用,如果它们的生命周期比外部类长,可能会导致外部类无法被垃圾回收。
  • 解决方案:使用 static 内部类,或者确保在内部类中不持有外部类的引用。
    // Static nested class avoids holding reference to the outer class
    static class StaticInnerClass {
        // ...
    }
    

4. 正确使用 ThreadLocal

  • 问题ThreadLocal 是线程局部变量,每个线程有一份独立的副本,如果不调用 remove(),会导致当前线程局部变量无法被垃圾回收,进而引发内存泄漏。
  • 解决方案:使用 ThreadLocal 时,在不再需要时调用 remove() 方法。
    threadLocal.remove();  // 手动清理
    

5. 监控和处理缓存

  • 问题:缓存数据可能会引发内存泄漏,特别是当缓存中的对象不再需要时,如果未及时清理,内存泄漏会逐渐增大。
  • 解决方案
    • 使用带有过期策略的缓存(如 LinkedHashMapGuava Cache),确保旧数据可以及时清理。
    • 使用 弱引用软引用 来缓存对象,允许垃圾回收器在内存不足时回收这些对象。
    // Example of using LinkedHashMap for cache with removal policy
    LinkedHashMap<K, V> cache = new LinkedHashMap<>(100, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > 100;  // Remove oldest entry if size exceeds 100
        }
    };
    

6. 及时清除监听器和回调

  • 问题:对象注册了监听器或回调(如事件处理程序)后,如果未能取消注册或移除它们,可能会导致这些对象无法被回收,从而导致内存泄漏。
  • 解决方案:确保在对象不再使用时取消注册事件监听器或回调。
    // Ensure to remove listener when no longer needed
    eventSource.removeEventListener(listener);
    

7. 避免过长生命周期的对象

  • 问题:将短生命周期的对象(如局部变量)存储到长生命周期的结构中(如静态变量)会导致内存泄漏,尤其当这些对象应该在短期内被回收。
  • 解决方案:避免将不需要长时间存活的对象存入静态变量或集合中。

8. 使用内存泄漏检测工具

  • 问题:有时内存泄漏的根源不容易定位。
  • 解决方案:使用工具如 VisualVMEclipse MAT(Memory Analyzer Tool)JProfiler 等来分析和查找内存泄漏,生成堆转储文件,查找长时间驻留的对象。
    • 通过分析堆转储文件可以发现哪些对象占用了大量内存,并进一步检查它们的引用链。

9. 避免过度使用全局变量

  • 问题:全局变量或 static 变量会一直保留在内存中,导致相关的对象无法被垃圾回收。
  • 解决方案:慎用全局变量,必要时在不再需要时显式清空全局对象的引用。

10. 选择合适的数据结构

  • 问题:选择不合适的数据结构会导致无效数据长期占用内存。例如,使用 LinkedList 而不是 ArrayList,当列表不再使用时,链表中的节点可能仍持有前后节点的引用,无法被回收。
  • 解决方案:根据具体需求选择最合适的数据结构,并定期清理集合中的无效数据。

11. 避免循环引用

  • 问题:如果两个对象相互引用,而外部已经不再持有它们的引用,但因为相互引用,它们无法被垃圾回收。
  • 解决方案:尽量减少对象间的循环引用,或者使用 弱引用 来解决对象间的相互引用问题。

总结

解决内存泄漏问题需要程序员谨慎管理对象的生命周期,特别是对于长时间驻留的对象和系统资源。使用合适的工具及时检测内存泄漏,并遵循良好的编码实践(如合理管理资源、使用弱引用和适当的缓存策略)可以有效防止内存泄漏的发生。

内存泄漏(memory leak):在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。
少量的内存泄漏可以容忍,但是如果发生持续的内存泄漏,就像滚雪球雪球越滚越大,不管有多大的内存迟早会被消耗完,最终导致的结果就是内存溢出。
解决内存泄漏问题总共分为四个步骤,其中前两个步骤是最核心的:
在这里插入图片描述

正常情况

在这里插入图片描述

  • 处理业务时会出现上下起伏,业务对象频繁创建内存会升高,触发MinorGC之后内存会降下来。
  • 手动执行FULL GC之后,内存大小会骤降,而且每次降完之后的大小是接近的。
  • 长时间观察内存曲线应该是在一个范围内。
    出现内存泄漏

在这里插入图片描述

  • 处于持续增长的情况,即使Minor GC也不能把大部分对象回收
  • 手动FULL GC之后的内存量每一次都在增长
  • 长时间观察内存曲线持续增长
  • 诊断 – 生成内存快照

诊断 – 生成内存快照

当堆内存溢出时,需要在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile )文件。

生成方式有两种

1、内存溢出时自动生成,添加生成内存快照的Java虚拟机参数:

-XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,自动生成hprof内存快照文件。

-XX:HeapDumpPath=< path >:指定hprof文件的输出路径。

在这里插入图片描述

发生oom之后,就会生成内存快照文件:

在这里插入图片描述

2、导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象:

通过JDK自带的jmap命令导出,格式为:

jmap -dump:live,format=b,file=文件路径和文件名 进程ID

通过arthas的heapdump命令导出,格式为:

heapdump --live 文件路径和文件名

在这里插入图片描述

诊断 – MAT定位问题

使用MAT打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源。

在这里插入图片描述

修复问题

修复内存溢出问题的要具体问题具体分析,问题总共可以分成三类:

  • 代码中的内存泄漏,由于代码的不合理写法存在隐患,导致内存泄漏

  • 并发引起内存溢出 - 参数不当,由于参数设置不当,比如堆内存设置过小,导致并发量增加之后超过堆内存的上限。解决方案:设置合理参数

  • 并发引起内存溢出 – 设计不当,系统的方案设计不当,比如:

    • 从数据库获取超大数据量的数据

    • 线程池设计不当

    • 生产者-消费者模型,消费者消费性能问题

解决方案:优化设计方案

常用的JVM工具

JDK自带的命令行工具:

jps 查看java进程,打印main方法所在类名和进程id

jmap 1、生成堆内存快照

2、打印类的直方图

第三方工具:

VisualVM 监控

Arthas 综合性工具

MAT 堆内存分析工具

监控工具:

Prometheus + grafana

16.常见的JVM参数?

参数1 : -Xmx 和 –Xms
-Xmx参数设置的是最大堆内存,但是由于程序是运行在服务器或者容器上,计算可用内存时,要将元空间、操作系统、其它软件占用的内存排除掉。
案例: 服务器内存4G,操作系统+元空间最大值+其它软件占用1.5G,-Xmx可以设置为2g。

在这里插入图片描述

最合理的设置方式应该是根据最大并发量估算服务器的配置,然后再根据服务器配置计算最大堆内存的值。

建议将-Xms设置的和-Xmx一样大,运行过程中不再产生扩容的开销。

参数2 : -XX:MaxMetaspaceSize 和 -Xss

-XX:MaxMetaspaceSize=值 参数指的是最大元空间大小,默认值比较大,如果出现元空间内存泄漏会让操作系统可用内存不可控,建议根据测试情况设置最大值,一般设置为256m。

-Xss256k 栈内存大小,如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。比如Linux x86 64位 : 1MB,如果不需要用到这么大的栈内存,完全可以将此值调小节省内存空间,合理值为256k – 1m之间。

参数3:-Xmn 年轻代的大小

默认值为整个堆的1/3,可以根据峰值流量计算最大的年轻代大小,尽量让对象只存放在年轻代,不进入老年代。但是实际的场景中,接口的响应时间、创建对象的大小、程序内部还会有一些定时任务等不确定因素都会导致这个值的大小并不能仅凭计算得出,如果设置该值要进行大量的测试。G1垃圾回收器尽量不要设置该值,G1会动态调整年轻代的大小。

在这里插入图片描述

打印GC日志

JDK8及之前 : -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:文件路径
JDK9及之后 : -Xlog:gc*:file=文件路径
-XX:+DisableExplicitGC

禁止在代码中使用System.gc(), System.gc()可能会引起FULLGC,在代码中尽量不要使用。使用DisableExplicitGC参数可以禁止使用System.gc()方法调用。

-XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,自动生成hprof内存快照文件。
-XX:HeapDumpPath=< path >:指定hprof文件的输出路径。

JVM参数模板:

-Xms1g-Xmx1g-Xss256k
-XX:MaxMetaspaceSize=512m 
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/dumps/my-service.hprof
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-Xloggc:文件路径

注意:
JDK9及之后gc日志输出修改为 -Xlog:gc*:file=文件名
堆内存大小和栈内存大小根据实际情况灵活调整。
在配置 Java 虚拟机(JVM)时,JVM 参数 是非常重要的。JVM 提供了大量的参数来控制内存管理、垃圾回收、性能调优、日志输出等方面的行为。以下是一些常见的 JVM 参数,以及它们的功能。


1. 堆内存相关参数

JVM 堆是运行时内存分配的重要部分,以下是常见的堆内存设置参数:

  • -Xms:设置 JVM 启动时的 初始堆大小
    • 例如:-Xms512m 表示初始堆大小为 512MB。
  • -Xmx:设置 JVM 允许的 最大堆大小
    • 例如:-Xmx2048m 表示最大堆大小为 2GB。
  • -Xmn:设置 新生代 内存大小。堆内存分为新生代和老年代,新生代用于存放短期生命周期的对象。
    • 例如:-Xmn512m 表示新生代的大小为 512MB。
  • -XX:NewRatio:设置 新生代老年代 之间的比例。默认为 2,表示新生代占堆大小的 1/3,老年代占 2/3。
    • 例如:-XX:NewRatio=3 表示新生代占堆的 1/4,老年代占 3/4。
  • -XX:SurvivorRatio:设置 Eden 区Survivor 区 的比例。新生代包含一个 Eden 区和两个 Survivor 区。
    • 例如:-XX:SurvivorRatio=8 表示 Eden 和 Survivor 的比例是 8:1。
  • -XX:MaxPermSize:设置 永久代(在 Java 8 之前用于存放类元数据)的最大值。
    • 例如:-XX:MaxPermSize=128m 表示永久代最大为 128MB。Java 8 之后被 Metaspace 取代。
  • -XX:MetaspaceSize-XX:MaxMetaspaceSize:设置 元空间 的初始大小和最大大小(Java 8 之后取代永久代)。

2. 垃圾回收器相关参数

  • -XX:+UseSerialGC:启用 Serial GC,单线程垃圾回收器,适用于小型应用或单核环境。
  • -XX:+UseParallelGC:启用 Parallel GC,多线程的垃圾回收器,注重吞吐量。
  • -XX:+UseParallelOldGC:启用并行的老年代垃圾回收器,与 -XX:+UseParallelGC 配合使用。
  • -XX:+UseConcMarkSweepGC:启用 CMS(Concurrent Mark-Sweep) 垃圾回收器,降低老年代的回收停顿时间。
  • -XX:+UseG1GC:启用 G1 GC,一种适合大堆内存和延迟敏感场景的垃圾回收器,具有较低的暂停时间。
  • -XX:+UseZGC:启用 Z Garbage Collector,一种低延迟的垃圾回收器,适合大内存场景,几乎没有停顿。
  • -XX:+UseShenandoahGC:启用 Shenandoah GC,一种并发的低延迟垃圾回收器。
  • -XX:ParallelGCThreads:设置 Parallel GC 的垃圾回收线程数。
    • 例如:-XX:ParallelGCThreads=4 表示使用 4 个线程进行垃圾回收。
  • -XX:MaxGCPauseMillis:设置垃圾回收的最大暂停时间,通常用于 G1 垃圾回收器。
    • 例如:-XX:MaxGCPauseMillis=200 表示垃圾回收的最大停顿时间为 200 毫秒。
  • -XX:+PrintGCDetails:启用垃圾回收的详细日志输出,用于调试和优化垃圾回收。
  • -XX:+PrintGCDateStamps:输出垃圾回收日志时包含时间戳。
  • -Xloggc:<filename>:将 GC 日志输出到指定文件。

3. 类加载和运行时优化

  • -XX:+TieredCompilation:启用分层编译,结合了解释执行和 JIT 编译的优点。
  • -XX:CompileThreshold:设置 方法被编译为本地代码 的调用次数阈值。JIT 编译器在方法调用达到一定次数后,将其编译为本地代码。
    • 例如:-XX:CompileThreshold=1000 表示方法被调用 1000 次后会被编译。
  • -XX:+UseCompressedOops:启用压缩指针(64 位 JVM 默认启用)。压缩对象指针可以节省内存空间。
  • -XX:+AlwaysPreTouch:在 JVM 启动时预先分配内存,避免在运行过程中动态分配。

4. 错误排查与调试

  • -XX:+HeapDumpOnOutOfMemoryError:在内存溢出(OOM)时生成堆转储文件,方便调试内存问题。
  • -XX:HeapDumpPath=<file-path>:设置 OOM 时堆转储文件的保存路径。
  • -XX:+PrintFlagsFinal:打印所有 JVM 参数的最终值,包括默认参数和手动设置的参数。
  • -XX:+PrintCompilation:打印 JIT 编译的相关信息,帮助分析哪些方法被编译成了本地代码。
  • -Xprof:启用轻量级的 CPU 性能分析。
  • -XX:+TraceClassLoading:打印类加载信息。
  • -XX:+TraceClassUnloading:打印类卸载信息。

5. 线程栈设置

  • -Xss:设置每个线程的栈大小。较小的栈大小可以让程序创建更多线程。
    • 例如:-Xss512k 表示每个线程的栈大小为 512KB。

6. 其他常见参数

  • -server:强制 JVM 以 服务器模式 运行(默认启用 JIT 编译,适合长时间运行的服务端应用)。
  • -client:强制 JVM 以 客户端模式 运行(适合短时间运行的桌面应用,启动时间较快,但执行效率稍低)。
  • -XX:MaxDirectMemorySize:设置 JVM 可以使用的最大直接内存大小(用于 NIO)。
  • -Dproperty=value:设置 JVM 系统属性(例如,设置代理服务器,-Dhttp.proxyHost=proxy.example.com)。

JDK 版本

JDK 8:
• 传统的 双亲委派模型 仍然适用。
• 类加载器机制无显著变化,继续使用 Bootstrap ClassLoader、Extension ClassLoader、System ClassLoader。

JDK 9:
模块系统(JPMS):引入模块化系统,类加载按模块边界加载,打破了单一类路径机制。
PlatformClassLoader:新增平台类加载器,用于加载 JDK 内部模块。

JDK 11:
• 模块系统的进一步优化。
• 移除了对 rt.jar 的依赖,所有核心类打包在模块中。

JDK 17:
• 继续改进模块系统和类加载性能,但类加载架构保持不变。
总结:

JDK 9 是类加载器机制的最大变革,主要通过模块系统增强了类加载的灵活性与隔离性。

jdk版本新特性

这里是 Java JDK 8 之后一些主流版本的重要新特性:

JDK 8:
Lambda 表达式:引入了函数式编程支持。
Streams API:简化集合数据的处理。
接口中的默认方法:接口可以包含带有实现的方法。
新日期时间 API:java.time 包提供更好的日期时间处理。
Nashorn JavaScript 引擎:用于在 JVM 上执行 JavaScript 代码。
PermGen 区移除:用 Metaspace 取代了永久代。

JDK 9:
模块化系统(JPMS):通过模块系统组织代码和依赖,提升了大型应用的维护性和性能。
jshell:交互式命令行工具,允许用户快速测试 Java 代码。
改进的 Stream API:增加了新的操作如 takeWhile、dropWhile 等。
多版本 JAR 文件:允许在同一个 JAR 文件中包含不同 JDK 版本的类文件。

JDK 10:
局部变量类型推断:引入 var 关键字,简化局部变量声明。
G1 垃圾回收器默认化:G1 成为默认垃圾回收器。
内存管理优化:提高了堆内存的使用效率。

JDK 11:
长期支持(LTS)版本
新 HTTP 客户端 API:支持异步和同步的 HTTP 请求。
Lambda 表达式局部变量语法增强:var 可以用于 Lambda 表达式的参数。
移除 Nashorn:弃用了 Nashorn JavaScript 引擎。
字符串处理增强:如 String::isBlank,strip,lines 和 repeat 方法。

JDK 12:
Switch 表达式(预览):switch 可以返回值,并支持简化的 case 语法。
Shenandoah GC:新引入的低暂停垃圾回收器。
JVM 常量 API:改善类文件的常量池处理。

JDK 13:
文本块(预览):支持多行字符串,简化了处理文本的格式化。
Switch 表达式正式版:Switch 表达式进一步完善,提升代码简洁性。

JDK 14:
记录(预览):引入了 record,简化了不可变数据类的定义。
NullPointerException 改进:NPE 的错误提示更详细,易于调试。
Switch 表达式正式版:成为正式特性。

JDK 15:
文本块正式版:多行字符串支持正式发布。
隐藏类:用于框架和库生成类的动态加载。

JDK 16:
记录正式版:record 成为正式语言特性。
强封装 JDK 内部 API:加强了模块系统的封装性。

JDK 17:
长期支持(LTS)版本
Sealed 类:允许限制哪些类可以扩展或实现某个类。
模式匹配(instanceof):简化了 instanceof 后的类型转换。
ZGC 改进:将 ZGC 转化为生产就绪状态,支持并发处理垃圾回收。

总结:
Java 的每个新版本都引入了增强语言表达能力、改进性能和提升开发体验的新特性。JDK 8 和 JDK 9 具有重大变革,JDK 11 和 JDK 17 是长期支持版本(LTS)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值