JVM面试题

JVM组成

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

Java虚拟机(JVM)
├── 类加载器(ClassLoader):主要负责将类的字节码文件加载到内存中,并在需要时进行连接和初始化
│   ├── Bootstrap ClassLoader:启动类加载器,加载核心类库
│   ├── Extension ClassLoader:扩展类加载器,加载扩展类库
│   |── Application Class Loader:系统类加载器,加载应用程序类
|   |── Custom Class Loader: 自定义类加载器
├── 运行时数据区(Runtime Data Area)
│   ├── 方法区(Method Area):存储类的结构信息、静态变量、常量、方法信息等
│   ├── 堆(Heap):存储对象实例和数组
│   ├── 栈(Stack):存储方法调用的局部变量、方法参数、返回值和方法调用的返回地址
│   ├── 程序计数器(Program Counter):记录当前线程正在执行的字节码指令地址
│   └── 本地方法栈(Native Method Stack):存储本地方法的参数和局部变量
├── 执行引擎(Execution Engine)
│   |
│   ├── 即时编译器(Just-In-Time Compiler,JIT Compiler):将Java字节码编译为本地机器代码
|   ├── 垃圾回收器(Garbage Collector)
│          ├── Serial GC:串行垃圾回收器
│          ├── Parallel GC:并行垃圾回收器
│          ├── CMS GC:CMS垃圾回收器
│          └── G1 GC:G1垃圾回收器
│
├── 本地方法接口(Native Interface)
|     └── Java Native Interface(JNI):允许Java代码和本地代码之间进行交互
|
├── 本地方法库(Native Method Library):包含一组本地方法的库


JVM(Java虚拟机)是Java语言的核心部分,它是一个虚拟的计算机,负责将Java字节码文件解释执行或编译成机器码。JVM的主要组成部分和作用如下:

1. 类加载器(ClassLoader):负责加载字节码文件(.class文件)并转换为JVM内部使用的数据结构。
   ●作用:将字节码文件加载到JVM中,并创建对应的类对象,以便后续的执行。

2. 运行时数据区(Runtime Data Area):是JVM的内存区域,包括方法区、堆、栈、程序计数器、本地方法栈等。
   ●作用:用于存放程序运行期间所需的数据,包括类的元数据、对象实例、方法的字节码等。

3. 执行引擎(Execution Engine):负责解释执行或编译执行字节码文件。
   ●作用:将字节码文件转换为机器码并执行,实现Java程序的运行。
   执行引擎包括即时编译器和垃圾回收器
   即时编译器(Just-In-Time Compiler,JIT):负责将字节码文件编译成机器码。
   ●作用:提高Java程序的运行效率,减少解释执行的时间。  
   
   垃圾回收器(Garbage Collector):负责回收无用的对象和释放内存。
   ●作用:自动回收无用的对象,防止内存泄漏和内存溢出。   

4. 本地方法库(Native Method Library):包含一组本地方法的库。
   ●作用:实现本地方法接口中的本地方法,与底层的操作系统和硬件进行交互。
5. 本地方法接口(Native Interface):允许Java应用程序调用本地代码(如C语言)。
   ●作用:与底层的操作系统和硬件进行交互,实现Java程序与本地代码的互操作。

6. 垃圾回收器(Garbage Collector):负责回收无用的对象和释放内存。
   ●作用:自动回收无用的对象,防止内存泄漏和内存溢出。

在这里插入图片描述

JVM 类加载相关

类加载器分类

类加载器(Class Loader)是Java虚拟机(JVM)的一部分,负责加载Java类文件到JVM中并生成对应的Class对象。类加载器是Java中实现动态类加载和动态类生成的基础,是Java语言的一项重要特性。

JVM类加载器分类
    ●启动类加载器(Bootstrap Class Loader)
        ●加载Java核心类库(rt.jar)
        ●使用C++实现,不是Java类
        ●JVM启动时由C++实现的类加载器加载
    ●扩展类加载器(Extension Class Loader)
        ●加载Java扩展类库(ext/*.jar)
        ●是sun.misc.Launcher$ExtClassLoader的实例
        ●在jre/lib/ext目录下的jar包会被它加载
    ●应用程序类加载器(Application Class Loader)
        ●加载应用程序的类路径上的类(classpath)
        ●是sun.misc.Launcher$AppClassLoader的实例
        ●在环境变量CLASSPATH指定的路径下的类会被它加载
    ●自定义类加载器(Custom Class Loader)
        ●通过继承ClassLoader类或URLClassLoader类实现
        ●可以加载自定义的类或动态加载类



在Java中,类加载器主要有以下几种类型:

启动类加载器(Bootstrap Class Loader):也称为根类加载器,是JVM的一部分,负责加载JVM运行时环境中的核心类库,如java.lang包中的类。启动类加载器是用本地代码实现的,不是Java类,因此无法在Java中直接获取到它的引用。

扩展类加载器(Extension Class Loader):扩展类加载器是sun.misc.Launcher$ExtClassLoader类的实例,负责加载JVM运行时环境中的扩展类库,如java.ext.dirs指定的目录中的类。

系统类加载器(Application Class Loader):系统类加载器是sun.misc.Launcher$AppClassLoader类的实例,负责加载JVM运行时环境中的应用程序类,即应用程序类路径(classpath)中指定的类。

自定义类加载器(Custom Class Loader):除了JVM提供的默认类加载器外,开发者还可以根据需要自定义类加载器,实现自己的类加载逻辑。自定义类加载器需要继承java.lang.ClassLoader类,并重写findClass方法或loadClass方法。

在Java中,类加载器采用双亲委派模型(Parent Delegation Model)进行类加载。即当需要加载一个类时,先由当前类加载器尝试加载,如果当前类加载器无法加载,则委派给父类加载器尝试加载,依次递归,直到找到合适的类加载器或无法加载类。这种模型可以保证类的唯一性,避免类被重复加载。

Java类加载过程

Java虚拟机的类加载机制是Java语言的一个重要特性,它负责在运行时加载、连接和初始化类。类加载机制的主要目标是实现Java语言的跨平台特性和动态扩展性。它由以下步骤组成:

Java类加载过程
├── 加载(Loading):查找并加载字节码文件(.class文件),通过类的全限定名查找类文件,将类文件加载到内存中
│   ├── 查找并加载字节码文件(.class文件)通过一个类的全限定名来获取定义此类的二进制字节流。
│   ├── 将字节流代表的静态存储结构转化为方法区的运行时数据结构。
│   └── 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
│
├── 链接(Linking)
│   ├── 验证(Verification)检查类文件的格式、语义等是否符合Java规范
│   │   ├── 文件格式验证:验证字节流是否符合Class文件格式的规范。
│   │   ├── 元数据验证:对字节码进行语义分析,保证其符合Java语言规范。
│   │   ├── 字节码验证:检查字节码流是否包含不正确的或不合法的代码。
│   │   └── 符号引用验证:确保解析后的符号引用可以正确链接到目标。
│   │
│   ├── 准备(Preparation):为类的静态变量分配内存并初始化默认值
│   │   ├── 为类变量分配内存并设置默认初始值(0、null等)。
│   │   └── 这里的“准备”阶段不会真正初始化类变量的值。
│   │
│   └── 解析(Resolution):将类、接口、字段和方法的符号引用解析为直接引用
│       ├── 将常量池中的符号引用替换为直接引用。
│       └── 解析过程主要针对类或接口、字段、类方法、接口方法、方法类型等。
│
│── 初始化(Initialization)为类的静态变量赋予正确的初始值、执行静态初始化器和执行静态变量赋值语句
│    ├── 类初始化时机:包括类被首次主动使用(如创建实例、调用静态方法、访问静态字段等)时
│    └── 类初始化的执行顺序:类初始化时,先初始化父类,然后再初始化子类。
│
│── 使用(Using)通过类的引用来使用该类
│   ├── 创建类的实例
│   ├── 调用类的方法
│   └── 访问类的字段
└── 卸载

 
 
1. 加载(Loading):
   ●加载是指通过类的全限定名来查找并加载类的二进制数据。
   ●加载的来源包括文件系统、网络等。
   ●加载后,类的二进制数据存放在方法区中。

2. 链接(Linking):链接包括验证、准备和解析这三个阶段。
   ●验证是确保类的二进制数据符合Java虚拟机规范的过程,包括检查字节码的语义、类型等。
   ●准备是为类的静态变量分配内存并初始化默认值的过程。
   ●解析是将类、接口、字段和方法的符号引用解析为直接引用的过程。

3. 初始化(Initialization):
   ●初始化是为类的静态变量赋予正确的初始值、执行静态初始化器和执行静态变量赋值语句的过程。
   ●类初始化的时机包括类被首次主动使用(如创建实例、调用静态方法、访问静态字段等)时。
   主要通过执行类构造器的<client>方法为类进行初始化。<client>方法是在编译阶段由编译器自动收集类中静态语句块和变量的赋值操作组成的。JVM规定,只有在父类的<client>方法都执行成功后,子类中的<client>方法才可以被执行。在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成<client>方法。
   在发生以下几种情况时,JVM不会执行类的初始化流程:
◎ 常量在编译时会将其常量值存入使用该常量的类的常量池中,该过程不需要调用常量所在的类,因此不会触发该常量类的初始化。
◎ 在子类引用父类的静态字段时,不会触发子类的初始化,只会触发父类的初始化。
◎ 定义对象数组,不会触发该类的初始化。
◎ 在使用类名获取Class对象时不会触发类的初始化。
◎ 在使用Class.forName加载指定的类时,可以通过initialize参数设置是否需要对类进行初始化。
◎ 在使用ClassLoader默认的loadClass方法加载类时不会触发该类的初始化。

4. 使用(Using):
   ●类加载完成后,Java虚拟机可以通过类的引用来使用该类。
   ●使用包括创建类的实例、调用类的方法、访问类的字段等。

5. 卸载(Unloading):
   ●当类加载器不再需要某个类时,可以通过垃圾收集器卸载类的二进制数据和相关的数据结构。

Java虚拟机的类加载机制具有很强的动态性和可扩展性,它允许在运行时加载、链接和初始化类,从而实现了Java语言的跨平台特性和动态扩展性。

什么是双亲委派模型?作用?

双亲委派模型(Parent Delegation Model)是 Java 类加载机制中的一种设计模式。它的作用是确保 Java 虚拟机(JVM)在加载类时按照一定的规则来查找和加载类,从而保证类的唯一性和安全性。

双亲委派模型的基本原理是:当一个类需要被加载时,首先会查找它的父类加载器(Parent Class Loader)是否已经加载了该类。
如果父类加载器已加载了该类,则直接返回该类的 Class 对象。如果父类加载器没有加载该类,则该类加载器会委派给它的父类加载器来加载该类。
这样一层层地向上查找,直到找到已经加载了该类的类加载器,或者到达了顶层的类加载器(Bootstrap Class Loader)。

双亲委派模型的作用主要有以下几点:

1. 类的唯一性:通过双亲委派模型,可以确保同一个类不会被同一个类加载器加载多次,从而保证了类的唯一性。

2. 类的安全性:通过双亲委派模型,可以防止恶意代码通过自定义类加载器来加载系统类,从而保证了类的安全性。

3. 类的共享性:通过双亲委派模型,可以实现类的共享,即一个类只需要被加载一次,多个类加载器可以共享同一个类的 Class 对象,从而节省了内存空间。

总的来说,双亲委派模型通过一种分层委派的方式来加载类,保证了类的唯一性、安全性和共享性,是 Java 类加载机制的一个重要设计模式。

双亲委派类加载机制的类加载流程如下,如图所示。

(1)将自定义加载器挂载到应用程序类加载器。

(2)应用程序类加载器将类加载请求委托给扩展类加载器。

(3)扩展类加载器将类加载请求委托给启动类加载器。

(4)启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由扩展类加载器加载。

(5)扩展类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由应用程序类加载器加载。

(6)应用程序类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由自定义加载器加载。

(7)在自定义加载器下查找并加载用户指定目录下的Class文件,如果在自定义加载路径下未找到目标Class文件,则抛出ClassNotFoud异常。

在这里插入图片描述

如何打破双亲委派机制

打破双亲委派机制意味着在 Java 的类加载过程中,绕过默认的双亲委派机制,自定义类加载器,以实现更灵活的类加载策略。
通常,打破双亲委派机制用于特定场景,例如实现热部署、动态更新等需求。

以下是一些在 Java 中打破双亲委派机制的方法:

1. 自定义类加载器: 实现自定义的类加载器,并在加载类的过程中修改默认的委派策略。你可以继承 java.lang.ClassLoader 类并覆写其中的 findClass() 方法或者 loadClass() 方法来实现自定义的类加载逻辑。在自定义的类加载器中,你可以根据具体需求修改双亲委派的行为,例如在父类加载器加载失败时自行加载类。

2. Java Instrumentation API: 使用 Java Instrumentation API,可以在类加载的过程中动态修改字节码,从而实现自定义的类加载逻辑。通过 Instrumentation API,你可以在类被加载到 JVM 之前修改类的字节码,并返回修改后的字节码,实现类加载的定制化。

3. Thread Context Classloader: Java 中的线程上下文类加载器(Thread Context Classloader)可以用于在特定线程中使用自定义的类加载器。通过设置线程上下文类加载器,你可以实现特定线程中的类加载逻辑,而不受双亲委派机制的限制。

4. OSGi(Open Service Gateway initiative): OSGi 是一个基于 Java 的动态模块化系统,它提供了一种打破双亲委派机制的机制。在 OSGi 中,每个模块(Bundle)都有自己的类加载器,可以独立加载和管理类,从而实现更灵活的模块化开发。

需要注意的是,打破双亲委派机制可能导致类加载的混乱和冲突,因此在使用时需要谨慎考虑,并确保不会破坏 Java 类加载的基本原则和安全性。

OSGi(Open Service Gateway Initiative,开放服务网关倡议)是一个Java平台的动态模块化系统,它提供了一种用于构建、部署和管理模块化应用程序的框架。OSGi框架将应用程序拆分为多个可重用的模块(bundles),并提供了一种用于动态加载、卸载和更新模块的机制。OSGi框架提供了一种基于服务的架构,使得模块之间可以相互通信、协作和交互。

OSGi框架的主要特点包括:

  1. 动态模块化: OSGi框架将应用程序拆分为多个可重用的模块(bundles),并提供了一种用于动态加载、卸载和更新模块的机制。这使得应用程序可以更灵活地部署和管理。

  2. 服务架构: OSGi框架提供了一种基于服务的架构,使得模块之间可以相互通信、协作和交互。每个模块都可以提供一个或多个服务,并且可以使用其他模块提供的服务。

  3. 模块化容器: OSGi框架提供了一个模块化容器,用于加载、管理和执行模块。容器负责加载模块的类和资源,并提供了一种安全的隔离机制,使得模块之间可以相互独立运行。

  4. 动态更新: OSGi框架支持动态更新模块,可以在运行时更新模块的代码和资源,而不需要停止应用程序。这使得应用程序可以更灵活地进行升级和维护。

  5. 服务注册和发现: OSGi框架提供了一种服务注册和发现的机制,使得模块可以动态地注册和发现服务。这使得模块之间可以更灵活地进行通信和协作。

  6. 安全和隔离: OSGi框架提供了一种安全和隔离的机制,使得模块之间可以相互独立运行,而不会相互影响。容器负责加载和执行模块的代码和资源,并提供了一种安全的隔离机制。

总的来说,OSGi框架是一个灵活、可扩展和可配置的模块化系统,适用于构建和部署各种类型的应用程序。通过OSGi框架,开发人员可以更灵活地构建、部署和管理模块化应用程序,并且可以更容易地进行升级和维护。

Java 程序的运行过程是怎样的?

Java程序的运行过程
    ●编写源代码
        ●Java程序员编写Java源代码,保存为.java文件
    ●编译
        ●使用Java编译器(javac)将Java源代码编译为字节码文件(.class文件)
    ●加载
        ●Java虚拟机(JVM)的类加载器负责加载字节码文件
        ●类加载器将字节码文件加载到JVM的方法区(Method Area)中,并生成对应的Class对象
    ●链接
        ●链接阶段包括验证、准备和解析三个步骤
            ●验证:验证字节码文件的合法性
            ●准备:为类的静态变量分配内存空间,并给静态变量赋默认值
            ●解析:将类、方法、字段等符号引用解析为直接引用
    ●初始化
        ●初始化阶段执行类的静态变量赋值和静态代码块
        ●如果有父类,会先初始化父类
    ●执行
        ●执行Java程序的主方法(main方法)
        ●JVM会从main方法开始执行,依次执行程序中的语句
    ●垃圾回收
        ●Java程序运行过程中,JVM会不断进行垃圾回收
        ●垃圾回收器会检查堆内存中的对象,找出不再被引用的对象,并回收它们所占用的内存空间
    ●终止
        ●当程序执行完毕或出现异常时,JVM会终止程序的执行,并释放程序所占用的内存空间

内存相关

JVM的运行时数据区组成是什么

JVM的运行时数据区(Runtime Data Area)是Java虚拟机在运行时用于存储数据的区域,

JVM的运行时数据区组成
    ●程序计数器(Program Counter Register)
        ●线程私有
        ●存储当前线程执行的字节码指令地址
    ●Java虚拟机栈(Java Virtual Machine Stacks)
        ●线程私有
        ●每个线程都有一个栈,用于存储方法的局部变量、操作数栈、动态链接、方法出口等信息
    ●本地方法栈(Native Method Stack)
        ●线程私有
        ●用于执行本地方法的栈
    ●Java堆(Java Heap)
        ●所有线程共享
        ●存放Java对象实例
    ●方法区(Method Area)
        ●所有线程共享
        ●存放类的元数据、静态变量、常量池等信息
    ●运行时常量池(Runtime Constant Pool)
        ●方法区的一部分
        ●存放编译期生成的字面量和符号引用
    ●直接内存(Direct Memory)
        ●不属于JVM的运行时数据区
        ●但是在一些情况下,JVM会使用它来提供更高效的I/O操作


JVM的运行时数据区是Java虚拟机在运行时用于存储数据的区域,它包含了程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、运行时常量池和直接内存等部分。这些数据区域在JVM运行时会根据需要动态分配和回收内存。

在这里插入图片描述

线程私有区域的生命周期与线程相同,随线程的启动而创建,随线程的结束而销毁。在JVM内,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存在与否和本地线程的启动和销毁对应

线程共享区域随虚拟机的启动而创建,随虚拟机的关闭而销毁。

直接内存也叫作堆外内存,它并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。JDK的NIO模块提供的基于Channel与Buffer的I/O操作方式就是基于堆外内存实现的,NIO模块通过调用Native函数库直接在操作系统上分配堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用对内存进行操作,Java进程可以通过堆外内存技术避免在Java堆和Native堆中来回复制数据带来的资源占用和性能消耗,因此堆外内存在高并发应用场景下被广泛使用(Netty、Flink、HBase、Hadoop都有用到堆外内存)

方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢

在 Java 中,每个对象都有一个特殊的字段称为 vtable(虚函数表)或者 dispatch table(调度表)。
vtable 是一个指向一个类的方法表的指针,它指向该对象的类的方法表。
方法表是一个数组,包含了类的所有方法的地址。
当一个对象调用一个方法时,Java 虚拟机会根据 vtable 中的指针查找方法表,并调用相应的方法。

当一个对象被创建时,Java 虚拟机会在对象的头部分配一块内存,用于存储对象的元信息,包括对象的类的指针和其他信息。
对象的类的指针是一个指向对象的类的元信息的指针,它指向该对象的类的元信息。
对象的类的元信息包括了类的结构信息、常量池、静态变量等。
当一个对象调用一个方法时,Java 虚拟机会根据对象的类的指针查找类的元信息,并从中获取 vtable 的指针。然后,Java 虚拟机会根据 vtable 的指针查找方法表,并调用相应的方法。

需要注意的是,对象的类的指针是在对象创建时由 Java 虚拟机自动设置的,程序员无法直接操作它。
对象的类的指针指向的是对象的类的元信息,而不是对象的类的定义。
对象的类的定义是一个模板,用于创建对象的实例。
对象的类的元信息包括类的结构信息、常量池、静态变量等。对象的类的定义和对象的类的元信息都是类的信息,但它们是不同的概念。
对象的类的定义是一个模板,用于创建对象的实例,而对象的类的元信息是对象的类的元信息,包括类的结构信息、常量池、静态变量等。

Java内存如何分配

Java内存分配
├── 堆内存
│   ├── 新生代
│   │   ├── Eden区
│   │   ├── Survivor区
│   │   └── To Survivor区
│   ├── 老年代
│   └── 永久代(JDK8之前)/元空间(JDK8及以后)
├── 栈内存
│   ├── 线程栈
│   ├── 本地方法栈
│   └── 操作数栈
|
│── 方法区
|
|── 程序计数器
│
└── 本地方法栈
  1. 方法区(Method Area):
    • 作用:用于存储类的元数据信息,例如类的字节码、字段信息、方法信息等。
    • 特点:方法区是线程共享的内存区域,因此只需要一个方法区来存储所有的类元数据信息。
  2. 堆(Heap):
    • 作用:用于存储对象实例和数组对象。
    • 特点:堆是线程共享的内存区域,因此所有线程都可以访问堆中的对象实例。
  3. 栈(Stack):
    • 作用:用于存储局部变量表、操作数栈和方法返回地址。
    • 特点:栈是线程私有的内存区域,每个线程都有自己的栈空间。
  4. 程序计数器(Program Counter):
    • 作用:用于存储当前线程执行的字节码指令地址。
    • 特点:程序计数器是线程私有的内存区域,每个线程都有自己的程序计数器。
  5. 本地方法栈(Native Method Stack):
    • 作用:用于存储本地方法的调用栈。
    • 特点:本地方法栈是线程私有的内存区域,每个线程都有自己的本地方法栈。
  6. 直接内存(Direct Memory):
    • 作用:用于存储NIO缓冲区。
    • 特点:直接内存不受JVM内存管理的限制,因此可以在程序中直接操作。

Java 堆和栈的区别是什么

Java 堆和栈的区别
    堆
        ●存储对象实例
        ●由垃圾收集器管理
        ●大小固定,通过-Xms 和 -Xmx 控制
        ●访问速度较慢
        ●所有线程共享
        
    栈
        ●存储基本数据类型、对象引用、方法调用等信息
        ●由 JVM 管理
        ●大小取决于操作系统限制
        ●访问速度较快
        ●每个线程独立
 

区别:

  1. 内存分配:

    • 堆内存用于存储对象实例,大小在程序启动时固定,可通过-Xms和-Xmx设置。
    • 栈内存用于存储基本数据类型、对象引用和方法调用等信息,大小取决于操作系统的限制。
  2. 内存管理:

    • 堆内存由垃圾收集器管理,会在对象不再被引用时回收。
    • 栈内存由JVM管理,在方法调用结束时自动释放栈帧所占用的内存。
  3. 存储内容:

    • 堆内存存储对象实例,这些对象在堆中被创建和销毁。
    • 栈内存存储基本数据类型的值、对象的引用和方法调用等信息,这些信息在方法调用结束时被销毁。
  4. 访问速度:

    • 堆内存访问速度较慢,因为它的大小固定且较大。
    • 栈内存访问速度较快,因为它的大小较小且分配在CPU上。
  5. 线程独立性:

    • 堆内存是所有线程共享的,一个线程创建的对象可以被其他线程访问。
    • 栈内存是每个线程独立的,一个线程的栈内存中的信息不会被其他线程访问。

Java堆内存

Java堆是Java虚拟机管理的内存中最大的一块,它是所有线程共享的内存区域。
Java堆主要用于存放对象实例

特点:
1. 对象存储:Java堆主要用于存放Java程序中创建的对象实例。
2. 自动内存管理:Java堆的内存由Java虚拟机自动分配和回收,不需要程序员手动管理。
3. 垃圾回收:Java堆的内存是垃圾收集器进行垃圾回收的最主要的内存区域,Java虚拟机会定期扫描堆中的对象,将不再被引用的对象进行回收,释放内存空间。
4. 分代设计:Java堆一般会分为新生代、老年代和永久代(或元空间),采用不同的垃圾回收算法和策略。
5. 堆大小配置: -Xms:设置初始堆大小,-Xmx:设置最大堆大小

内存结构:
1. 新生代(Young Generation):存放新创建的对象。新生代又被分为三个区域:
   ●Eden Space:最初存放新对象的区域。
   ●Survivor Space 0 和 Survivor Space 1:存放从Eden Space中复制过来的存活对象。
2. 老年代(Old Generation):存放已经存活一段时间的对象。
3. 永久代(PermGen)(在Java 8之前):存放类的元数据和静态变量。
4. 元空间(Metaspace)(Java 8及以后版本):存放类的元数据和静态变量。
5. 代码缓存(Code Cache):存放编译后的本地机器代码。


Java堆(Java Heap)
│
├── 新生代(Young Generation)
│   ├── Eden Space
│   ├── Survivor Space 0
│   └── Survivor Space 1
│
├── 老年代(Old Generation)
│
├── 永久代(PermGen)(在Java 8之前)
│
├── 元空间(Metaspace)(Java 8及以后版本)
│
└── 代码缓存(Code Cache)


使用场景:
1. Java堆主要用于存放对象实例,因此在Java程序中,几乎所有的对象都存放在Java堆中。
2. Java堆的内存分配和回收由Java虚拟机自动管理,程序员无需手动管理Java堆的内存。

堆为什么进行分代设计

Java虚拟机的堆内存进行分代设计是为了更好地管理内存,提高程序的性能。分代设计基于以下两个假设:

1. 新生代对象生命周期短:大多数新创建的对象的生命周期都很短,它们会很快变成垃圾。因此,对于这些对象,分配和回收内存的开销应该尽可能小。

2. 老年代对象生命周期长:相反,一些对象的生命周期可能很长,它们会在程序的整个生命周期中被使用。对于这些对象,分配和回收内存的开销相对较小,因为它们不会频繁地被创建和销毁。

基于以上两个假设,Java堆内存通常被分为三代:

1. Young Generation:存放新创建的对象。这一代的特点是对象的生命周期短,因此垃圾回收的频率较高,采用的算法也比较简单。常见的算法有复制算法和标记-清除算法。

2. Old Generation:存放已经存活一段时间的对象。这一代的特点是对象的生命周期长,因此垃圾回收的频率较低,采用的算法相对复杂。常见的算法有标记-清除算法、标记-整理算法和分代收集算法。

3. Perm Generation(在Java 8之前):存放类的元数据和静态变量。这一代的特点是对象的生命周期非常长,甚至可能与程序的生命周期一样长。因此,垃圾回收的频率很低,采用的算法也很简单。

分代设计使得Java堆内存的管理更加灵活,可以根据对象的生命周期选择合适的垃圾回收算法。这样可以减少内存分配和回收的开销,提高程序的性能。

虚拟机栈

线程私有,描述Java方法的执行过程

虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧(Stack Frame)中存储了局部变量表、操作数栈、动态链接、方法出口等信息。

同时,栈帧用来存储部分运行时数据及其数据结构,处理动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch Exception)

栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈和出栈。无论方法是正常运行完成还是异常完成(抛出了在方法内未被捕获的异常),都视为方法运行结束。下图展示了线程运行及栈帧变化的过程。线程 1在CPU1上运行,线程 2在CPU2上运行,在CPU资源不够时其他线程将处于等待状态(等待的线程 N ),等待获取CPU时间片。而在线程内部,每个方法的执行和返回都对应一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活动状态。

在这里插入图片描述

栈帧结构

栈帧(Stack Frame)是Java虚拟机(JVM)中的一种数据结构,用于存储方法的局部变量、操作数栈、方法返回地址等信息。每个线程在调用一个方法时,都会创建一个栈帧,栈帧保存了方法的运行状态,当方法执行完毕时,栈帧会被销毁。

栈帧的结构包括以下几个部分:

栈帧(Stack Frame)是存储函数调用和局部变量信息的数据结构。栈帧通常包含以下信息:

1. 返回地址(Return Address):调用函数时,程序会将返回地址压入栈中。当函数执行完毕后,程序会从栈中弹出返回地址,并跳转到该地址继续执行。

2. 函数参数(Arguments):调用函数时传递给函数的参数值。

3. 局部变量(Local Variables):函数内部声明的变量。

4. 临时变量(Temporary Variables):一些编译器可能会将一些局部变量优化成临时变量,以提高程序执行效率。

5. 上一个栈帧指针(Previous Stack Frame Pointer):指向上一个栈帧的指针,用于实现栈的链表结构。

6. 异常处理信息(Exception Handling Information):用于异常处理。

7. 编译器自动生成的其他信息:如调试信息、优化信息等。

下面是栈帧结构的思维导图示例:

栈帧(Stack Frame)
│
├── 返回地址(Return Address)
│
├── 函数参数(Arguments)
│
├── 局部变量(Local Variables)
│
├── 临时变量(Temporary Variables)
│
├── 上一个栈帧指针(Previous Stack Frame Pointer)
│
├── 异常处理信息(Exception Handling Information)
│
└── 编译器自动生成的其他信息

每当一个函数被调用,都会创建一个新的栈帧。

当函数执行完毕后,栈帧会被销毁,返回地址会被弹出,程序会跳转到返回地址继续执行。

方法区

线程共享

方法区也被称为永久代,用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据

在这里插入图片描述

方法区和永久代以及元空间什么关系

方法区是Java虚拟机的一部分,它用于存储类的元数据、静态变量、常量池等。在Java 8之前的版本中,方法区是通过永久代来实现的。永久代是堆内存的一部分,用于存储类的元数据和静态变量。

但是,永久代存在一些问题,例如不能动态调整大小、容易出现内存溢出等。为了解决这些问题,Java 8引入了元空间(Metaspace),并取代了永久代。元空间是一块与堆内存分离的本地内存区域,用于存储类的元数据、静态变量和常量池。与永久代相比,元空间具有更好的性能和可管理性,可以动态调整大小,不容易出现内存溢出。

因此,方法区和永久代以及元空间的关系是:方法区是Java虚拟机的内存区域之一,用于存储类的元数据、静态变量和常量池;在Java 8之前的版本中,方法区是通过永久代来实现的;而在Java 8之后的版本中,方法区是通过元空间来实现的。

为什么Eden:S0:S1是8:1:1

在Java虚拟机中,新生代内存分为Eden区、Survivor0区和Survivor1区(也称为S0区和S1区)。Eden区是用于存放新创建的对象的区域,而Survivor0和Survivor1区则是用于存放从Eden区复制过来的存活对象的区域。在垃圾回收时,存活的对象会被复制到Survivor区,并且根据存活时间的长短,可以被晋升到老年代。

Eden区和Survivor区的比例通常是8:1:1。这个比例的设定是基于以下考虑:

1. 对象的生命周期短:大多数新创建的对象的生命周期很短,它们会很快变成垃圾。因此,Eden区需要足够大,以便存放这些对象。

2. 对象的生命周期长:一些对象的生命周期可能很长,它们会在程序的整个生命周期中被使用。因此,Survivor区也需要足够大,以便存放这些对象。

3. 垃圾回收效率:Eden区和Survivor区的比例设定为8:1:1,是为了保证垃圾回收的效率。如果Eden区和Survivor区的比例过小,会导致Survivor区过小,不足以存放所有的存活对象,从而频繁地触发垃圾回收。而如果Eden区和Survivor区的比例过大,会导致Eden区过大,占用过多的内存空间。

因此,Eden区和Survivor区的比例通常是8:1:1。这个比例的设定是为了平衡对象的生命周期长短和垃圾回收效率。

什么是 Java 内存模型(JMM)

Java 内存模型(Java Memory Model,JMM)是一种规范,用于描述 Java 程序在多线程环境中的内存访问和同步行为。JMM 定义了一组规则和约定,用于确保多线程环境中的内存访问和同步操作的正确性和一致性。JMM 包含了以下重要概念:

Java 内存模型(JMM)
   |
   |-●主内存(Main Memory)
   |
   |-●工作内存(Working Memory)
   |
   |-●内存模型(Memory Model)
   |       |
   |       |-●原子性(Atomicity)
   |       |
   |       |-●可见性(Visibility)
   |       |
   |       |-●有序性(Ordering)
   |       |
   |       |-●Happens-Before 关系(Happens-Before Relation)
   |       |
   |       |-●线程安全性(Thread Safety)
   |       |
   |       |-●内存屏障(Memory Barrier)
   |
   |-●线程(Thread)
          |
          |-●线程启动和终止
          |
          |-●线程 join 方法

  1. 主内存(Main Memory): 主内存是所有线程共享的内存区域,用于存储所有共享变量和对象实例。

  2. 工作内存(Working Memory): 工作内存是每个线程私有的内存区域,用于存储线程私有的变量和对象实例。工作内存中的变量和对象实例的值可以和主内存中的值不同。

  3. 内存模型(Memory Model): 内存模型是一种抽象的、理想化的内存模型,用于描述程序在多线程环境中的内存访问和同步行为。JMM 定义了一组规则和约定,用于描述内存模型。

  4. 原子性(Atomicity): 原子性是指操作的一致性和不可分割性。在 Java 中,原子性可以通过 synchronized 关键字或者 volatile 关键字来实现。

  5. 可见性(Visibility): 可见性是指一个线程对共享变量的修改对其他线程是可见的。在 Java 中,可见性可以通过 synchronized 关键字或者 volatile 关键字来实现。

  6. 有序性(Ordering): 有序性是指程序执行的顺序和程序中的代码顺序一致。在 Java 中,有序性可以通过 synchronized 关键字或者 volatile 关键字来实现。

  7. Happens-Before 关系(Happens-Before Relation): Happens-Before 关系是一种偏序关系,用于描述程序中事件的顺序。在 Java 中,Happens-Before 关系可以通过 synchronized 关键字、volatile 关键字、线程的启动和终止、线程的 join 方法等来实现。

  8. 线程安全性(Thread Safety): 线程安全性是指一个程序在多线程环境中的正确性和一致性。在 Java 中,线程安全性可以通过 synchronized 关键字、volatile 关键字、原子类等来实现。

  9. 内存屏障(Memory Barrier): 内存屏障是一种硬件或者编译器提供的机制,用于保证内存访问和同步操作的顺序和一致性。在 Java 中,内存屏障可以通过 synchronized 关键字、volatile 关键字等来实现。

需要注意的是,JMM 是一种抽象的、理想化的内存模型,它并不是一个具体的实现。JMM 只是一个规范,具体的实现由 Java 虚拟机和操作系统提供。因此,JMM 的具体实现可能会因 Java 虚拟机和操作系统的不同而有所差异。

PS:

volatile保证有序性和内存可见性,不能保证原子性

synchronized保证原子性、内存可见性、有序性

什么是线程安全?如何保证线程安全?

线程安全(Thread Safety)是指在多线程环境下,程序能够正确地处理共享数据,不会产生不确定的结果。

保证线程安全的主要方法:
    ●互斥锁(Mutex Lock)
        ●保护共享资源,只允许一个线程访问共享资源
        ●其他线程需要等待锁释放后才能访问
    ●原子操作(Atomic Operation)
        ●保证对共享资源的操作是原子性的,不会被其他线程中断
    ●可重入锁(Reentrant Lock)
        ●支持多次加锁,同一线程可以多次获取锁
    ●信号量(Semaphore)
        ●限制同时访问共享资源的线程数量
    ●读写锁(Read-Write Lock)
        ●将共享资源分为读写两种操作
        ●读操作可以多个线程同时访问,写操作需要排他性
    ●线程局部变量(Thread-Local Variable)
        ●每个线程拥有自己的变量副本
        ●不受其他线程影响

Java中的四种引用类型

在Java中一切皆对象,对象的操作是通过该对象的引用(Reference)实现的,Java中的引用类型有4种,分别为强引用、软引用、弱引用和虚引用,如图1-13所示。

(1)强引用:在Java中最常见的就是强引用。在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用。有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收。因此,强引用是造成Java内存泄漏(Memory Link)的主要原因。

(2)软引用:软引用通过SoftReference类实现。如果一个对象只有软引用,则在系统内存空间不足时该对象将被回收。

(3)弱引用:弱引用通过WeakReference类实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收。

(4)虚引用:虚引用通过PhantomReference类实现,虚引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态。

性能调优

Java堆内存调优

Java 堆内存调优(Heap Tuning)是一种优化 Java 应用程序性能的重要方法,Java 堆内存是 Java 虚拟机管理的一种重要内存区域,它用于存储 Java 对象的实例。Java 堆内存调优的目标是提高 Java 应用程序的性能和稳定性,减少垃圾回收的停顿时间。以下是一些常用的 Java 堆内存调优方法:

Java 堆内存调优方法
   |
   |-●调整堆内存大小
   |       |
   |       |-●-Xms 参数调整初始大小
   |       |
   |       |-●-Xmx 参数调整最大大小
   |
   |-●调整新生代和老年代的比例
   |       |
   |       |-●-XX:NewRatio 参数调整比例
   |
   |-●调整新生代和老年代的大小
   |       |
   |       |-●-XX:NewSize 参数调整新生代的初始大小
   |       |
   |       |-●-XX:MaxNewSize 参数调整新生代的最大大小
   |       |
   |       |-●-XX:OldSize 参数调整老年代的初始大小
   |       |
   |       |-●-XX:MaxOldSize 参数调整老年代的最大大小
   |
   |-●调整 Eden 区和 Survivor 区的大小
   |       |
   |       |-●-XX:SurvivorRatio 参数调整比例
   |       |
   |       |-●-XX:SurvivorSize 参数调整 Eden 区的大小
   |       |
   |       |-●-XX:MaxSurvivorSize 参数调整 Survivor 区的大小
   |
   |-●调整垃圾回收策略
   |       |
   |       |-●-XX:+UseSerialGC 使用串行垃圾回收器
   |       |
   |       |-●-XX:+UseParallelGC 使用并行垃圾回收器
   |       |
   |       |-●-XX:+UseConcMarkSweepGC 使用 CMS 垃圾回收器
   |       |
   |       |-●-XX:+UseG1GC 使用 G1 垃圾回收器
   |
   |-●调整垃圾回收器的线程数
   |       |
   |       |-●-XX:ParallelGCThreads 参数调整并行垃圾回收器的线程数
   |       |
   |       |-●-XX:ConcGCThreads 参数调整 CMS 垃圾回收器的线程数
   |       |
   |       |-●-XX:G1HeapRegionSize 参数调整 G1 垃圾回收器的线程数
   |
   |-●调整垃圾回收的停顿时间
   |       |
   |       |-●-XX:MaxGCPauseMillis 参数调整最大停顿时间
   |       |
   |       |-●-XX:GCTimeRatio 参数调整时间比例
   |
   |-●调整垃圾回收的触发条件
   |       |
   |       |-●-XX:+UseAdaptiveSizePolicy 使用自适应的垃圾回收策略
   |       |
   |       |-●-XX:AdaptiveSizePolicyInitializingSteps 参数调整初始化步数
   |       |
   |       |-●-XX:AdaptiveSizePolicyGCDurationThreshold 参数调整 GC 时长阈值
   |
   |-●调整垃圾回收的日志输出
   |       |
   |       |-●-XX:+PrintGC 输出垃圾回收的日志
   |       |
   |       |-●-XX:+PrintGCDetails 输出垃圾回收的详细日志
   |       |
   |       |-●-XX:+PrintGCTimeStamps 输出垃圾回收的时间戳
   |
   |-●调整垃圾回收的统计信息
           |
           |-●-XX:+PrintHeapAtGC 输出垃圾回收时的堆信息



 1. 调整堆内存大小:
   ●通过 `-Xms` 和 `-Xmx` 参数调整 Java 堆的初始大小和最大大小。
   ●初始大小和最大大小的设置应根据应用程序的需求和硬件资源来确定。
   ●通常情况下,初始大小和最大大小的设置应该相同,以避免垃圾回收频繁触发。

 2. 调整新生代和老年代的比例:
   ●通过 `-XX:NewRatio` 参数调整新生代和老年代的比例。
   ●新生代和老年代的比例应根据应用程序的需求和硬件资源来确定。
   ●通常情况下,新生代和老年代的比例应该根据应用程序的对象的生命周期来确定。

 3. 调整新生代和老年代的大小:
   ●通过 `-XX:NewSize` 和 `-XX:MaxNewSize` 参数调整新生代的初始大小和最大大小。
   ●通过 `-XX:OldSize` 和 `-XX:MaxOldSize` 参数调整老年代的初始大小和最大大小。
   ●新生代和老年代的大小应根据应用程序的需求和硬件资源来确定。

 4. 调整 Eden 区和 Survivor 区的大小:
   ●通过 `-XX:SurvivorRatio` 参数调整 Eden 区和 Survivor 区的比例。
   ●通过 `-XX:SurvivorSize` 和 `-XX:MaxSurvivorSize` 参数调整 Eden 区和 Survivor 区的大小。
   ●Eden 区和 Survivor 区的大小应根据应用程序的需求和硬件资源来确定。

 5. 调整垃圾回收策略:
   ●通过 `-XX:+UseSerialGC` 参数使用串行垃圾回收器。
   ●通过 `-XX:+UseParallelGC` 参数使用并行垃圾回收器。
   ●通过 `-XX:+UseConcMarkSweepGC` 参数使用 CMS 垃圾回收器。
   ●通过 `-XX:+UseG1GC` 参数使用 G1 垃圾回收器。

 6. 调整垃圾回收器的线程数:
   ●通过 `-XX:ParallelGCThreads` 参数调整并行垃圾回收器的线程数。
   ●通过 `-XX:ConcGCThreads` 参数调整 CMS 垃圾回收器的线程数。
   ●通过 `-XX:G1HeapRegionSize` 参数调整 G1 垃圾回收器的线程数。

 7. 调整垃圾回收的停顿时间:
   ●通过 `-XX:MaxGCPauseMillis` 参数调整垃圾回收的最大停顿时间。
   ●通过 `-XX:GCTimeRatio` 参数调整垃圾回收的时间比例。

 8. 调整垃圾回收的触发条件:
   ●通过 `-XX:+UseAdaptiveSizePolicy` 参数使用自适应的垃圾回收策略。
   ●通过 `-XX:AdaptiveSizePolicyInitializingSteps` 和 `-XX:AdaptiveSizePolicyGCDurationThreshold` 参数调整自适应的垃圾回收策略的触发条件。

 9. 调整垃圾回收的日志输出:
   ●通过 `-XX:+PrintGC` 参数输出垃圾回收的日志。
   ●通过 `-XX:+PrintGCDetails` 参数输出垃圾回收的详细日志。
   ●通过 `-XX:+PrintGCTimeStamps` 参数输出垃圾回收的时间戳。

 10. 调整垃圾回收的统计信息:
   ●通过 `-XX:+PrintHeapAtGC` 参数输出垃圾回收时的

JVM调优参数

JVM调优参数的含义和作用
    ●堆内存调优参数
        ●-Xms和-Xmx参数:设置堆的初始堆大小和最大堆大小
        ●-XX:NewRatio参数:设置新生代和老年代的大小比例
        ●-XX:MaxNewSize参数:设置新生代的最大大小
        ●-XX:SurvivorRatio参数:设置新生代的Eden区和Survivor区的大小比例
        ●-XX:TargetSurvivorRatio参数:设置新生代的Survivor区的使用率阈值
        ●-XX:MaxTenuringThreshold参数:设置对象晋升老年代的阈值
        ●-XX:InitiatingHeapOccupancyPercent参数:设置堆内存的使用率阈值
    ●GC调优参数
        ●-XX:+UseSerialGC、-XX:+UseParallelGC、-XX:+UseConcMarkSweepGC、-XX:+UseG1GC参数:设置新生代和老年代的收集算法
        ●-XX:+PrintGCDetails、-XX:+PrintGCDateStamps参数:设置垃圾回收日志的输出路径和输出的详细信息
        ●-Xloggc参数:设置垃圾回收日志的输出路径
    ●堆外内存调优参数
        ●-XX:MaxDirectMemorySize参数:设置堆外内存的最大大小
    ●线程调优参数
        ●-XX:ParallelGCThreads参数:设置并行垃圾回收的线程数
        ●-XX:ConcGCThreads参数:设置并发垃圾回收的线程数
        ●-XX:CICompilerCount参数:设置编译器的线程数
        ●-XX:ThreadStackSize参数:设置线程栈的大小
        ●-XX:MaxInlineSize参数:设置方法内联的最大大小
        ●-XX:FreqInlineSize参数:设置方法内联的频率
        ●-XX:CompileThreshold参数:设置编译的阈值
        ●-XX:CompileCommand参数:设置编译的命令
    ●代码缓存调优参数
        ●-XX:ReservedCodeCacheSize参数:设置代码缓存的最大大小
        ●-XX:InitialCodeCacheSize参数:设置代码缓存的初始大小
        ●-XX:CodeCacheExpansionSize参数:设置代码缓存的扩展大小
        ●-XX:CodeCacheMinimumFreeSpace参数:设置代码缓存的最小空闲空间
    ●其他调优参数
        ●-XX:+UseCompressedOops参数:开启压缩对象指针
        ●-XX:+UseCompressedClassPointers参数:开启压缩类指针
        ●-XX:+UseNUMA参数:开启NUMA(Non-Uniform Memory Access)
        ●-XX:+UseBiasedLocking参数:开启偏向锁
        ●-XX:+UseTLAB参数:开启线程本地分配缓冲区(TLAB)
        ●-XX:+UseNUMA参数:开启NUMA(Non-Uniform Memory Access)
        ●-XX:+UseNUMAInterleaving参数:开启NUMA(Non-Uniform Memory Access)交错分配

JVM参数配置根据应用程序的特点和需求来确定。以下是一些常见的应用场景及对应的JVM参数配置示例:

1. 大型Web应用:对于大型Web应用,通常需要大量的内存和高并发能力。
    ●`-Xms``-Xmx`参数设置堆的初始大小和最大大小,例如`-Xms4g -Xmx4g`。
    ●`-XX:+UseG1GC`参数设置使用G1垃圾回收器,例如`-XX:+UseG1GC`。
    ●`-XX:+UseNUMA`参数开启NUMA(Non-Uniform Memory Access),例如`-XX:+UseNUMA`2. 大数据处理应用:对于大数据处理应用,通常需要大量的内存和高效的垃圾回收。
    ●`-Xms``-Xmx`参数设置堆的初始大小和最大大小,例如`-Xms8g -Xmx8g`。
    ●`-XX:+UseG1GC`参数设置使用G1垃圾回收器,例如`-XX:+UseG1GC`。
    ●`-XX:+UseNUMA`参数开启NUMA(Non-Uniform Memory Access),例如`-XX:+UseNUMA`。
    ●`-XX:ReservedCodeCacheSize`参数设置代码缓存的最大大小,例如`-XX:ReservedCodeCacheSize=256m`3. 实时数据处理应用:对于实时数据处理应用,通常需要低延迟和高吞吐量。
    ●`-Xms``-Xmx`参数设置堆的初始大小和最大大小,例如`-Xms8g -Xmx8g`。
    ●`-XX:+UseG1GC`参数设置使用G1垃圾回收器,例如`-XX:+UseG1GC`。
    ●`-XX:ReservedCodeCacheSize`参数设置代码缓存的最大大小,例如`-XX:ReservedCodeCacheSize=256m`。
    ●`-XX:+UseNUMA`参数开启NUMA(Non-Uniform Memory Access),例如`-XX:+UseNUMA`4. 内存密集型应用:对于内存密集型应用,通常需要大量的内存。
    ●`-Xms``-Xmx`参数设置堆的初始大小和最大大小,例如`-Xms16g -Xmx16g`。
    ●`-XX:+UseG1GC`参数设置使用G1垃圾回收器,例如`-XX:+UseG1GC`。
    ●`-XX:ReservedCodeCacheSize`参数设置代码缓存的最大大小,例如`-XX:ReservedCodeCacheSize=512m`。
    ●`-XX:+UseNUMA`参数开启NUMA(Non-Uniform Memory Access),例如`-XX:+UseNUMA`5. CPU密集型应用:对于CPU密集型应用,通常需要高性能的垃圾回收和线程调度。
    ●`-Xms``-Xmx`参数设置堆的初始大小和最大大小,例如`-Xms4g -Xmx4g`。
    ●`-XX:+UseParallelGC`参数设置使用并行垃圾回收器,例如`-XX:+UseParallelGC`。
    ●`-XX:ParallelGCThreads`参数设置并行垃圾回收的线程数,例如`-XX:ParallelGCThreads=8`。
    ●`-XX:CICompilerCount`参数设置编译器的线程数,例如`-XX:CICompilerCount=4`。

这些示例说明了JVM参数配置是根据应用程序的特点和需求来确定的。根据不同的应用场景,可以调整堆内存大小、垃圾回收器、代码缓存大小、线程数等参数,以优化应用程序的性能和资源利用率。

JVM 有哪些核心指标?合理范围应该是多少?

AVG/TP999/TP9999通常用于描述系统在处理请求时的响应时间分布。

  1. AVG:表示平均响应时间(Average)。它是所有请求的响应时间的平均值,单位为毫秒。平均响应时间越小,表示系统处理请求的速度越快。
  2. TP999:表示 99% 的请求响应时间(99th Percentile)。它是所有请求的响应时间中,排在 99% 的位置的响应时间,单位为毫秒。TP999 越小,表示系统处理大多数请求的速度较快。
  3. TP9999:表示 99.99% 的请求响应时间(99.99th Percentile)。它是所有请求的响应时间中,排在 99.99% 的位置的响应时间,单位为毫秒。TP9999 越小,表示系统处理绝大多数请求的速度较快。

通常来说,TP999 和 TP9999 用于描述系统处理大多数请求的响应时间,AVG 用于描述系统的平均响应时间。

对于普通的 Java 后端应用来说,我这边给出一份相对合理的范围值。以下指标都是对于单台服务器来说:

  • jvm.gc.time:每分钟的GC耗时在1s以内,500ms以内尤佳
  • jvm.gc.meantime:每次YGC耗时在100ms以内,50ms以内尤佳
  • jvm.fullgc.count:FGC最多几小时1次,1天不到1次尤佳
  • jvm.fullgc.time:每次FGC耗时在1s以内,500ms以内尤佳

通常来说,只要这几个指标正常,其他的一般不会有问题,如果其他地方出了问题,一般都会影响到这几个指标。

JVM 优化步骤

 1、分析和定位当前系统的瓶颈

对于JVM的核心指标,我们的关注点和常用工具如下:

 1)CPU指标

- 查看占用CPU最多的进程
- 查看占用CPU最多的线程
- 查看线程堆栈快照信息
- 分析代码执行热点
- 查看哪个代码占用CPU执行时间最长
- 查看每个方法占用CPU时间比例

常见的命令:

text
// 显示系统各个进程的资源使用情况
top
// 查看某个进程中的线程占用情况
top -Hp pid
// 查看当前 Java 进程的线程堆栈信息
jstack pid


常见的工具:JProfiler、JVM Profiler、Arthas等。



 2)JVM 内存指标

- 查看当前 JVM 堆内存参数配置是否合理
- 查看堆中对象的统计信息
- 查看堆存储快照,分析内存的占用情况
- 查看堆各区域的内存增长是否正常
- 查看是哪个区域导致的GC
- 查看GC后能否正常回收到内存

常见的命令:

text
// 查看当前的 JVM 参数配置
ps -ef | grep java
// 查看 Java 进程的配置信息,包括系统属性和JVM命令行标志
jinfo pid
// 输出 Java 进程当前的 gc 情况
jstat -gc pid
// 输出 Java 堆详细信息
jmap -heap pid
// 显示堆中对象的统计信息
jmap -histo:live pid
// 生成 Java 堆存储快照dump文件
jmap -F -dump:format=b,file=dumpFile.phrof pid


常见的工具:Eclipse MAT、JConsole等。



 3)JVM GC指标

- 查看每分钟GC时间是否正常
- 查看每分钟YGC次数是否正常
- 查看FGC次数是否正常
- 查看单次FGC时间是否正常
- 查看单次GC各阶段详细耗时,找到耗时严重的阶段
- 查看对象的动态晋升年龄是否正常

JVM 的 GC指标一般是从 GC 日志里面查看,默认的 GC 日志可能比较少,我们可以添加以下参数,来丰富我们的GC日志输出,方便我们定位问题。

GC日志常用 JVM 参数:

text
// 打印GC的详细信息
-XX:+PrintGCDetails
// 打印GC的时间戳
-XX:+PrintGCDateStamps
// 在GC前后打印堆信息
-XX:+PrintHeapAtGC
// 打印Survivor区中各个年龄段的对象的分布信息
-XX:+PrintTenuringDistribution
// JVM启动时输出所有参数值,方便查看参数是否被覆盖
-XX:+PrintFlagsFinal
// 打印GC时应用程序的停止时间
-XX:+PrintGCApplicationStoppedTime
// 打印在GC期间处理引用对象的时间(仅在PrintGCDetails时启用)
-XX:+PrintReferenceGC


以上就是我们定位系统瓶颈的常用手段,大部分问题通过以上方式都能定位出问题原因,然后结合代码去找到问题根源。



 2、确定优化目标

定位出系统瓶颈后,在优化前先制定好优化的目标是什么,例如:

- 将FGC次数从每小时1次,降低到1天1次
- 将每分钟的GC耗时从3s降低到500ms
- 将每次FGC耗时从5s降低到1s以内



 3、制订优化方案

针对定位出的系统瓶颈制定相应的优化方案,常见的有:

- 代码bug:升级修复bug。典型的有:死循环、使用无界队列。
- 不合理的JVM参数配置:优化 JVM 参数配置。典型的有:年轻代内存配置过小、堆内存配置过小、元空间配置过小。

 4、对比优化前后的指标,统计优化效果

 5、持续观察和跟踪优化效果

 6、如果还需要的话,重复以上步骤


JVM调优案例

以下案例来源于网络或本人真实经验,皆能自圆其说,理解掌握后同学们皆可拿来与面试官对线。

服务环境:ParNew + CMS + JDK8

问题现象:服务频繁出现FGC

原因分析:

1)首先查看GC日志,发现出现FGC的原因是metaspace空间不够

对应GC日志:

Full GC (Metadata GC Threshold)
2)进一步查看日志发现元空间存在内存碎片化现象

对应GC日志:

Metaspace       used 35337K, capacity 56242K, committed 56320K, reserved 1099776K
这边简单解释下这几个参数的意义

used :已使用的空间大小
capacity:当前已经分配且未释放的空间容量大小
committed:当前已经分配的空间大小
reserved:预留的空间大小
这边 used 比较容易理解,reserved 在本例不重要可以先忽略,主要是 capacity 和 committed 这2个容易搞混。

结合下图来看更容易理解,元空间的分配以 chunk 为单位,当一个 ClassLoader 被垃圾回收时,所有属于它的空间(chunk)被释放,此时该 chunk 称为 Free Chunk,而 committed chunk 就是 capacity chunk 和 free chunk 之和。


之所以说内存存在碎片化现象就是根据 used 和 capacity 的数据得来的,上面说了元空间的分配以 chunk 为单位,即使一个 ClassLoader 只加载1个类,也会独占整个 chunk,所以当出现 used 和 capacity 两者之差较大的时候,说明此时存在内存碎片化的情况。

GC日志demo如下:

{Heap before GC invocations=0 (full 0):
 par new generation   total 314560K, used 141123K [0x00000000c0000000, 0x00000000d5550000, 0x00000000d5550000)
  eden space 279616K,  50% used [0x00000000c0000000, 0x00000000c89d0d00, 0x00000000d1110000)
  from space 34944K,   0% used [0x00000000d1110000, 0x00000000d1110000, 0x00000000d3330000)
  to   space 34944K,   0% used [0x00000000d3330000, 0x00000000d3330000, 0x00000000d5550000)
 concurrent mark-sweep generation total 699072K, used 0K [0x00000000d5550000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 35337K, capacity 56242K, committed 56320K, reserved 1099776K
  class space    used 4734K, capacity 8172K, committed 8172K, reserved 1048576K
1.448: [Full GC (Metadata GC Threshold) 1.448: [CMS: 0K->10221K(699072K), 0.0487207 secs] 141123K->10221K(1013632K), [Metaspace: 35337K->35337K(1099776K)], 0.0488547 secs] [Times: user=0.09 sys=0.00, real=0.05 secs] 
Heap after GC invocations=1 (full 1):
 par new generation   total 314560K, used 0K [0x00000000c0000000, 0x00000000d5550000, 0x00000000d5550000)
  eden space 279616K,   0% used [0x00000000c0000000, 0x00000000c0000000, 0x00000000d1110000)
  from space 34944K,   0% used [0x00000000d1110000, 0x00000000d1110000, 0x00000000d3330000)
  to   space 34944K,   0% used [0x00000000d3330000, 0x00000000d3330000, 0x00000000d5550000)
 concurrent mark-sweep generation total 699072K, used 10221K [0x00000000d5550000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 35337K, capacity 56242K, committed 56320K, reserved 1099776K
  class space    used 4734K, capacity 8172K, committed 8172K, reserved 1048576K
}
{Heap before GC invocations=1 (full 1):
 par new generation   total 314560K, used 0K [0x00000000c0000000, 0x00000000d5550000, 0x00000000d5550000)
  eden space 279616K,   0% used [0x00000000c0000000, 0x00000000c0000000, 0x00000000d1110000)
  from space 34944K,   0% used [0x00000000d1110000, 0x00000000d1110000, 0x00000000d3330000)
  to   space 34944K,   0% used [0x00000000d3330000, 0x00000000d3330000, 0x00000000d5550000)
 concurrent mark-sweep generation total 699072K, used 10221K [0x00000000d5550000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 35337K, capacity 56242K, committed 56320K, reserved 1099776K
  class space    used 4734K, capacity 8172K, committed 8172K, reserved 1048576K
1.497: [Full GC (Last ditch collection) 1.497: [CMS: 10221K->3565K(699072K), 0.0139783 secs] 10221K->3565K(1013632K), [Metaspace: 35337K->35337K(1099776K)], 0.0193983 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
Heap after GC invocations=2 (full 2):
 par new generation   total 314560K, used 0K [0x00000000c0000000, 0x00000000d5550000, 0x00000000d5550000)
  eden space 279616K,   0% used [0x00000000c0000000, 0x00000000c0000000, 0x00000000d1110000)
  from space 34944K,   0% used [0x00000000d1110000, 0x00000000d1110000, 0x00000000d3330000)
  to   space 34944K,   0% used [0x00000000d3330000, 0x00000000d3330000, 0x00000000d5550000)
 concurrent mark-sweep generation total 699072K, used 3565K [0x00000000d5550000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 17065K, capacity 22618K, committed 35840K, reserved 1079296K
  class space    used 1624K, capacity 2552K, committed 8172K, reserved 1048576K
}
元空间主要适用于存放类的相关信息,而存在内存碎片化说明很可能创建了较多的类加载器,同时使用率较低。

因此,当元空间出现内存碎片化时,我们会着重关注是不是创建了大量的类加载器。

3)通过 dump 堆存储文件发现存在大量 DelegatingClassLoader

通过进一步分析,发现是由于反射导致创建大量 DelegatingClassLoader。其核心原理如下:

在 JVM 上,最初是通过 JNI 调用来实现方法的反射调用,当 JVM 注意到通过反射经常访问某个方法时,它将生成字节码来执行相同的操作,称为膨胀(inflation)机制。如果使用字节码的方式,则会为该方法生成一个 DelegatingClassLoader,如果存在大量方法经常反射调用,则会导致创建大量 DelegatingClassLoader。

反射调用频次达到多少才会从 JNI 转字节码?

默认是15次,可通过参数 -Dsun.reflect.inflationThreshold 进行控制,在小于该次数时会使用 JNI 的方式对方法进行调用,如果调用次数超过该次数就会使用字节码的方式生成方法调用。

分析结论:反射调用导致创建大量 DelegatingClassLoader,占用了较大的元空间内存,同时存在内存碎片化现象,导致元空间利用率不高,从而较快达到阈值,触发 FGC。

优化策略:

1)适当调大 metaspace 的空间大小。
2)优化不合理的反射调用。例如最常见的属性拷贝工具类 BeanUtils.copyProperties 可以使用 mapstruct 替换。

怎样优化 Java 程序的性能?

优化 Java 程序的性能涉及多个方面,下面详细说明各个方面的优化方法:

1. 代码优化:
   ●减少循环次数、减少方法调用、减少对象创建、减少异常处理、减少字符串操作、减少IO操作等。
   ●使用StringBuilder代替StringBuffer,使用ArrayList代替Vector,使用HashMap代替Hashtable等。
   ●使用Lambda表达式和Stream API简化代码,提高程序的可读性和性能。简化的代码通常更加清晰,减少了临时变量的使用;Stream API 提供了并行流的支持,可以将数据分成多个部分进行处理,提高程序的并发性能;延迟执行: Stream API 使用惰性求值的方式进行操作,只有在需要结果的时候才会进行计算,避免了不必要的计算,提高了程序的性能;优化内存占用: Stream API 使用流水线的方式进行操作,避免了创建大量的临时对象,减少了内存占用,提高了程序的性能。
   ●使用缓存机制(如EHCache、Redis等)缓存计算结果,减少计算量。
   ●优化不合理的反射调用。例如最常见的属性拷贝工具类 BeanUtils.copyProperties 可以使用 mapstruct 替换。

2. 内存优化:
   ●减少内存占用,避免创建大量的对象,及时释放不再使用的对象,避免内存泄露。
   ●使用对象池(如Apache Commons Pool、Google Guava ObjectPool等)复用对象,减少对象的创建和销毁。
   ●使用内存分析工具(如Eclipse Memory Analyzer、VisualVM、jvisualvm、YourKit等)分析内存使用情况,找出内存泄露和过度消耗。

3. 数据库优化:
   ●使用索引优化数据库查询,减少数据库的IO操作。
   ●使用批量更新和批量插入优化数据库操作,减少数据库的网络开销。
   ●使用连接池(如Apache Commons DBCP、HikariCP等)管理数据库连接,减少数据库的连接开销。

4. 网络优化:
   ●使用连接池(如Apache HttpComponents、OkHttp等)管理HTTP连接,减少网络的连接开销。
   ●使用缓存机制(如EHCache、Redis等)缓存网络数据,减少网络的传输量。
   ●使用压缩算法(如GZIP、Brotli等)压缩网络数据,减少网络的传输时间。

5. IO优化:
   ●使用NIO(Java NIO、Netty、Apache MINA等)代替IO,提高IO的效率。
   ●使用缓冲流(如BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter等)减少IO的次数。
   ●使用文件映射(如FileChannel.map())减少IO的次数。

6. 缓存优化:
   ●使用本地缓存(如ConcurrentHashMap、Guava Cache、Caffeine Cache等)缓存计算结果,减少计算量。
   ●使用分布式缓存(如Redis、Memcached、Ehcache)缓存网络数据,减少网络的传输量。
   ●使用缓存预热,提前加载缓存数据,减少请求的响应时间。
   ●使用缓存过期,自动清理过期的缓存数据,减少内存的占用。
   ●使用缓存刷新,定时刷新缓存数据,保持缓存数据的新鲜度。
   ●手动失效缓存数据,提高缓存数据的可用性。
   ●限制缓存大小,避免缓存穿透导致内存泄露。

7. 并发优化:
   ●使用线程池(如ThreadPoolExecutor、ForkJoinPool、CompletableFuture等)管理线程,提高程序的并发性能。
   ●使用并发容器(如ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque等)管理共享数据,减少线程的竞争。
   ●使用非阻塞算法:使用非阻塞算法可以减少线程的竞争,提高线程的并发性能,降低线程的阻塞时间。
     1. NIO(New I/O): Java NIO 是一种基于同步非阻塞 I/O 模型。它可以实现非阻塞的、高效的、可扩展的 I/O 操作。NIO 提供了 Buffer 类、Channel 接口、Selector 类、SocketChannel 类、ServerSocketChannel 类等多个类和接口,可以满足不同的 I/O 需求。
     2. AIO(Asynchronous I/O): Java AIO 是一种基于异步非阻塞 I/O 模型,它可以实现非阻塞的、高效的、可扩展的异步非阻塞 I/O 操作。AIO 提供了 AsynchronousChannelGroup 类和 AsynchronousChannel 类,可以实现对异步通道组和异步通道的读写操作。
     3. ConcurrentHashMap: Java ConcurrentHashMap 是一种基于 CAS(Compare And Swap)的线程安全的哈希表,它可以实现非阻塞的、高效的、可扩展的哈希表操作。
     4. AtomicInteger: Java AtomicInteger 是一种基于 CAS 的线程安全的整数类型,它可以实现非阻塞的、高效的、可扩展的整数操作。
     5. AtomicReference: Java AtomicReference 是一种基于 CAS 的线程安全的引用类型,它可以实现非阻塞的、高效的、可扩展的引用操作。
   ●使用锁和条件变量:使用锁和条件变量可以控制线程的访问顺序,提高线程的并发性能,降低线程的阻塞时间。
   ●使用原子变量:使用原子变量可以减少线程的竞争,提高线程的并发性能,降低线程的阻塞时间。原子变量通常使用 `java.util.concurrent.atomic` 包下的原子类来实现,主要包括 AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference、AtomicStampedReference 等多种类型。
   ●使用并发工具: 使用并发工具可以简化并发编程的复杂度,提高并发编程的效率,降低并发编程的风险。
   ●使用异步编程: 使用异步编程可以提高系统的并发性能,降低系统的响应时间,提高系统的吞吐量。异步编程可以通过 Future 和 Callable、CompletableFuture、定时器等多种方式实现。
   ●使用分布式锁: 使用分布式锁可以控制多个节点的访问顺序,提高系统的并发性能,降低系统的阻塞时间。
   ●使用消息队列: 使用消息队列可以解耦系统的各个模块,提高系统的并发性能,降低系统的风险。
   ●使用缓存: 使用缓存可以减少系统的访问次数,提高系统的并发性能,降低系统的响应时间。

8. 使用JIT编译器:
   ●使用JIT编译器(如HotSpot JIT编译器)优化Java程序的性能,提高程序的运行速度。

9. 使用性能测试工具:
   ●使用性能测试工具(如JMeter、Gatling、Apache Bench等)对Java程序进行性能测试,找出性能瓶颈和优化建议。

总的来说,优化Java程序的性能需要综合考虑代码、内存、数据库、网络、IO、缓存、并发、JIT编译器、性能测试工具等多个方面,综合采用各种优化方法,才能达到最佳的性能效果。

Java代码性能测试流程?

Java 代码性能测试流程
   |
   |-●需求分析
   |       |
   |       |-●确定测试目的和范围
   |       |
   |       |-●了解业务需求和用户期望的性能指标
   |
   |-●环境搭建
   |       |
   |       |-●配置硬件环境、操作系统、JDK 版本、Web 容器、数据库、测试工具等
   |
   |-●性能测试计划
   |       |
   |       |-●制定测试目标、测试场景、测试数据、测试脚本、测试指标、测试工具、测试时间、测试人员等
   |
   |-●性能测试准备
   |       |
   |       |-●准备测试数据,包括用户数、并发数、请求频率、请求类型、请求参数等
   |
   |-●性能测试执行
   |       |
   |       |-●使用性能测试工具对 Java 应用程序进行性能测试,包括负载测试、压力测试、并发测试、稳定性测试等
   |
   |-●性能测试分析
   |       |
   |       |-●分析性能测试结果,包括响应时间、吞吐量、并发用户数、错误率、资源利用率等指标,找出性能瓶颈和优化建议
   |
   |-●性能测试报告
   |       |
   |       |-●编写性能测试报告,包括测试结果、测试分析、性能瓶颈、优化建议、测试总结等
   |
   |-●性能测试优化
   |       |
   |       |-●根据性能测试结果和优化建议,对 Java 应用程序进行优化,包括代码优化、内存优化、数据库优化、网络优化等
   |
   |-●性能测试验证
   |       |
   |       |-●验证优化后的 Java 应用程序的性能,包括性能指标、性能瓶颈、性能优化效果等
   |
   |-●性能测试监控
           |
           |-●定期监控 Java 应用程序的性能,包括性能指标、性能瓶颈、性能优化效果等



Java 代码性能测试是评估 Java 应用程序性能的过程,通常包括几个步骤:

1. 需求分析: 确定测试的目的和范围,了解业务需求和用户期望的性能指标。

2. 环境搭建: 配置测试环境,包括硬件环境、操作系统、JDK 版本、Web 容器(如 Tomcat、Jetty)、数据库(如 MySQL、Oracle)、测试工具(如 JMeter、Gatling)等。

3. 性能测试计划: 制定性能测试计划,包括测试目标、测试场景、测试数据、测试脚本、测试指标、测试工具、测试时间、测试人员等。

4. 性能测试准备: 准备测试数据,包括用户数、并发数、请求频率、请求类型、请求参数等。

5. 性能测试执行: 执行性能测试,根据测试计划和测试数据,使用性能测试工具(如 JMeter、Gatling)对 Java 应用程序进行性能测试,包括负载测试、压力测试、并发测试、稳定性测试等。

6. 性能测试分析: 分析性能测试结果,包括响应时间、吞吐量、并发用户数、错误率、资源利用率等指标,找出性能瓶颈和优化建议。

7. 性能测试报告: 编写性能测试报告,包括测试结果、测试分析、性能瓶颈、优化建议、测试总结等。

8. 性能测试优化: 根据性能测试结果和优化建议,对 Java 应用程序进行优化,包括代码优化、内存优化、数据库优化、网络优化等。

9. 性能测试验证: 验证优化后的 Java 应用程序的性能,包括性能指标、性能瓶颈、性能优化效果等。

10. 性能测试监控: 定期监控 Java 应用程序的性能,包括性能指标、性能瓶颈、性能优化效果等。

总的来说,Java 代码性能测试流程包括需求分析、环境搭建、性能测试计划、性能测试准备、性能测试执行、性能测试分析、性能测试报告、性能测试优化、性能测试验证、性能测试监控等多个步骤,是一个全面评估 Java 应用程序性能的过程。

JVM 监控与调优

你了解 JVM 监控工具有哪些?

JVM(Java Virtual Machine)监控工具是用于监控Java虚拟机的工具,可以用于监控Java虚拟机的内存、线程、GC、类加载、CPU等情况。以下是一些常用的JVM监控工具:

1. JConsole:JConsole是Java自带的监控工具,可以用于监控Java虚拟机的内存、线程、GC、类加载、CPU等情况。可以通过jconsole命令启动JConsole。

2. VisualVM:VisualVM是一个开源的监控工具,可以用于监控Java虚拟机的内存、线程、GC、类加载、CPU等情况。可以通过visualvm命令启动VisualVM。

3. JProfiler:JProfiler是一个商业的监控工具,可以用于监控Java虚拟机的内存、线程、GC、类加载、CPU等情况。可以通过jprofiler命令启动JProfiler。

4. YourKit:YourKit是一个商业的监控工具,可以用于监控Java虚拟机的内存、线程、GC、类加载、CPU等情况。可以通过yourkit命令启动YourKit。

5. MAT:MAT(Memory Analyzer Tool)是一个开源的监控工具,可以用于监控Java虚拟机的内存、线程、GC、类加载、CPU等情况。可以通过mat命令启动MAT。

6. Mission Control:Mission Control是一个商业的监控工具,可以用于监控Java虚拟机的内存、线程、GC、类加载、CPU等情况。可以通过mission-control命令启动Mission Control。

以上是一些常用的JVM监控工具,可以根据实际情况选择合适的工具进行监控。这些工具可以帮助开发人员监控Java虚拟机的状态,从而优化Java程序的性能。

怎样使用 JVM 监控工具进行调优?

使用JVM监控工具进行调优,通常包括以下步骤:

1. 选择合适的JVM监控工具:常用的JVM监控工具有JVisualVM、VisualGC、JConsole、JMC(Java Mission Control)等。根据需要选择合适的工具。

2. 启动JVM监控工具:启动JVM监控工具并连接到运行中的Java应用程序。可以通过命令行启动,也可以通过图形界面启动。

3. 收集JVM性能数据:在JVM监控工具中,可以收集JVM的各种性能数据,如内存使用情况、垃圾回收情况、线程情况等。

4. 分析JVM性能数据:通过收集到的JVM性能数据,可以分析出JVM的性能瓶颈。例如,如果发现内存使用过高,可能需要调整堆大小;如果发现垃圾回收时间过长,可能需要调整垃圾回收器的参数等。

5. 优化JVM配置:根据分析结果,可以优化JVM的配置。例如,调整堆大小、调整垃圾回收器的参数、调整新生代和老生代的空间比例等。

6. 重新启动Java应用程序:在调整完JVM配置后,需要重新启动Java应用程序,使新的配置生效。

7. 验证优化效果:重新启动Java应用程序后,可以再次收集JVM性能数据,并验证优化效果。如果发现性能有所提升,则说明优化成功。

8. 持续监控和优化:JVM的性能优化是一个持续的过程,需要不断地监控和优化。可以定期收集JVM性能数据,分析性能瓶颈,并根据分析结果进行优化。

JVM调优主要工作

1.配置jstatd的远程RMI服务

当我们要看远程服务器上JAVA程序的GC情况的时候,需要执行此步骤,允许JVM工具查看JVM使用情况

jstatd 是 Java HotSpot VM 提供的一个监控工具,它通过 RMI (Remote Method Invocation) 提供服务。要配置 jstatd 的远程 RMI 服务,您需要遵循以下步骤:

  1. 创建策略文件 (policy file):在您的 Java 安装目录下,创建一个策略文件,例如 jstatd.all.policy。内容可以是:

    grant codebase "file:${java.home}/../lib/tools.jar" {
        permission java.security.AllPermission;
    };
    

    这个策略文件允许所有的代码库拥有完全权限。

    可以执行 which java查看JAVA_HOME目录

  2. 启动 jstatd:在命令行中输入以下命令:

    jstatd -J-Djava.security.policy=jstatd.all.policy -J-Djava.rmi.server.hostname=<your-hostname>
    

    其中 <your-hostname> 是您的计算机的主机名。如果您不指定 -Djava.rmi.server.hostname,jstatd 将使用默认的主机名。请确保此主机名能够被远程访问到。

    例如:

    jstatd -J-Djava.security.policy=jstatd.all.policy -J-Djava.rmi.server.hostname=10.27.20.38 &
    
    
    

    其中10.27.20.38为你服务器的ip地址,&表示用守护线程的方式运行

  3. 防火墙设置:如果您的计算机有防火墙,确保允许 jstatd 的 RMI 端口(默认为 1099)被远程访问。

  4. 远程访问:现在您可以通过远程的 jstat 命令(或其他任何支持 jstat 的工具)连接到 jstatd 了。例如:

    jstat -J-Djava.security.policy=jstatd.all.policy -J-Djava.rmi.server.hostname=<your-hostname> -J-Djava.rmi.server.port=1099 -t <pid>
    

    其中 <pid> 是您想要监控的 Java 进程的 PID。

注意:在生产环境中,应该仔细考虑安全性和网络设置,确保远程访问是受控的。

2.执行jvisualvm.exe, 打开JVM控制台

打开jvisualvm.exe,远程—添加远程主机—输入远程IP----添加JMX连接

3.对要执行java程序进行调优

在该jar包所在目录下建立一个start.sh文件

在该jar包所在目录下建立一个start.sh文件,文件内容如下。

java -server -Xms4G -Xmx4G -Xmn2G -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1100 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar c1000k.jar&

下面是这些参数的解释:
java:这是 Java 命令,用于启动 Java 应用程序。
-server:告诉 JVM 使用服务器模式。服务器模式会优化性能,但可能会使用更多的资源。
-Xms4G:设置 JVM 的初始堆内存大小为 4GB。
-Xmx4G:设置 JVM 的最大堆内存大小为 4GB。
-Xmn2G:设置新生代的初始内存大小为 2GB。
-XX:SurvivorRatio=1:设置新生代中 Eden 区和 Survivor 区的比例为 1:1。
-XX:+UseConcMarkSweepGC:告诉 JVM 使用 CMS (Concurrent Mark Sweep) 垃圾回收器。CMS 是一种并发的垃圾回收器,可以在不暂停应用程序的情况下进行垃圾回收。
-Dcom.sun.management.jmxremote:启用 JMX (Java Management Extensions),允许远程管理和监控 JVM。
-Dcom.sun.management.jmxremote.port=1100:设置 JMX 远程端口为 1100-Dcom.sun.management.jmxremote.authenticate=false:禁用 JMX 远程认证。
-Dcom.sun.management.jmxremote.ssl=false:禁用 JMX 远程 SSL 加密。
-jar c1000k.jar:指定要运行的 Java 应用程序的 JAR 文件。
&:在 Unix/Linux 系统中,& 符号表示将命令放入后台运行。

总的来说,这个命令启动了一个使用 4GB 堆内存、2GB 新生代内存和 CMS 垃圾回收器的 Java 应用程序,并允许使用 JMX 远程管理和监控。

垃圾回收相关

什么是垃圾回收?为什么需要垃圾回收?

垃圾回收(Garbage Collection)是指清除不再被程序使用的内存对象,释放这些内存空间供程序重新使用的过程。

在Java等高级编程语言中,开发者不需要手动管理内存,因为语言提供了自动内存管理机制。这种机制通过垃圾回收器(Garbage Collector)来实现。垃圾回收器会周期性地检查程序中不再使用的对象,并释放这些对象所占用的内存空间。

需要垃圾回收的原因有以下几点:

1. 资源回收:垃圾回收可以释放不再使用的内存对象,避免内存泄漏和资源浪费。

2. 内存分配:垃圾回收可以帮助程序动态分配内存,减少程序的内存占用。

3. 程序健壮性:垃圾回收可以减少程序中的内存错误,提高程序的健壮性和稳定性。

4. 性能优化:垃圾回收可以优化程序的性能,提高程序的运行效率。

总之,垃圾回收是一种重要的自动内存管理机制,它可以帮助程序管理内存,提高程序的健壮性和性能。

如何理解Minor/Major/Full GC

Java 垃圾回收类型
   |
   |-●Minor GC(Young GC)
   |       |
   |       |-●新生代(Young Generation)
   |       |
   |       |-●回收新生代中不再使用的对象
   |       |
   |       |-●将新生代中的存活对象移动到老年代
   |
   |-●Major GC(Old GC)
   |       |
   |       |-●老年代(Old Generation)
   |       |
   |       |-●回收老年代中不再使用的对象
   |       |
   |       |-●将老年代中的存活对象移动到新生代
   |
   |-●Full GC
           |
           |-●整个 Java 堆(Java Heap)
           |
           |-●同时回收新生代和老年代中的垃圾
           |
           |-●释放整个 Java 堆中的内存空间

JVM垃圾回收算法

垃圾回收算法是一种通过检查程序中不再使用的对象,并释放这些对象所占用的内存空间的算法。垃圾回收算法的主要目标是减少内存泄漏、提高内存利用率、减少内存碎片、提高程序的性能等。

常见的垃圾回收算法包括:

1. 标记-清除算法(Mark-Sweep):标记-清除算法是最基础的垃圾回收算法。它的原理是先标记出所有需要回收的对象,然后清除这些对象。这个算法的缺点是会产生内存碎片,不利于内存的分配和回收。

2. 复制算法(Copying):复制算法是一种针对新生代的垃圾回收算法。它的原理是将内存分为两块,每次只使用其中一块。当一块内存中的对象被标记为垃圾时,将存活的对象复制到另一块内存中,然后清除当前内存中的所有对象。这个算法的优点是简单高效,但是会浪费一半的内存。

3. 标记-压缩算法(Mark-Compact):标记-压缩算法是一种针对老年代的垃圾回收算法。它的原理是先标记出所有需要回收的对象,然后将存活的对象压缩到一端,然后清除压缩后的末端的对象。这个算法的优点是节省内存,但是需要移动对象,可能会产生内存碎片。

4. 分代算法(Generational):分代算法是一种综合利用多种垃圾回收算法的算法。它的原理是根据对象的年龄将内存分为新生代和老年代,然后针对不同代使用不同的垃圾回收算法。新生代通常使用复制算法,老年代通常使用标记-压缩算法。分代算法的优点是结合了多种算法的优点,能够提高垃圾回收的效率。


JVM的垃圾回收算法
    ●标记-清除算法(Mark-Sweep)
        ●原理:先标记出所有需要回收的对象,然后清除这些对象
        ●缺点:会产生内存碎片,不利于内存的分配和回收
    ●复制算法(Copying)
        ●原理:将内存分为两块,每次只使用其中一块
        ●优点:简单高效,但是会浪费一半的内存
    ●标记-压缩算法(Mark-Compact)
        ●原理:先标记出所有需要回收的对象,然后将存活的对象压缩到一端,然后清除压缩后的末端的对象
        ●优点:节省内存,但是需要移动对象,可能会产生内存碎片
    ●分代算法(Generational)
        ●原理:根据对象的年龄将内存分为新生代和老年代,然后针对不同代使用不同的垃圾回收算法
        ●优点:结合了多种算法的优点,能够提高垃圾回收的效率

在这里插入图片描述

JVM有哪些垃圾回收器,各自的特点和适用场景

常见的垃圾回收器
    ●SerialGC
        ●名词解释:Serial Garbage Collector,串行垃圾回收器
        ●算法:复制算法和标记-清除算法
        ●优点:简单高效,适用于单核CPU和小内存环境
        ●缺点:只能使用单个CPU核心,不适用于大内存环境
        ●适用场景:小型Web应用、移动应用
    ●ParallelGC
        ●名词解释:Parallel Garbage Collector,并行垃圾回收器
        ●算法:复制算法和标记-清除算法
        ●优点:多线程并发处理,适用于多核CPU和大内存环境
        ●缺点:停顿时间较长,不适用于低延迟要求的应用
        ●适用场景:大型Web应用、大数据处理应用
    ●CMS GC
        ●名词解释:Concurrent Mark-Sweep Garbage Collector,并发标记-清除垃圾回收器
        ●算法:标记-清除算法
        ●优点:并发处理,停顿时间较短,适用于大内存环境
        ●缺点:停顿时间仍然较长,不适用于低延迟要求的应用
        ●适用场景:大型Web应用、实时数据处理应用
    ●G1 GC
        ●名词解释:Garbage-First Garbage Collector,垃圾优先垃圾回收器
        ●算法:复制算法和标记-压缩算法
        ●优点:根据程序运行情况动态调整垃圾回收策略,适用于大内存环境
        ●缺点:停顿时间较长,不适用于低延迟要求的应用
        ●适用场景:大型Web应用、大数据处理应用
    ●ZGC
        ●名词解释:Z Garbage Collector,Z垃圾回收器
        ●算法:复制算法和标记-压缩算法
        ●优点:低延迟,停顿时间较短,适用于低延迟要求的应用
        ●缺点:仅适用于64位系统
        ●适用场景:实时数据处理应用、低延迟要求的应用
    ●Shenandoah
        ●名词解释:Shenandoah Garbage Collector,Shenandoah垃圾回收器
        ●算法:复制算法和标记-压缩算法
        ●优点:低延迟,停顿时间较短,适用于低延迟要求的应用
        ●缺点:仅适用于64位系统
        ●适用场景:实时数据处理应用、低延迟要求的应用

在这里插入图片描述

详细介绍一下 CMS 垃圾回收器

CMS(Concurrent Mark Sweep)垃圾收集器是为老年代设计的垃圾收集器,其主要目的是达到最短的垃圾回收停顿时间,基于线程的标记清除算法实现,以便在多线程并发环境下以最短的垃圾收集停顿时间提高系统的稳定性。
以下是 CMS 垃圾回收器的工作过程、优点和缺点:

CMS (Concurrent Mark-Sweep) 垃圾回收器
│
├─ 工作过程
│  ├─ 初始标记 (Initial Mark)
│  ├─ 并发标记 (Concurrent Mark)
│  ├─ 重新标记 (Remark)
│  ├─ 并发清除 (Concurrent Sweep)
│  └─ 并发重置 (Concurrent Reset)
│
├─ 优点
│  ├─ 减少停顿时间
│  ├─ 适用于大型堆内存
│  └─ 相对其他垃圾回收器有优势
│
└─ 缺点
   ├─ 可能导致吞吐量降低
   ├─ 内存碎片化问题
   └─ 不适用于所有应用程序



1. 工作过程:
   - 初始标记(Initial Mark):暂停应用程序线程,标记根对象(例如静态变量、活动线程的栈上的引用等),并标记与之直接关联的对象。
   - 并发标记(Concurrent Mark):标记所有与根对象直接或间接关联的对象,这个过程与应用程序线程并发执行。
   - 重新标记(Remark):暂停应用程序线程,标记在并发标记阶段中有可能被修改的对象。
   - 并发清除(Concurrent Sweep):清除未被标记为存活的对象,这个过程与应用程序线程并发执行。
   - 并发重置(Concurrent Reset):重置垃圾回收器的状态,为下一次垃圾回收做准备。

2. 优点:
   - 并发标记和清除可以减少应用程序的停顿时间,提高了应用程序的响应性。
   - 适用于大型堆内存,因为在并发标记和清除过程中不需要暂停整个应用程序。
   - 与其他垃圾回收器相比,CMS 在应用程序暂停时间方面有明显的优势。

3. 缺点:
   - 并发标记和清除可能会导致应用程序的吞吐量降低,因为在并发标记和清除过程中,部分 CPU 和内存资源会被垃圾回收器占用。
   - 由于并发标记和清除过程中没有进行全局整理,可能会导致内存碎片化问题,进而影响性能。
   - CMS 不适用于所有应用程序,对于大型堆内存和高并发的应用程序,可能会出现并发问题。

总的来说,CMS 垃圾回收器通过并发标记和清除的方式来实现垃圾回收,减少了应用程序的停顿时间,提高了应用程序的响应性,但也存在一些缺点,如可能降低应用程序的吞吐量和内存碎片化问题。

详细介绍一下G1垃圾回收器

G1(Garbage-First)垃圾回收器是Java虚拟机的一种垃圾回收器,它是一种低停顿时间(Low Pause)的垃圾回收器,主要用于降低垃圾回收时的停顿时间,以提高应用程序的响应性能。G1垃圾回收器是Java 7版本中引入的,它是CMS垃圾回收器的一种替代方案,主要用于替代CMS垃圾回收器。
G1(Garbage-First)垃圾回收器的设计目标之一是尽量减少内存碎片。G1的设计思想是将堆空间划分为多个大小相等的区域(Region),每个区域都是一个连续的内存块,包含多个对象。G1会根据应用程序的运行情况动态地选择回收哪些区域,以尽量减少内存碎片。
G1会在堆中选择回收价值最高的区域来进行回收,这通常是那些包含大量已死对象的区域。通过选择回收价值最高的区域,G1可以尽量减少内存碎片,并且提高回收效率。
此外,G1还会对堆中的对象进行重分配,以尽量减少堆中的碎片。在并发重分配阶段,G1会将堆中的对象移动到新的位置,以尽量减少碎片。这可以进一步减少内存碎片,并且提高内存利用率。


 G1垃圾回收器的工作过程:
初始标记(Initial Mark):在这个阶段,G1垃圾回收器会标记所有的根对象,并标记被根对象直接引用的对象。这个阶段是短暂的,因为只需要标记根对象和直接引用的对象。

并发标记(Concurrent Mark):在初始标记之后,G1垃圾回收器将继续进行并发标记。在这个阶段,G1会遍历堆中的对象图,并标记所有可达的对象。这个阶段是并发的,即在执行的同时,应用程序可以继续执行。这个阶段的结束标志是“活动数据集”(Active Data Set)的大小达到了一定的阈值。

重新标记(Remark):在并发标记结束后,G1垃圾回收器会进行一次短暂的停顿,进行重新标记。在这个阶段,G1会遍历堆中的对象图,重新标记所有在并发标记期间产生的新对象。这个阶段是短暂的,因为只需要重新标记新对象。

筛选回收(Cleanup):在重新标记之后,G1垃圾回收器会进行筛选回收。在这个阶段,G1会根据堆中各个区域的回收价值(Recycling Efficiency)来决定哪些区域需要进行回收。G1会选择回收价值最高的区域来进行回收,以最大限度地提高回收效率。这个阶段是并发的,即在执行的同时,应用程序可以继续执行。

并发清理(Concurrent Cleanup):在筛选回收之后,G1垃圾回收器会进行并发清理。在这个阶段,G1会回收筛选回收阶段选择的区域,并将这些区域标记为可用的内存。这个阶段是并发的,即在执行的同时,应用程序可以继续执行。

并发重分配(Concurrent Relocation):在并发清理之后,G1垃圾回收器会进行并发重分配。在这个阶段,G1会对堆中的对象进行重分配,以尽量减少堆中的碎片。这个阶段是并发的,即在执行的同时,应用程序可以继续执行。

并发清理(Concurrent Cleanup):在并发重分配之后,G1垃圾回收器会进行最后一次并发清理。在这个阶段,G1会回收所有未使用的区域,并将这些区域标记为可用的内存。这个阶段是并发的,即在执行的同时,应用程序可以继续执行。

 G1垃圾回收器的主要特点:
1. 低停顿时间:G1垃圾回收器采用并发标记和清除的方式,减少了垃圾回收的停顿时间。在多核处理器上,G1可以利用多个线程并行标记和清除垃圾,加快了垃圾回收的速度。
2. 分代回收:G1垃圾回收器将堆内存分成多个区域,可以根据应用程序的需求进行配置,以达到更好的性能。
3. 可预测的停顿时间:G1垃圾回收器可以根据应用程序的需求,设置目标停顿时间,以达到可预测的停顿时间。
4. 适用于大堆内存:由于G1垃圾回收器可以根据应用程序的需求,将堆内存分成多个区域,因此适用于大堆内存的场景。

 G1垃圾回收器的优点:
优点:
1.基于标记整理算法,不产生内存碎片。
2.可以精确地控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收。

缺点:
1.内存占用: G1垃圾回收器在运行时需要维护额外的数据结构,这可能会导致更多的内存占用。这对于内存较小的应用程序来说可能是一个问题。
2.停顿时间: 尽管G1垃圾回收器旨在提供可预测的停顿时间,但在某些情况下,停顿时间仍然可能会变得不可预测。这可能会对某些应用程序产生负面影响。
3.性能: 在某些情况下,G1垃圾回收器可能会导致性能下降。这主要是因为G1垃圾回收器在处理大量并发请求时可能会导致CPU使用率过高。
4.配置: G1垃圾回收器有许多配置参数,需要根据应用程序的需求进行调整。这可能会增加配置的复杂

以下是与G1垃圾回收器相关的一些JVM参数:

1. `-XX:+UseG1GC`:启用G1垃圾回收器。
2. `-XX:MaxGCPauseMillis=<N>`:设置目标停顿时间(MaxGCPauseMillis),即G1垃圾回收器的目标是在不超过N毫秒的情况下尽量减少停顿时间。默认值为200毫秒。
3. `-XX:InitiatingHeapOccupancyPercent=<N>`:设置触发Mixed GC的堆空间使用率(InitiatingHeapOccupancyPercent)。当堆空间使用率达到N%时,G1垃圾回收器会触发Mixed GC。
4. `-XX:G1HeapRegionSize=<N>`:设置G1堆区域大小(G1HeapRegionSize)。默认值为2MB。G1堆区域大小是影响G1垃圾回收器性能的重要参数,通常通过调整这个参数来改变G1垃圾回收器的性能。
5. `-XX:ConcGCThreads=<N>`:设置并发GC线程数(ConcGCThreads)。默认值为处理器核心数的1/4。
6. `-XX:ParallelGCThreads=<N>`:设置并行GC线程数(ParallelGCThreads)。默认值为处理器核心数的1/4。
7. `-XX:G1ReservePercent=<N>`:设置保留空间百分比(G1ReservePercent)。默认值为10%,即G1垃圾回收器会保留10%的堆空间作为保留空间,用于避免OutOfMemoryError。

这些JVM参数可以通过在启动Java程序时使用`-XX`选项来设置,例如:
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -XX:G1HeapRegionSize=4 -XX:ConcGCThreads=4 -XX:ParallelGCThreads=4 -XX:G1ReservePercent=10 -jar your-application.jar


这里的`your-application.jar`是您的Java应用程序的jar包名称。您可以根据应用程序的需求,调整这些参数来达到更好的性能。

G1调优最佳实践

G1垃圾回收器的调优最佳实践主要包括以下几个方面:

  1. G1垃圾回收器的调优最佳实践主要包括以下几个方面:

    1. 选择合适的区域大小(Region Size): G1将堆内存划分为多个大小相等的区域(Region),区域的大小对于G1垃圾回收器的性能影响很大。通常情况下,G1垃圾回收器的区域大小可以设置为2MB到32MB之间。较小的区域大小可以减少垃圾回收的停顿时间,但会增加垃圾回收的频率。较大的区域大小可以减少垃圾回收的频率,但会增加垃圾回收的停顿时间。因此,选择合适的区域大小是非常重要的。

    2. 设置目标停顿时间(Target Pause Time): G1垃圾回收器可以根据应用程序的需求,设置目标停顿时间。目标停顿时间是指G1垃圾回收器在进行垃圾回收时,希望达到的停顿时间。G1会根据目标停顿时间来调整垃圾回收的策略,以尽量减少停顿时间。通常情况下,目标停顿时间可以设置为100ms到200ms之间。较长的目标停顿时间可以减少垃圾回收的频率,但会增加垃圾回收的停顿时间。较短的目标停顿时间可以减少垃圾回收的停顿时间,但会增加垃圾回收的频率。因此,选择合适的目标停顿时间是非常重要的。

    3. 调整并发线程数(Concurrent Threads): G1垃圾回收器可以根据应用程序的需求,调整并发线程数。并发线程数是指G1垃圾回收器在进行垃圾回收时,可以同时执行的线程数。通常情况下,G1垃圾回收器的并发线程数可以设置为2到8之间。较少的并发线程数可以减少垃圾回收的CPU使用率,但会增加垃圾回收的停顿时间。较多的并发线程数可以减少垃圾回收的停顿时间,但会增加垃圾回收的CPU使用率。因此,选择合适的并发线程数是非常重要的。

    4. 调整老年代的大小(Old Generation Size): G1垃圾回收器可以根据应用程序的需求,调整老年代的大小。老年代是指G1垃圾回收器中的主要垃圾回收区域,存放着长时间存活的对象。通常情况下,老年代的大小可以设置为整个堆内存的一半到三分之二之间。较小的老年代大小可以减少垃圾回收的停顿时间,但会增加垃圾回收的频率。较大的老年代大小可以减少垃圾回收的频率,但会增加垃圾回收的停顿时间。因此,选择合适的老年代大小是非常重要的。

    5. 调整新生代的大小(Young Generation Size): G1垃圾回收器可以根据应用程序的需求,调整新生代的大小。新生代是指G1垃圾回收器中的辅助垃圾回收区域,存放着新创建的对象。通常情况下,新生代的大小可以设置为整个堆内存的一半到三分之一之间。较小的新生代大小可以减少垃圾回收的停顿时间,但会增加垃圾回收的频率。较大的新生代大小可以减少垃圾回收的频率,但会增加垃圾回收的停顿时间。因此,选择合适的新生代大小是非常重要的。

    6. 调整Eden区和Survivor区的比例(Eden/Survivor Ratio): G1垃圾回收器可以根据应用程序的需求,调整Eden区和Survivor区的比例。Eden区是指G1垃圾回收器中的主要新生代区域,存放着新创建的对象。Survivor区是指G1垃圾回收器中的辅助新生代区域,存放着部分存活的对象。通常情况下,Eden区和Survivor区的比例可以设置为1:1到1:4之间。较大的Eden区和Survivor区的比例可以减少垃圾回收的停顿时间,但会增加垃圾回收的频率。较小的Eden区和Survivor区的比例可以减少垃圾回收的频率,但会增加垃圾回收的停顿时间。因此,选择合适的Eden区和Survivor区的比例是非常重要的。

    7. 调整空闲区的比例(Idle Space Ratio): G1垃圾回收器可以根据应用程序的需求,调整空闲区的比例。空闲区是指G1垃圾回收器中的未使用的区域,用于存放着未使用的对象。通常情况下,空闲区的比例可以设置为1:1到1:2之间。较大的空闲区的比例可以减少垃圾回收的停顿时间,但会增加垃圾回收的频率。较小的空闲区的比例可以减少垃圾回收的频率,但会增加垃圾回收的停顿时间。因此,选择合适的空闲区的比例是非常重要的。

    8. 调整并发线程数(Concurrent Threads): G1垃圾回收器可以根据应用程序的需求,调整并发线程数。并发线程数是指G1垃圾回收器在进行垃圾回收时,可以同时执行的线程数。通常情况下,G1垃圾回收器的并发线程数可以设置为2到8之间。较少的并发线程数可以减少垃圾回收的CPU使用率,但会增加垃圾回收的停顿时间。较多的并发线程数可以减少垃圾回收的停顿时间,但会增加垃圾回收的CPU使用率。因此,选择合适的并发线程数是非常重要的。

1. 启用G1垃圾收集器:在启动JVM时,通过参数 `-XX:+UseG1GC` 来启用G1垃圾收集器。

2. 设置G1最大内存:通过参数 `-Xmx` 来设置G1垃圾收集器的最大内存,以确保G1垃圾收集器有足够的内存来工作。

3. 设置G1初始内存:通过参数 `-Xms` 来设置G1垃圾收集器的初始内存,以确保G1垃圾收集器有足够的内存来工作。

4. 设置G1垃圾回收时间:通过参数 `-XX:MaxGCPauseMillis` 来设置G1垃圾收集器的最大停顿时间,以确保G1垃圾收集器能够在指定的时间内完成垃圾回收。

5. 设置G1垃圾回收阶段:通过参数 `-XX:G1MixedGCCountTarget` 来设置G1垃圾收集器的混合垃圾回收阶段的目标次数,以确保G1垃圾收集器能够在指定的次数内完成混合垃圾回收。

6. 设置G1年轻代空间:通过参数 `-XX:G1NewSizePercent` 来设置G1垃圾收集器的年轻代空间的百分比,以确保G1垃圾收集器有足够的年轻代空间来工作。

7. 设置G1老年代空间:通过参数 `-XX:G1OldCSetRegionLiveThresholdPercent` 来设置G1垃圾收集器的老年代空间的百分比,以确保G1垃圾收集器有足够的老年代空间来工作。

8. 设置G1堆空间:通过参数 `-XX:G1HeapRegionSize` 来设置G1垃圾收集器的堆空间的大小,以确保G1垃圾收集器有足够的堆空间来工作。

9. 设置G1线程数:通过参数 `-XX:ParallelGCThreads` 来设置G1垃圾收集器的线程数,以确保G1垃圾收集器有足够的线程来工作。

10. 设置G1日志:通过参数 `-XX:+PrintGCDetails` 和 `-XX:+PrintGCDateStamps` 来设置G1垃圾收集器的日志输出,以便更好地了解G1垃圾收集器的工作情况。

你了解哪些垃圾回收器的工作原理

垃圾回收器是一种内存管理机制,用于自动检测和释放不再被程序使用的内存。
常见的垃圾回收器包括串行垃圾回收器(Serial GC)、并行垃圾回收器(Parallel GC)、CMS垃圾回收器(Concurrent Mark-Sweep GC)、G1垃圾回收器(Garbage-First GC)等。下面是这几种垃圾回收器的工作原理:

1. 串行垃圾回收器(Serial GC):串行垃圾回收器是最简单的垃圾回收器,它使用单线程进行垃圾回收。在串行垃圾回收器的工作过程中,应用程序的所有线程都会被暂停,直到垃圾回收完成。这种垃圾回收器适用于单核处理器或者对垃圾回收停顿时间要求不高的场景。

2. 并行垃圾回收器(Parallel GC):并行垃圾回收器使用多线程进行垃圾回收,可以充分利用多核处理器的优势,加快垃圾回收的速度。在并行垃圾回收器的工作过程中,应用程序的所有线程都会被暂停,直到垃圾回收完成。

3. CMS垃圾回收器(Concurrent Mark-Sweep GC):CMS垃圾回收器是一种低停顿时间(Low Pause)的垃圾回收器,主要用于降低垃圾回收时的停顿时间,以提高应用程序的响应性能。CMS垃圾回收器采用并发标记和清除的方式,减少了垃圾回收的停顿时间。在多核处理器上,CMS可以利用多个线程并行标记和清除垃圾,加快了垃圾回收的速度。

4. G1垃圾回收器(Garbage-First GC):G1垃圾回收器是一种低停顿时间(Low Pause)的垃圾回收器,主要用于降低垃圾回收时的停顿时间,以提高应用程序的响应性能。G1垃圾回收器采用分代回收的方式,将堆内存分成多个区域,可以根据应用程序的需求进行配置,以达到更好的性能。G1垃圾回收器的工作过程包括初始标记阶段、并发标记阶段、重新标记阶段、并发清除阶段。

新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

在Java虚拟机中,新生代和老生代是两个主要的内存区域,分别用于存储新创建的对象和存储存活时间较长的对象。Java虚拟机中有多种垃圾回收器用于对新生代和老生代进行垃圾回收,以下是一些常见的垃圾回收器:

新生代垃圾回收器:
1. Serial GC:Serial GC是一种串行垃圾回收器,它通过单个线程对新生代进行垃圾回收。Serial GC适用于单核CPU和小内存的场景,可以通过`-XX:+UseSerialGC`选项启用。

2. ParNew GC:ParNew GC是一种并行垃圾回收器,它通过多个线程对新生代进行垃圾回收。ParNew GC适用于多核CPU和大内存的场景,可以通过`-XX:+UseParNewGC`选项启用。

3. G1 GC:G1 GC是一种分代垃圾回收器,它通过多个线程对新生代进行垃圾回收。G1 GC适用于多核CPU和大内存的场景,可以通过`-XX:+UseG1GC`选项启用。

老生代垃圾回收器:
1. Serial Old GC:Serial Old GC是一种串行垃圾回收器,它通过单个线程对老生代进行垃圾回收。Serial Old GC适用于单核CPU和小内存的场景,可以通过`-XX:+UseSerialOldGC`选项启用。

2. Parallel Old GC:Parallel Old GC是一种并行垃圾回收器,它通过多个线程对老生代进行垃圾回收。Parallel Old GC适用于多核CPU和大内存的场景,可以通过`-XX:+UseParallelOldGC`选项启用。

3. CMS GC:CMS GC是一种并发垃圾回收器,它通过多个线程对老生代进行垃圾回收。CMS GC适用于多核CPU和大内存的场景,可以通过`-XX:+UseConcMarkSweepGC`选项启用。

以上是一些常见的新生代和老生代垃圾回收器,可以根据实际情况选择合适的垃圾回收器。新生代和老生代垃圾回收器的主要区别在于回收算法和线程数。新生代垃圾回收器通常使用复制算法,老生代垃圾回收器通常使用标记-清除算法或标记-整理算法。新生代垃圾回收器通常使用单个线程,老生代垃圾回收器通常使用多个线程。

对象被垃圾回收的条件

在Java中,对象是否可以被回收(GC)是由JVM的垃圾回收器来决定的,而不是由程序员来判断的。垃圾回收器会根据对象的存活情况、引用情况等来判断对象是否可以被回收。通常情况下,只有满足以下条件的对象才会被回收:

对象不再被任何引用指向,即没有任何活动的引用指向该对象。
对象所在的线程没有活动的引用指向该对象。
对象所在的线程没有活动的引用指向该对象的引用(即该对象的引用被置为null)。
对象所在的线程已经离开了对象的作用域。
对象所在的线程已经结束了,或者对象所在的线程已经停止了。
当对象满足以上条件时,垃圾回收器会将该对象标记为可回收的,然后在下次垃圾回收时将其回收并释放内存。需要注意的是,对象的finalize()方法不会影响垃圾回收器对对象的回收判断,只有满足以上条件的对象才会被回收。

JVM判断对象是否可以被回收的算法

JVM判断对象是否可以被回收的算法主要有两种:引用计数法和可达性分析法。

引用计数法:
引用计数法是一种简单的算法,它通过计算对象的引用数量来判断对象是否可以被回收。每个对象都有一个引用计数,当对象被引用时,引用计数加1,当对象不再被引用时,引用计数减1。当引用计数为0时,表示对象不再被引用,可以被回收。
引用计数法的优点是简单,实现起来比较容易,但是它有一个明显的缺点是无法解决循环引用的问题。比如,如果对象A引用了对象B,而对象B又引用了对象A,那么它们的引用计数都不为0,即使它们不再被其他对象引用,也无法被回收,从而导致内存泄漏。
可达性分析法:

可达性分析法是一种更加复杂的算法,它通过分析对象的引用关系来判断对象是否可以被回收。可达性分析法从一组根对象(比如虚拟机栈、本地方法栈、方法区中的静态变量)开始,递归地遍历所有被引用的对象,标记被引用的对象为可达对象,然后将不可达对象标记为可回收对象。
可达性分析法可以解决循环引用的问题,因为它通过遍历引用关系来判断对象是否可以被回收,不会出现引用计数法的循环引用问题。

JVM通常使用可达性分析法来判断对象是否可以被回收,因为它可以解决循环引用的问题,保证对象的正确回收。但是,可达性分析法的实现比较复杂,会消耗一定的性能,所以在某些特定场景下,JVM也可能使用引用计数法来判断对象是否可以被回收。


├── 引用计数法
│   ├── 对象的引用计数为0
│   ├── 优点:简单易实现
│   └── 缺点:无法解决循环引用问题
└── 可达性分析法
    ├── 从一组根对象出发,递归遍历所有被引用的对象
    ├── 标记被引用的对象为可达对象,将不可达对象标记为可回收对象
    └── 优点:可以解决循环引用问题

在这里插入图片描述

简述分代垃圾回收器是怎么工作的

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。

新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

· 把 Eden + From Survivor 存活的对象放入 To Survivor 区;

· 清空 Eden 和 From Survivor 分区;

· From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。

老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

什么是分布式垃圾回收(DGC)?它是如何工作的

分布式垃圾回收(Distributed Garbage Collection,DGC)是指在分布式系统中对垃圾对象进行回收的过程。
在分布式系统中,由于对象的生命周期可能涉及到多个节点,因此需要对这些节点上的垃圾对象进行回收,以释放内存空间。
DGC是一种垃圾回收机制,用于在分布式系统中对垃圾对象进行回收。

DGC通常工作流程如下:

1. 标记阶段:首先,DGC会在分布式系统中的每个节点上进行标记,标记出哪些对象是可达的(即存活的),哪些对象是不可达的(即垃圾的)。

2. 传输阶段:然后,DGC会将标记结果传输到分布式系统中的每个节点上,使得每个节点都能知道哪些对象是可达的,哪些对象是不可达的。

3. 回收阶段:最后,DGC会在分布式系统中的每个节点上对不可达的对象进行回收,以释放内存空间。

DGC的工作原理是通过在分布式系统中的每个节点上进行标记、传输和回收,从而对垃圾对象进行回收。DGC通常使用分布式垃圾回收算法来实现对垃圾对象的回收,如分布式标记-清除算法、分布式复制算法、分布式标记-整理算法等。

问题和故障

内存泄漏

内存泄漏是指程序中的对象在不再使用时,没有被正确释放,导致占用的内存无法被回收,最终导致内存溢出的情况。内存泄漏通常是由于以下几个原因造成的:

1. 对象引用未被释放:在程序中,如果对象的引用没有被正确释放,即使程序中不再使用这个对象,它仍然占用内存空间。这样就会导致内存泄漏。

2. 长生命周期的对象:一些对象的生命周期可能很长,它们可能被存储在静态变量或全局变量中,导致无法被正确释放。这样也会导致内存泄漏。

3. 资源未释放:在程序中,如果资源(如文件、数据库连接、网络连接等)没有被正确释放,会导致内存泄漏。这是因为资源占用了内存空间,并且无法被回收。

4. 循环引用:如果程序中存在循环引用的情况,即两个或多个对象相互引用,而且没有其他对象引用这些对象,这样就会导致内存泄漏。

5. 缓存未清理:在程序中,如果缓存没有被正确清理,会导致缓存中的对象无法被回收,从而导致内存泄漏。

6. 监听器未移除:在程序中,如果监听器没有被正确移除,会导致监听器持有对象的引用,从而导致对象无法被回收,从而导致内存泄漏。

要解决内存泄漏问题,可以采取以下几个方法:

1. 正确释放对象引用:在程序中,要确保对象的引用在不再使用时被正确释放。

2. 注意对象生命周期:在程序中,要注意对象的生命周期,及时释放不再使用的对象。

3. 使用资源管理器:在程序中,要使用资源管理器来管理资源,确保资源在不再使用时被正确释放。

4. 避免循环引用:在程序中,要避免循环引用的情况,确保对象的引用关系是单向的。

5. 及时清理缓存:在程序中,要及时清理缓存,确保缓存中的对象在不再使用时被正确释放。

6. 移除监听器:在程序中,要在不再需要监听器时,移除监听器,确保对象的引用关系被正确释放。

通过以上方法,可以有效地避免和解决内存泄漏问题。

内存溢出(Out of Memory)

OOM (Out Of Memory) 是一种 Java 程序运行时可能遇到的错误。当 Java 程序试图分配内存时,如果堆内存中没有足够的空间来容纳新的对象,就会发生 Out Of Memory 错误。

OOM 错误通常由以下几个原因引起:

1. 内存泄漏:Java 程序中可能存在内存泄漏,导致无法释放已经不再使用的内存。这样会导致堆内存中的空间被占满,无法再分配给新的对象。

2. 堆内存设置不足:Java 程序的堆内存设置可能不足以支撑程序的内存需求。可以通过调整堆内存的大小来解决这个问题。

3. 对象过多:Java 程序中创建的对象过多,导致堆内存被占满。可以通过减少对象的创建次数或者优化程序来解决这个问题。

4. 内存分配失败:Java 程序试图分配内存时,操作系统的内存不足,导致分配失败。可以通过增加操作系统的内存来解决这个问题。

5. 死循环:Java 程序中可能存在死循环,导致程序一直运行下去,不会释放内存。可以通过检查代码逻辑来解决这个问题。

在发生 OOM 错误时,Java 程序会抛出 java.lang.OutOfMemoryError 异常,程序会停止运行。可以通过查看异常信息来确定具体的错误原因,并根据错误原因采取相应的措施来解决问题。

内存泄露和内存溢出的区别和联系

内存泄漏(Memory Leak)和内存溢出(Out of Memory)是两种不同的内存问题,但它们之间有一定的联系。

1. 内存泄漏:内存泄漏指的是程序中的对象在不再使用时,没有被正确释放,导致占用的内存无法被回收。内存泄漏通常是由于程序中的对象引用没有被正确释放、长生命周期的对象没有被正确释放、资源没有被正确释放、循环引用等原因造成的。

2. 内存溢出:内存溢出指的是程序中的内存使用超过了系统或虚拟机所能提供的内存限制。内存溢出通常是由于程序中创建了大量的对象、对象生命周期过长、资源占用过多、循环引用等原因造成的。

联系:
●内存泄漏和内存溢出都与内存管理有关,都会导致程序运行出现问题。
●内存泄漏和内存溢出都会导致程序的性能下降,甚至导致程序崩溃。
●内存泄漏和内存溢出都需要程序员检查代码、分析内存使用情况、优化程序等手段来解决。

区别:
●内存泄漏是指程序中的对象在不再使用时,没有被正确释放,导致占用的内存无法被回收;而内存溢出是指程序中的内存使用超过了系统或虚拟机所能提供的内存限制。
●内存泄漏通常是由于程序中的对象引用没有被正确释放、长生命周期的对象没有被正确释放、资源没有被正确释放、循环引用等原因造成的;而内存溢出通常是由于程序中创建了大量的对象、对象生命周期过长、资源占用过多、循环引用等原因造成的。
●内存泄漏是由于程序中的对象没有被正确释放,导致占用的内存无法被回收;而内存溢出是由于程序中的内存使用超过了系统或虚拟机所能提供的内存限制。

GC频繁

GC (Garbage Collection) 是 Java 虚拟机自动管理内存的过程。在 Java 中,内存是由虚拟机自动分配和回收的,程序员无需手动管理内存。Java 中的 GC 主要用于回收不再使用的对象,释放其占用的内存空间。GC 频繁通常有以下几个原因:

1. 内存泄漏:Java 程序中可能存在内存泄漏,导致无法释放已经不再使用的内存。这样会导致频繁的 GC,以释放堆内存中被占用的空间。

2. 对象过多:Java 程序中创建的对象过多,导致堆内存被占满。这样会导致频繁的 GC,以释放堆内存中被占用的空间。

3. 堆内存设置不足:Java 程序的堆内存设置可能不足以支撑程序的内存需求。这样会导致频繁的 GC,以释放堆内存中被占用的空间。

4. 对象生命周期长:Java 程序中的对象生命周期过长,导致这些对象无法被 GC 回收。这样会导致堆内存中的空间被占满,无法再分配给新的对象。

5. 大对象的分配:Java 程序中可能存在大对象的分配,导致堆内存中的空间被占满。这样会导致频繁的 GC,以释放堆内存中被占用的空间。

6. 堆内存碎片:Java 程序中可能存在堆内存碎片,导致无法分配连续的内存空间给新的对象。这样会导致频繁的 GC,以释放堆内存中被占用的空间。

在发生频繁 GC 的情况下,可以通过调整堆内存的大小、优化程序、检查代码逻辑等方式来解决问题。可以通过查看 GC 日志来确定具体的 GC 原因,并根据 GC 原因采取相应的措施来解决问题。

死锁

死锁是指两个或多个线程互相等待对方释放资源而无法继续执行的情况。在 Java 中,死锁通常是由于多个线程持有不同的锁,并且互相等待对方释放锁而导致的。

以下是一个简单的死锁示例:
public class DeadlockExample {
    public static void main(String[] args) {
        final Object lock1 = new Object();
        final Object lock2 = new Object();

        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                synchronized (lock1) {
                    System.out.println("Thread1 acquired lock1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (lock2) {
                        System.out.println("Thread1 acquired lock2");
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                synchronized (lock2) {
                    System.out.println("Thread2 acquired lock2");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (lock1) {
                        System.out.println("Thread2 acquired lock1");
                    }
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这个例子中,thread1 线程首先获取 lock1 锁,然后尝试获取 lock2 锁;而 thread2 线程首先获取 lock2 锁,然后尝试获取 lock1 锁。由于两个线程互相等待对方释放锁,因此会发生死锁。

要避免死锁,可以采用以下几个方法:

1. 避免嵌套锁:尽量避免在一个锁的 synchronized 块中再次获取另一个锁。

2. 按顺序获取锁:如果多个线程需要获取多个锁,可以按照固定的顺序获取锁,从而避免发生死锁。

3. 使用可重入锁:可重入锁(ReentrantLock)是一种支持重入的锁,可以在同一个线程中多次获取同一个锁而不会发生死锁。

4. 使用锁超时:可以使用 tryLock() 方法尝试获取锁,并设置超时时间,以避免线程无限等待锁而发生死锁。

5. 使用死锁检测工具:可以使用死锁检测工具来检测死锁,并对其进行解决。

通过以上方法,可以有效地避免和解决 Java 中的死锁问题。

线程池不够用

当 Java 线程池不够用时,可能是由于以下几个原因造成的:

1. 线程池配置不合理:线程池的核心线程数、最大线程数、队列容量等配置不合理,导致线程池无法满足程序的需求。可以通过调整线程池的配置来解决问题。

2. 任务执行时间过长:线程池中的任务执行时间过长,导致线程池中的线程长时间被占用,无法处理新的任务。可以通过优化任务的执行时间来解决问题。

3. 任务队列阻塞:线程池中的任务队列阻塞,导致线程池中的线程长时间等待新的任务。可以通过增加任务队列的容量、调整任务队列的实现等方式来解决问题。

4. 线程泄漏:线程池中的线程泄漏,导致线程池中的线程长时间占用内存而无法被回收。可以通过检查代码逻辑、优化线程池的使用等方式来解决问题。

5. 资源不足:系统资源不足,导致无法创建新的线程。可以通过增加系统资源、优化线程池的使用等方式来解决问题。

要解决线程池不够用的问题,可以采取以下几个方法:

1. 调整线程池配置:可以通过调整线程池的核心线程数、最大线程数、队列容量等配置来解决问题。

2. 优化任务的执行时间:可以通过优化任务的执行时间来减少线程池中的线程占用时间,从而提高线程池的利用率。

3. 增加任务队列的容量:可以通过增加任务队列的容量来减少线程池中的线程等待时间,从而提高线程池的利用率。

4. 优化线程池的使用:可以通过检查代码逻辑、优化线程池的使用等方式来减少线程池中的线程占用时间,从而提高线程池的利用率。

5. 增加系统资源:可以通过增加系统资源来提高系统的性能,从而解决线程池不够用的问题。

CPU负载过高

Java 程序的 CPU Load(CPU 负载)高通常是由于程序的计算密集型任务、IO 密集型任务或者内存泄漏等问题引起的。下面是一些可能导致 Java 程序 CPU Load 高的原因:

1. 计算密集型任务:如果 Java 程序中有大量的计算密集型任务,会导致 CPU Load 高。这可能是因为代码中有大量的循环、递归或者复杂的计算逻辑。

2. IO 密集型任务:如果 Java 程序中有大量的 IO 操作,比如读取文件、网络通信等,会导致 CPU Load 高。这可能是因为 IO 操作需要等待外部资源的响应,而 CPU 需要等待 IO 操作完成。

3. 内存泄漏:如果 Java 程序中存在内存泄漏,会导致 JVM 中的内存占用不断增加,最终导致 CPU Load 高。这可能是因为程序中有大量的对象没有被正确地释放。

4. 线程阻塞:如果 Java 程序中存在线程阻塞,比如线程死锁、线程等待资源等,会导致 CPU Load 高。这可能是因为线程在等待资源时会占用 CPU 资源。

5. JVM 参数不合理:如果 Java 程序中的 JVM 参数设置不合理,比如堆内存设置过小、GC 策略不合理等,会导致 CPU Load 高。这可能是因为 JVM 在执行 GC 操作时会占用大量的 CPU 资源。

6. 并发编程问题:如果 Java 程序中存在并发编程问题,比如竞态条件、死锁等,会导致 CPU Load 高。这可能是因为并发问题会导致 CPU 资源被浪费。

解决 Java 程序 CPU Load 高的方法包括:

●优化程序中的计算密集型任务,减少 CPU 负载。
●优化程序中的 IO 密集型任务,减少 IO 操作对 CPU 的占用。
●修复程序中的内存泄漏问题,释放不需要的对象。
●优化程序中的线程阻塞问题,减少线程等待时间。
●调整 JVM 参数,优化 GC 策略,减少 GC 对 CPU 的占用。
●解决并发编程问题,避免竞态条件、死锁等问题。

JVM性能优化问题排查

JVM 性能调优是一个复杂的过程,需要综合考虑多个因素。在排查 JVM 性能问题时,可以采取以下几个步骤:

1. 监控 JVM 资源使用情况:首先,需要监控 JVM 的资源使用情况,包括 CPU 使用率、内存使用情况、线程数量、GC 次数和耗时等。可以使用工具如 jconsole、jvisualvm、jstat 等来监控 JVM 资源使用情况。

2. 排查 GC 情况:GC 是 JVM 中一个重要的性能指标,可以通过监控 GC 次数和耗时来排查 GC 问题。可以使用工具如 jstat、jvisualvm、jmap、jmap、jcmd、G1 GC 日志等来排查 GC 问题。

3. 排查线程情况:线程是 JVM 中一个重要的性能指标,可以通过监控线程数量和线程状态来排查线程问题。可以使用工具如 jstack、jcmd 等来排查线程问题。

4. 排查内存情况:内存是 JVM 中一个重要的性能指标,可以通过监控内存使用情况和内存泄漏情况来排查内存问题。可以使用工具如 jmap、jcmd、jconsole、jvisualvm 等来排查内存问题。

5. 排查代码逻辑:最后,需要排查代码逻辑,确保代码逻辑正确、高效、优化,不会导致性能问题。可以使用工具如 jprofiler、YourKit 等来排查代码逻辑问题。

通过以上步骤,可以对 JVM 的性能问题进行排查,找出问题的根源,并采取相应的措施来解决问题。需要注意的是,JVM 性能调优是一个复杂的过程,需要综合考虑多个因素,需要有一定的经验和技能。

其他

什么是 Java 的反射?如何使用反射?

Java的反射是指在运行时动态地获取类的信息、调用类的方法、操作类的属性等。通过反射,可以在运行时动态地创建对象、调用对象的方法、操作对象的属性等,从而实现对类的动态操作。

使用反射可以通过以下几个步骤来实现:

1. 获取类的信息:可以通过Class类的静态方法forName()来获取类的Class对象,然后通过Class对象的各种方法来获取类的信息,如类的名称、类的修饰符、类的父类、类的接口、类的构造方法、类的字段、类的方法等。

2. 创建对象:可以通过Class类的newInstance()方法来创建类的对象,然后通过反射调用对象的方法、操作对象的属性等。

3. 调用方法:可以通过Method类的invoke()方法来调用类的方法,可以通过Constructor类的newInstance()方法来调用类的构造方法。

4. 操作属性:可以通过Field类的get()和set()方法来操作类的属性,可以通过Field类的setAccessible()方法来设置属性的可访问性。

5. 动态代理:可以通过Proxy类的newProxyInstance()方法来创建动态代理对象,然后通过反射调用代理对象的方法,从而实现对方法的动态代理。

以上是使用反射的基本步骤,可以根据实际情况选择合适的方法进行反射。使用反射可以实现对类的动态操作,从而实现对类的灵活调用。


什么是 Java 的动态代理?如何使用动态代理?

Java的动态代理是指在运行时动态地创建代理对象,然后通过代理对象来调用被代理对象的方法,从而实现对方法的动态代理。

使用动态代理可以通过以下几个步骤来实现:

1. 创建接口:首先,需要创建一个接口,用于定义需要被代理的方法。

2. 创建被代理类:然后,需要创建一个被代理类,实现接口中的方法。

3. 创建代理类:然后,需要创建一个代理类,实现InvocationHandler接口,然后在invoke()方法中调用被代理类的方法。

4. 创建代理对象:然后,通过Proxy类的newProxyInstance()方法来创建代理对象,然后将代理对象转换为接口类型,从而实现对方法的动态代理。

5. 调用被代理类的方法:最后,通过代理对象来调用被代理类的方法,从而实现对方法的动态代理。

以上是使用动态代理的基本步骤,可以根据实际情况选择合适的方法进行动态代理。使用动态代理可以实现对方法的动态代理,从而实现对方法的灵活调用。

什么是CSA

CAS(Compare And Swap)是一种并发算法,用于实现多线程环境下的原子操作。它通过比较内存位置的值与预期值,只有在相等的情况下才会将新值写入内存位置。CAS 是一种乐观锁,它通过自旋重试的方式来解决并发冲突。

CAS 操作的基本原理如下:

  1. 读取内存位置的当前值。
  2. 比较内存位置的当前值与预期值。
  3. 如果相等,则将新值写入内存位置。
  4. 如果不相等,则重新读取内存位置的当前值并重复步骤 2。

在 Java 中,CAS(Compare And Swap)操作主要通过 java.util.concurrent.atomic 包中的原子类来实现。这些原子类提供了一系列的方法,可以实现针对基本数据类型(如 intlong)和引用类型(如 Object)的原子操作。

常见的原子类包括:

  1. AtomicInteger:提供了对 int 类型的原子操作。
  2. AtomicLong:提供了对 long 类型的原子操作。
  3. AtomicReference:提供了对引用类型的原子操作。
  4. AtomicBoolean:提供了对 boolean 类型的原子操作。
  5. AtomicIntegerArray:提供了对 int 类型数组的原子操作。
  6. AtomicLongArray:提供了对 long 类型数组的原子操作。
  7. AtomicReferenceArray:提供了对引用类型数组的原子操作。

这些原子类使用了 CAS 操作来实现原子性的读取、写入和更新操作。例如,AtomicInteger 类中的 compareAndSet 方法就是 CAS 操作的一个典型应用,它可以原子地比较并设置一个新值。

以下是一个简单的示例,演示了如何使用 AtomicInteger 来实现原子性的增加操作:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) {
        // 原子性增加操作
        int newValue = counter.incrementAndGet();
        System.out.println("New value: " + newValue);
    }
}

在多线程环境中,使用原子类可以避免使用锁来保护共享资源,提高了并发性能。

需要注意的是,CAS 操作的成功与失败都是非阻塞的,并且 CAS 操作是一个原子操作,不会被中断。因此,使用 CAS 操作时,需要注意处理 CAS 操作失败的情况。通常,可以通过自旋重试的方式来处理 CAS 操作失败的情况。

阻塞I/O模型(Blocking I/O Model)

阻塞I/O模型(Blocking I/O Model)是一种简单的I/O模型,也是最常见的I/O模型之一。在阻塞I/O模型中,当应用程序发送或接收数据时,程序会被阻塞,直到数据被发送或接收完成。

典型的阻塞I/O模型的例子为data = socket.read(),如果内核数据没有就绪,Socket线程就会一直阻塞在read()中等待内核数据就绪

阻塞I/O模型的工作流程如下:

  1. 应用程序发送数据:应用程序通过调用系统调用发送数据,然后将数据放入发送缓冲区。当数据被放入发送缓冲区后,应用程序将被阻塞,直到数据被发送完成。

  2. 操作系统发送数据:操作系统将数据从发送缓冲区复制到网络协议栈的发送缓冲区。当数据被复制到发送缓冲区后,操作系统将通过网卡发送数据。

  3. 网卡发送数据:网卡将数据从发送缓冲区发送到网络上。

  4. 网络接收数据:接收端的网卡将数据从网络上接收到接收缓冲区。

  5. 操作系统接收数据:操作系统将数据从接收缓冲区复制到网络协议栈的接收缓冲区。

  6. 应用程序接收数据:应用程序通过调用系统调用接收数据,然后将数据从接收缓冲区读取出来。当数据被读取出来后,应用程序将被阻塞,直到数据被接收完成。

阻塞I/O模型的优点是简单易用,适用于大多数应用场景。但是,阻塞I/O模型的缺点是效率较低,因为当应用程序被阻塞时,CPU无法进行其他任务。此外,阻塞I/O模型还容易发生死锁,因为当发送方和接收方都被阻塞时,数据无法在两端传输。

阻塞I/O模型适用于需要低延迟和高吞吐量的应用场景,例如视频流传输、音频流传输等。在这些应用场景中,延迟较低的传输方式可以提高用户体验。

非阻塞I/O模型(Non-blocking I/O Model)

非阻塞I/O模型(Non-blocking I/O Model)是一种相对于阻塞I/O模型的进阶模型,它的核心思想是在等待数据时不会阻塞整个线程,而是通过不断地轮询来检查数据是否已经准备好,从而提高了多任务处理的效率。

在非阻塞I/O模型中,当应用程序发送或接收数据时,程序不会被阻塞,而是可以继续执行其他任务。当数据准备好时,应用程序会收到一个通知,然后可以调用系统调用来发送或接收数据。

非阻塞I/O模型的工作流程如下:

  1. 应用程序发送数据:应用程序通过调用系统调用发送数据,并设置为非阻塞模式。当数据被放入发送缓冲区后,应用程序不会被阻塞,而是可以继续执行其他任务。

  2. 操作系统发送数据:操作系统将数据从发送缓冲区复制到网络协议栈的发送缓冲区。当数据被复制到发送缓冲区后,操作系统将通过网卡发送数据。

  3. 网卡发送数据:网卡将数据从发送缓冲区发送到网络上。

  4. 网络接收数据:接收端的网卡将数据从网络上接收到接收缓冲区。

  5. 操作系统接收数据:操作系统将数据从接收缓冲区复制到网络协议栈的接收缓冲区。

  6. 应用程序接收数据:应用程序通过调用系统调用接收数据,并设置为非阻塞模式。当数据被读取出来后,应用程序不会被阻塞,而是可以继续执行其他任务。

非阻塞I/O模型的优点是提高了多任务处理的效率,可以同时处理多个连接。但是,非阻塞I/O模型的缺点是实现较为复杂,需要使用轮询来检查数据是否已经准备好,从而增加了CPU的开销。此外,非阻塞I/O模型也容易发生死锁,因为当发送方和接收方都被阻塞时,数据无法在两端传输。

非阻塞I/O模型适用于需要处理大量并发连接的应用场景

多路复用I/O模型(Multiplexing I/O Model)

多路复用I/O模型(Multiplexing I/O Model)是一种高效的I/O模型,它的核心思想是通过一个线程同时处理多个连接,从而提高了I/O操作的效率。

在多路复用I/O模型中,有一个专门的线程负责监听所有的连接,并将准备好的连接放入一个队列中。然后,应用程序可以通过调用系统调用来获取队列中的连接,并进行数据的发送和接收。

多路复用I/O模型的工作流程如下:

  1. 应用程序注册连接:应用程序通过调用系统调用将连接注册到监听器中。

  2. 监听器监听连接:监听器不断地轮询所有的连接,检查是否有数据准备好。当有数据准备好时,监听器将连接放入一个队列中。

  3. 应用程序获取连接:应用程序通过调用系统调用获取队列中的连接,并进行数据的发送和接收。

  4. 操作系统发送数据:操作系统将数据从发送缓冲区复制到网络协议栈的发送缓冲区。当数据被复制到发送缓冲区后,操作系统将通过网卡发送数据。

  5. 网卡发送数据:网卡将数据从发送缓冲区发送到网络上。

  6. 网络接收数据:接收端的网卡将数据从网络上接收到接收缓冲区。

  7. 操作系统接收数据:操作系统将数据从接收缓冲区复制到网络协议栈的接收缓冲区。

  8. 应用程序接收数据:应用程序通过调用系统调用接收数据,并进行数据的处理。

多路复用I/O模型的优点是提高了I/O操作的效率,可以同时处理多个连接。但是,多路复用I/O模型的缺点是实现较为复杂,需要使用轮询来检查数据是否已经准备好,从而增加了CPU的开销。此外,多路复用I/O模型也容易发生死锁,因为当发送方和接收方都被阻塞时,数据无法在两端传输。

多路复用I/O模型适用于需要处理大量并发连接的应用场景

信号驱动I/O模型(Signal-driven I/O Model)

信号驱动I/O模型(Signal-driven I/O Model)是一种基于信号通知的I/O模型,它允许应用程序在数据准备就绪时得到通知,而不必阻塞等待数据的到达。

在信号驱动I/O模型中,应用程序首先向操作系统注册一个信号处理函数,然后将文件描述符与该信号处理函数进行关联。当文件描述符上有数据准备就绪时,操作系统会发送一个信号给应用程序,触发相应的信号处理函数执行,应用程序就可以在信号处理函数中执行I/O操作,而不必阻塞等待数据的到达。

信号驱动I/O模型的工作流程如下:

  1. 应用程序注册信号处理函数:应用程序通过调用系统调用将信号处理函数注册到操作系统中,并将文件描述符与该信号处理函数进行关联。

  2. 等待数据就绪:应用程序调用系统调用等待数据就绪。此时,应用程序不会被阻塞,而是可以继续执行其他任务。

  3. 数据就绪时发送信号:当文件描述符上有数据准备就绪时,操作系统会发送一个信号给应用程序,触发相应的信号处理函数执行。

  4. 信号处理函数执行I/O操作:信号处理函数被触发后,应用程序可以在信号处理函数中执行I/O操作,例如读取数据或写入数据。

  5. 完成I/O操作:当I/O操作完成后,应用程序可以继续执行其他任务,或者等待下一个信号。

信号驱动I/O模型的优点是能够实现非阻塞式的I/O操作,从而提高了应用程序的响应性能。但是,信号驱动I/O模型的缺点是实现相对复杂,需要注册信号处理函数和进行信号处理函数的关联,容易引入一些难以调试的问题。

信号驱动I/O模型适用于需要处理大量并发连接的应用场景

异步I/O模型(Asynchronous I/O Model)

异步I/O模型(Asynchronous I/O Model)是一种高效的I/O模型,它的核心思想是在等待数据时不会阻塞整个线程,而是在数据准备好时通过回调函数来通知应用程序。

在异步I/O模型中,应用程序首先向操作系统注册一个回调函数,然后调用系统调用等待数据就绪。当数据准备好时,操作系统会调用注册的回调函数,通知应用程序数据已经准备好,应用程序就可以在回调函数中执行I/O操作,而不必阻塞等待数据的到达。

异步I/O模型的工作流程如下:

  1. 应用程序注册回调函数:应用程序通过调用系统调用将回调函数注册到操作系统中。

  2. 等待数据就绪:应用程序调用系统调用等待数据就绪。此时,应用程序不会被阻塞,而是可以继续执行其他任务。

  3. 数据就绪时调用回调函数:当数据准备好时,操作系统会调用注册的回调函数,通知应用程序数据已经准备好。

  4. 回调函数执行I/O操作:回调函数被调用后,应用程序可以在回调函数中执行I/O操作,例如读取数据或写入数据。

  5. 完成I/O操作:当I/O操作完成后,应用程序可以继续执行其他任务,或者等待下一个数据就绪。

异步I/O模型的优点是提高了I/O操作的效率,可以同时处理多个连接。但是,异步I/O模型的缺点是实现较为复杂,需要使用回调函数来处理数据,容易引入一些难以调试的问题。

Java I/O(Input/Output)

Java I/O(Input/Output)是Java语言中用于输入和输出的标准库,提供了丰富的类和方法用于读取和写入数据。Java I/O包括文件I/O、网络I/O、对象I/O、字符I/O等多种类型。

Java I/O的主要特点包括:

  1. 面向流的I/O模型: Java I/O采用了面向流的I/O模型,将数据视为一系列连续的字节或字符流。Java I/O提供了InputStream和OutputStream、Reader和Writer等类用于读取和写入字节和字符流。
  2. 字节流和字符流: Java I/O提供了字节流和字符流两种类型的I/O类。字节流用于读取和写入字节流,例如文件I/O、网络I/O等;字符流用于读取和写入字符流,例如文本文件I/O等。Java I/O提供了InputStream和OutputStream、Reader和Writer等类用于读取和写入字节和字符流。
  3. 缓冲流: Java I/O提供了缓冲流用于提高I/O操作的效率。缓冲流可以将数据缓存到内存中,减少了磁盘或网络I/O的次数,从而提高了I/O操作的效率。Java I/O提供了BufferedInputStream和BufferedOutputStream、BufferedReader和BufferedWriter等类用于缓存数据。
  4. 对象I/O: Java I/O提供了对象I/O,可以将对象序列化成字节流或字符流,然后再反序列化成原始对象。Java I/O提供了ObjectInputStream和ObjectOutputStream等类用于对象的序列化和反序列化。
  5. 文件I/O: Java I/O提供了文件I/O,可以读取和写入文件。Java I/O提供了FileInputStream和FileOutputStream、FileReader和FileWriter等类用于文件的读取和写入。
  6. 网络I/O: Java I/O提供了网络I/O,可以进行网络通信。Java I/O提供了Socket和ServerSocket等类用于客户端和服务器端的网络通信。
  7. 标准I/O: Java I/O提供了标准I/O,可以进行控制台输入和输出。Java I/O提供了System.in和System.out等类用于标准输入和标准输出。
  8. NIO(New I/O): Java 1.4版本引入了NIO(New I/O)包,提供了更高效的I/O操作。NIO提供了通道Selector(选择器)、(Channel)和缓冲区(Buffer)等类用于高效的I/O操作。通过NIO,可以实现非阻塞I/O、多路复用I/O等高级的I/O模型。

Java NIO

  1. Selector(选择器): 选择器是Java NIO提供的一种多路复用的机制,用于同时监视多个通道的I/O事件。通过选择器,可以将多个通道注册到一个选择器中,并在通道准备好I/O事件时进行通知。选择器可以监视通道的读就绪、写就绪、连接就绪和接收就绪等事件。
  2. Channel(通道): 通道是Java NIO提供的一种双向的数据传输通道,可以进行读取和写入。通道可以分为文件通道、网络通道和管道等类型。通道是非阻塞的,当应用程序调用读取或写入数据的系统调用时,如果数据没有准备好,应用程序不会被阻塞,可以继续执行其他任务。
  3. Buffer(缓冲区): 缓冲区是Java NIO提供的一种临时存储数据的内存区域,用于缓存数据。缓冲区可以分为直接缓冲区和非直接缓冲区两种类型。直接缓冲区使用操作系统的内存,可以加速I/O操作;非直接缓冲区使用Java堆内存,适合小数据量的I/O操作。

Java NIO的实现主要基于这三大核心内容,通过Selector、Channel和Buffer等类和方法,可以实现高效的I/O操作。通过Selector,可以同时监视多个通道的I/O事件;通过Channel,可以进行读取和写入数据;通过Buffer,可以缓存数据,提高I/O操作的效率。

Java NIO和传统I/O的区别

Java NIO(New I/O)和传统I/O(Input/Output)是Java语言中用于输入和输出的两种不同的I/O模型,它们之间有以下几点区别:

  1. I/O模型: 传统I/O模型是面向流的I/O模型,将数据视为一系列连续的字节或字符流;而Java NIO模型是面向缓冲区的I/O模型,将数据缓存到内存中,然后再进行读取和写入。

  2. 阻塞与非阻塞: 传统I/O模型是阻塞式的I/O模型,当应用程序调用读取或写入数据的系统调用时,如果数据没有准备好,应用程序会被阻塞,直到数据准备好;而Java NIO模型是非阻塞式的I/O模型,当应用程序调用读取或写入数据的系统调用时,如果数据没有准备好,应用程序不会被阻塞,可以继续执行其他任务。

  3. 通道与流: 传统I/O模型使用流(Stream)来进行输入和输出;而Java NIO模型使用通道(Channel)和缓冲区(Buffer)来进行输入和输出。

  4. 选择器: Java NIO模型提供了选择器(Selector)用于多路复用I/O操作,可以同时处理多个连接;而传统I/O模型没有提供选择器。

  5. 性能: Java NIO模型的性能比传统I/O模型更高,因为Java NIO模型将数据缓存到内存中,减少了磁盘或网络I/O的次数,从而提高了I/O操作的效率。

总的来说,Java NIO模型是一种高效的I/O模型,适用于需要处理大量并发连接的应用场景;而传统I/O模型是一种简单易用的I/O模型,适用于一般的I/O操作。开发人员可以根据具体的应用场景选择合适的I/O模型。

Java NIO使用

下面是一个简单的Java NIO服务器实现示例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 创建选择器
        Selector selector = Selector.open();
        // 创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定端口
        serverSocketChannel.bind(new InetSocketAddress(8888));
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 将ServerSocketChannel注册到选择器上,监听连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 循环等待客户端连接
        while (true) {
            // 选择器阻塞,等待就绪事件
            selector.select();
            // 获取就绪事件的SelectionKey集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍历就绪事件的SelectionKey集合
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                // 获取SelectionKey
                SelectionKey selectionKey = iterator.next();
                // 如果是连接事件
                if (selectionKey.isAcceptable()) {
                    // 获取ServerSocketChannel
                    ServerSocketChannel serverChannel = (ServerSocketChannel) selectionKey.channel();
                    // 接受连接
                    SocketChannel socketChannel = serverChannel.accept();
                    // 设置为非阻塞模式
                    socketChannel.configureBlocking(false);
                    // 将SocketChannel注册到选择器上,监听读事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                // 如果是读事件
                else if (selectionKey.isReadable()) {
                    // 获取SocketChannel
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    // 读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer);
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    System.out.println(new String(bytes));
                }
                // 移除SelectionKey
                iterator.remove();
            }
        }
    }
}

在这个示例中,我们首先创建了一个Selector对象,然后创建了一个ServerSocketChannel对象,并将其绑定到指定的端口。接着,我们将ServerSocketChannel注册到Selector上,并监听连接事件(OP_ACCEPT)。在循环中,我们使用Selector的select()方法来等待就绪事件,然后遍历就绪事件的SelectionKey集合,处理连接事件和读事件。

具体的Client实现代码如下:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

public class NIOClient {
    public static void main(String[] args) throws IOException {
        // 创建选择器
        Selector selector = Selector.open();
        // 创建SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        // 连接服务器
        socketChannel.connect(new InetSocketAddress("localhost", 8888));
        // 设置为非阻塞模式
        socketChannel.configureBlocking(false);
        // 将SocketChannel注册到选择器上,监听读事件
        socketChannel.register(selector, SelectionKey.OP_READ);
        // 创建一个线程用于发送消息
        new Thread(() -> {
            try {
                // 创建一个Scanner对象用于输入消息
                Scanner scanner = new Scanner(System.in);
                while (true) {
                    // 读取输入的消息
                    String msg = scanner.nextLine();
                    // 将消息写入到SocketChannel中
                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                    socketChannel.write(buffer);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
        // 循环等待服务器响应
        while (true) {
            // 选择器阻塞,等待就绪事件
            selector.select();
            // 获取就绪事件的SelectionKey集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍历就绪事件的SelectionKey集合
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                // 获取SelectionKey
                SelectionKey selectionKey = iterator.next();
                // 如果是读事件
                if (selectionKey.isReadable()) {
                    // 获取SocketChannel
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    // 读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    channel.read(buffer);
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    System.out.println(new String(bytes));
                }
                // 移除SelectionKey
                iterator.remove();
            }
        }
    }
}

在这个示例中,我们首先创建了一个Selector对象,然后创建了一个SocketChannel对象,并连接到指定的服务器。接着,我们将SocketChannel注册到Selector上,并监听读事件(OP_READ)。在循环中,我们使用Selector的select()方法来等待就绪事件,然后遍历就绪事件的SelectionKey集合,处理读事件。同时,我们创建了一个线程用于发送消息,通过Scanner对象从控制台读取输入的消息,并将消息写入到SocketChannel中。

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值