4.面试题--JVM虚拟机

1 JVM

Java内存区域

在这里插入图片描述

JVM 的主要组成部分

JVM包含两个子系统和两个组件。
两个子系统为类加载子系统、执行引擎;
两个组件为运行时数据区域、本地接口。

类加载子系统:负责加载类的.class文件,将类的字节码数据加载到内存中,并生成对应的Class对象。包括加载、连接(验证、准备、解析)和初始化阶段。

执行引擎: 执行类加载后的字节码指令,完成程序的实际运行。

  1. 解释器:将字节码文件逐行解释为机器码执行。
  2. 即时编译器(JIT编译器):将频繁执行的字节码编译成本地机器码,以提高执行效率。生成的机器码可以被缓存以供后续调用。

本地接口:提供了与操作系统及其他本地库交互的接口,允许Java代码调用本地方法。

运行时数据区域:JVM在操作系统内存中分配的内存区域,用于存放运行时的数据结构。
包括方法区、堆、虚拟机栈、本地方法栈、程序计数器等。

方法区、堆是线程共享
虚拟机栈、本地方法栈、程序计数器是线程私有的

  1. 方法区
    方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
    方法区很少发生垃圾回收,但是并不代表不发生 GC,在这里进行的 GC 主要是对方法区里的常量池和对类型的卸载。
    方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。


  2. ①堆是Java虚拟机管理的最大的内存区域,用于存储由 new 关键字创建的对象实例和数组对象。这个内存区域是所有线程共享的。
    垃圾回收器负责在堆内存中进行垃圾回收操作,主要目的是回收不再被任何引用指向的对象,从而释放内存空间。
    ②堆内存通常被划分为新生代(Young Generation)和老年代(Old Generation)两部分。
    新生代又分为Eden区和两个Survivor区(通常称为S0和S1或From和To区)。大多数对象的生命周期从Eden区开始。
    ③可以通过Java虚拟机的启动参数来调整堆内存的大小,如 -Xms(堆的初始大小)和 -Xmx(堆的最大大小)。
    ④对象何时被垃圾回收器回收取决于对象是否有引用指向,以及垃圾回收器的算法和策略。程序员无法直接控制对象的释放时机,但可以通过将对象的引用设置为null来帮助垃圾回收器更快地识别出不再被使用的对象,从而加速内存回收过程。

  3. 虚拟机栈
    ①虚拟机栈就是常说的栈内存,它为 java 方法服务,每个方法被执行时,会创建一个栈帧,栈帧用于存储局部变量、操作数栈、动态链接、方法出口等信息。它的生命周期与线程相同。
    ②局部变量表里存储的是基本数据类型、指向一条字节码指令的地址和对象引用。局部变量所需的内存空间在编译器间确定。
    ③操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索
    引来访问,而是压栈和出栈的方式。
    ④每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了
    支持方法调用过程中的动态连接,动态链接就是将常量池中的符号引用在运行期转化为直接
    引用。

  4. 本地方法栈
    本地方法栈与虚拟机栈类似,但是它为本地方法(Native方法)服务。

  5. 程序计数器
    程序计数器是当前线程所执行的字节码的行号指示器,确保线程可以正确地执行代码。
    程序计数器在线程切换时,会被保存和恢复,以确保线程切换后能够正确地继续执行。

方法区与永久代、 元空间之间的关系

①方法区是 JVM规范中定义的一块内存区域,用于存储已被虚拟机加载的类的相关信息,如类型信息、常量、静态变量、即时编译器编译后的代码等。

②永久代是jdk1.8之前,虚拟机中方法区的具体实现,用于存放被虚拟机加载的类的元数据信息。永久代有一个固定的大小上限,垃圾回收效果并不理想,大部分的数据会一直存在直到程序停止运行。当程序中有大量动态生成类时,这些类信息都要存储到永久代,很容易造成方法区溢出。

③元空间是jdk1.8及之后,虚拟机中方法区的具体实现 ,并不在虚拟机内存中,而是使用本地内存来存储类的元数据。元空间的大小仅受本地内存限制,可以通过参数来指定元空间的大小,具有更好的伸缩性和性能。

堆栈的区别

物理地址分配

堆上的内存分配是不连续的。

栈上的内存分配是连续的,遵循后进先出的原则。每个线程都有自己的栈,栈上的每个栈帧对应于一个方法调用。栈帧中包含了局部变量、操作数栈等信息。

内存大小和分配时机

堆的大小在运行时确定,可以动态地增加或减少。堆的大小通常远大于栈。

栈的大小在编译时确定,通常每个线程有一个固定大小的栈空间。

存放内容

堆主要用于存放对象的实例和数组。

栈主要用于存放局部变量、方法调用信息和操作数栈。

程序的可见度

堆对于多个线程来说都可以访问和操作堆上的对象。

栈对于每个线程是私有的,每个线程都有自己的栈空间,互不干扰。

生命周期

堆上的对象生命周期由垃圾回收器决定,当对象不再被引用时,垃圾回收器会回收其占用的内存。

栈上的变量和方法调用随着方法的执行和返回而创建和销毁。

栈和队列

栈(Stack)
栈是一种基于先进后出(LIFO)原则的线性数据结构,即最先进栈的元素,最后出栈,只允许在栈顶进行进行插入和删除操作。
栈内存主要用于存储基本数据类型的变量和对象的引用变量。
栈内存的生命周期与所属线程相同,当线程结束时,栈内存也会被释放。每个线程都有独立的栈内存。
栈的大小在程序启动时确定,且通常比堆小得多。栈上的内存分配和释放是自动的,由编译器和运行时环境管理。

队列(Queue)

队列是一种基于先进先出(FIFO)的线性数据结构,即最先进队列的元素最先出队列。
队列的实现可以是基于数组或链表等数据结构。
只允许队尾入队(插入操作),队头出队(删除操作)。队列中没有元素时,称为空队列。
队列的大小通常可以动态增长,取决于具体的实现。
队列广泛应用于需要按顺序处理元素的场景,如任务调度、消息传递、广度优先搜索等

堆溢出的原因和危害

堆溢出指的是在程序中使用堆空间进行内存分配时,当堆空间不够用时,会导致写入内存中的数据超出堆空间所分配的内存范围,从而造成数据损坏、程序崩溃等问题。

堆溢出的原因

内存泄漏:如果没有及时释放已经分配的内存,会导致内存占用越来越多,最终导致堆溢出。

缓冲区溢出:当向堆缓冲区内写入的数据超过缓冲区的大小时,也会导致堆溢出。

不正确的内存处理:在程序中错误使用指针或动态内存分配函数,例如使用已经被释放的指针或尝试释放非动态内存等,也会导致堆溢出。

堆溢出的危害

程序崩溃:当程序发生堆溢出时,会导致程序崩溃,从而影响系统的稳定性和可靠性。

数据损坏:堆溢出会导致内存中的数据被破坏,从而导致数据错误或损坏,影响程序的正确运行。

安全漏洞:当堆溢出时,攻击者可以改变程序的控制流,将程序控制权转移到恶意代码上,导致系统安全漏洞,例如缓冲区溢出漏洞和堆溢出漏洞。跨站脚本攻击(XSS)就是一种堆溢出漏洞造成的安全漏洞类型。

如何防止栈和堆溢出

1.减少嵌套调用深度:尽量减少函数的嵌套调用深度,避免无限递归和函数调用过程中占用过多的栈空间。

2.减少局部变量的使用:可以尝试使用静态变量或全局变量来避免函数调用过深,减少局部变量的使用可以减小栈空间的消耗。

3.缓冲区检查:使用具有缓冲区检查功能的编程语言,例如 Java 或 C#,可以在编译过程中检查缓冲区溢出问题。

4.使用内存池:使用内存池可以减少动态内存分配和销毁的开销,提高程序效率。

5.立即释放内存:尽可能的及时释放已经分配的内存,避免内存泄漏,特别是在程序用完一个指针后,要立即释放它所指向的内存空间。

6.限制缓存区大小:在分配缓存区时,应该限制缓存区大小,避免产生缓冲区溢出。

7.安全编程实践:程序员应该注意编写安全的程序,避免调用不安全的函数或不安全的指针操作,应该注意编写完整、健壮、可靠的代码。

克隆

克隆就是根据已有的数据,拷贝出一份完全一样的数据。

实现克隆的方式:

1.手动复制:可以手工的new出一个新的对象,然后将原来的对象信息一个一个的set到新的对象中。

2.实现Cloneable接口并重写clone()方法,用于创建对象的浅克隆 。

3.使用序列化:实现Serializable接口,通过对象的序列化和反序列化实现克隆,实现真正的深克隆

在Java中对象的克隆有深拷贝和浅拷贝之分。有这种区分的原因是Java中分为基本数据类型和引用数据类型,对于不同的数据类型在内存中的存储的区域是不同的。

基本数据类型存储在栈中,引用数据类型存储在堆中。

基本数据类型:int, double, char等。
引用数据类型:类、接口、数组等。

浅克隆和深克隆的区别

浅克隆
浅克隆会创建一个新对象,并复制原始对象中的所有字段到新对象中。
对于基本数据类型,直接复制其值。
对于引用类型,只复制引用本身,而不是引用的对象。这意味着新对象和原始对象中的引用类型字段都指向内存中的同一个对象。
如果通过新对象修改了引用类型字段所指向的对象的内部状态,原始对象中的对应字段也会受到影响.

深克隆
深克隆会创建一个新对象,并递归地复制原始对象本身及其所有引用类型的成员变量所指向的对象.
对于基本数据类型,直接复制其值。
对于引用类型,会创建一个新的对象实例,并将原始对象中的引用类型字段的内容复制到新创建的对象实例中。这意味着新对象和原始对象中的引用类型字段虽然内容相同,但实际上是两个独立的对象实例。

浅克隆

/**
 * @Author cyc
 * @Description 地址类
 * @Date 2024/7/3 10:07
 * @Version 1.0
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address {
    //地址
    private String addressName;
}


/**
 * @Author cyc
 * @Description 人员类实现浅克隆
 * @Date 2024/7/3 10:07
 * @Version 1.0
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Cloneable {

    //姓名
    private String userName;

    private Address address;

    //浅克隆
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }


    public static void main(String[] args) throws CloneNotSupportedException {
        //新建人员类
        Person p1 = new Person();
        p1.setUserName("张三");
        p1.setAddress(new Address("成都市"));
        //浅克隆
        Person p2 =(Person) p1.clone();
        System.out.println("原p1对象中的地址="+p1.getAddress().getAddressName());
        System.out.println("原p2对象中的地址="+p2.getAddress().getAddressName());
        //修改p2中的address的addressName
        p2.getAddress().setAddressName("浙江市");
        System.out.println("修改后p1对象中的地址="+p1.getAddress().getAddressName());
        System.out.println("修改后p2对象中的地址="+p2.getAddress().getAddressName());
        
        //原p1对象中的地址=成都市
        //原p2对象中的地址=成都市
        //修改后p1对象中的地址=浙江市
        //修改后p2对象中的地址=浙江市
    }
}

深克隆

/**
 * @Author cyc
 * @Description 雇员类实现深克隆
 * @Date 2024/7/3 10:07
 * @Version 1.0
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Cloneable {
    //姓名
    private String userName;

    private Address address;


    @Override
    protected Object clone() throws CloneNotSupportedException {
        //调用super.clone()进行浅克隆
        Employee clonedEmployee = (Employee) super.clone();
        //对person属性进行深克隆
        clonedEmployee.address = new Address(this.address.getAddressName());
        return clonedEmployee;
    }


    public static void main(String[] args) throws CloneNotSupportedException {
        //创建一个e1对象
        Employee e1 = new Employee();
        e1.setUserName("张三");
        e1.setAddress(new Address("峨眉市"));
        //克隆e1
        Employee e2 = (Employee) e1.clone();
        System.out.println("原e1对象中的地址="+e1.getAddress().getAddressName());
        System.out.println("原e2对象中的地址="+e2.getAddress().getAddressName());
        //修改e2中的address的addressName
        e2.getAddress().setAddressName("上海市");
        System.out.println("修改后e1对象中的地址="+e1.getAddress().getAddressName());
        System.out.println("修改后e2对象中的地址="+e2.getAddress().getAddressName());

        //原e1对象中的地址=峨眉市
        //原e2对象中的地址=峨眉市
        //修改后e1对象中的地址=峨眉市
        //修改后e2对象中的地址=上海市
    }
}

2 虚拟机类加载

类加载机制

虚拟机把描述类的数据从.class文件加载到内存中,并对数据进行校验,解析和初始化,最
终形成可以被虚拟机直接使用的 java 类型。

类加载器

启动类加载器(Bootstrap ClassLoader)
负责加载Java核心库,这些类库通常位于JAVA_HOME/jre/lib目录下.
一般由 C++ 实现,并不继承自java.lang.ClassLoader。

扩展类加载器(Extension ClassLoader)
负责加载Java的扩展库,这些库位于JAVA_HOME/jre/lib/ext目录下.
它的父类加载器通常是启动类加载器。

应用程序类加载器(Application ClassLoader)
也称为系统类加载器,负责加载应用程序classpath路径下的类。
是大多数Java应用程序默认的类加载器,也是默认的线程上下文类加载器

自定义类加载器(Custom ClassLoader)
开发者可以根据需要自行实现的类加载器,继承自 java.lang.ClassLoader 类。

这些类加载器组成了Java虚拟机的类加载体系结构,负责在运行时加载Java类文件到内存中。每个类加载器负责加载不同位置的类文件,通过双亲委派机制来保证类的唯一性和安全性。

自己编写一个假冒的java.lang.System类加载器能加载到吗

①假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正java.lang.System,自然不会加载假冒的java.lang.System类。

②假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的java.lang.System时,它需要先加载父类java.lang.Object,而你没有用双亲委派,找不到java.lang.Object,所以加载失败 。

③以上也仅仅是假设,实际操作你就会发现自定义类加载器加载以java.打头的类时,会抛安全异常,在jdk9以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了。

类加载过程

类加载过程:加载——>链接——>初始化——>使用——>卸载,链接部分又可以分为三步:验证——>准备——>解析

加载

①通过一个类的全限定名获取该类的二进制
②将该二进制流中的静态存储结构转化为方法区运行时数据结构
③在内存中生成该类的 Class 对象,作为该类的数据访问入口。

注意:如果此类的父类没有加载, 先加载父类
加载是懒惰执行,代码中使用了这个类,才会加载这个类。

链接

①验证-确保类的字节码符合JVM 规范,防止恶意代码破坏JVM。
验证阶段主要分为四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
②准备- 为类的静态变量分配内存空间,并设置默认初始值,这些变量在方法区中分配内存。
③解析- 将类中的符号引用解析为直接引用
例如将类、方法、字段的引用转换为内存地址

初始化

①执行类构造器 () 方法,初始化类的静态变量和执行静态初始化块中的代码。
②初始化是懒惰执行的,在首次使用类时进行初始化

使用

卸载

类加载中的验证阶段主要分为四种验证

文件格式验证
验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

元数据验证
对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

字节码验证
是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。

符号引用验证
这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

双亲委派机制

**双亲委派机制 **是 Java 类加载机制中的一种重要设计,用于保证类的唯一性和避免冲突。

当一个类加载器需要加载一个类时,它首先不会自己尝试去加载这个类,而是将其委派给父类加载器,由父类加载器去加载当前类, 如果父类加载器加载成功(在其加载路径下找到这个类),则直接返回父类加载器加载的类。 只有当父类加载器无法加载该类时(即在委派链上找不到该类),子类加载器才会尝试自己加载该类。

双亲委派目的

①让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到jdk提供的核心类

②让类的加载有优先次序, 保证核心类优先加载

3 垃圾收集器

JVM哪些部分会出现内存溢出

出现内存溢出的情况

①堆内存耗尽 :对象越来越多,又一直在使用,不能被垃圾回收

②方法区内存耗尽:加载的类越来越多,很多框架都会在运行期间动态产生新的类

③虚拟机栈累积 :每个线程最多会占用1 M内存,线程个数越来越多,而又长时间运行不销毁时,出现 栈内存溢出异常。

什么是GC、GC目的

GC是自动内存管理的一个主要部分,它负责识别并释放不再使用的对象所占用的内存。

GC的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度 。

GC 具体的实现称为垃圾回收器 。

判断一个对象是否存活

引用计数法:为每个对象设置一个引用计数器,每当有一个地方引用这个对象时,计数器 +1,引用被释放时计数器 -1,当一个对象的计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题(当对象 A 引用对象 B,对象B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,无法完成垃圾回收)

可达性分析法:从 GC Roots对象开始向下搜索,搜寻走过的路径称为引用链,如果某个对象到GC Roots没有任何引用链相连,就该对象为不可达对象,垃圾回收器会将其标记为垃圾对象,并将在后续的垃圾收集过程中将其回收,释放其占用的内存空间。

哪些对象可以作为 GC Roots

虚拟机栈中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(Native方法)引用的对象。

三色标记方法

①黑色- 已标记 ②灰色-标记中 ③白色-还未标记

要找出存活对象,根据可达性分析,从GC Roots开始进行遍历访问,可达的则为存活对象。

黑色:表示该对象及其所有可达子对象都已经被访问过,并且已经确定是存活的对象。

灰色:表示该对象已经被访问过,但是其所有可达子对象还未完全访问。在遍历过程中,灰色对象的子对象逐步被访问,如果所有子对象都被访问完,则该对象会变成黑色。

白色:表示尚未被访问过的对象。如果整个可达性分析过程完成后,仍然有白色对象存在,这些对象就被认定为不可达对象,即可以被回收的垃圾对象。

垃圾回收器的工作流程

标记存活对象标记过程通常从GC Root(垃圾回收根)对象开始,垃圾回收器通过可达性分析标记出所有存活的对象。所有未被标记的对象即可被回收,这些对象被称为垃圾对象。

回收操作:回收过程可能包括清除、复制或整理垃圾对象。具体操作取决于垃圾回收算法的实现,如标记-清除、复制算法、标记-整理算法等。

对象移动和引用更新:在某些垃圾回收算法中,如复制算法和标记-整理算法,存活对象可能会被移动。这时,所有引用这些对象的地方都需要更新,以确保引用指向新的内存地址,避免出现悬空引用。

JVM垃圾回收算法

①标记清理算法

标记阶段从根对象开始遍历,找出所有被引用的对象,标记成存活对象。
清理阶段垃圾回收器会清理未被标记的对象所占用的内存。

缺点:清除阶段会产生大量不连续的内存碎片,虽然释放了很多内存,但是在后面需要分配连续的大对象时无法找到足够大的连续内存而导致不得不提前触发另一次垃圾收集动作。

②标记整理算法
标记阶段从根对象开始遍历,找出所有被引用的对象,标记成存活对象。
整理阶段会把存活的对象移动到内存空间的一端,而未标记的对象(即垃圾对象)则会被清理掉。
回收阶段回收并释放未标记的垃圾对象所占用的内存空间。
解决了内存碎片的问题,是Java虚拟机中常用的垃圾回收策略之一。

③标记复制算法

首先把内存分为两部分,A部分内存存放对象,B部分空闲。
标记阶段从根对象开始遍历,找出所有被引用的对象,标记成存活对象。
复制阶段把A部分内存中存活对象复制到B部分空闲内存中。在复制过程中,可能需要更新某些对象的引用,以确保它们指向正确的内存地址。
清除阶段:完成复制后,清理A部分内存中的所有对象,释放A部分内存空间。

不用考虑内存碎片,可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

④分代回收算法

根据对象的存活周期将内存划分为几块。一般包括新生代、老年代 和 永久代。
在这里插入图片描述
新生代主要用于存放新创建的对象。新生代又可以进一步划分为三个区域:伊甸园(Eden Space)、幸存区0(Survivor From,通常称为S0区)和幸存区1(Survivor To,通常称为S1区)。

伊甸园
对象主要在伊甸园中被创建。

幸存区
当伊甸园内存不足,会触发一次轻量级的垃圾回收,存活的对象会被复制到幸存区中的一个,下一次YGC时,采用标记复制算法,S0区中的存活对象会复制到S1区,同时清空S0区。S0区和S1区的角色会交换。

老年代
对象在多次YGC后仍然存活,它们会被晋升到老年代。
新创建的对象太大以至于无法放入新生代时,这些对象会被晋升到老年代。
老年代发生垃圾回收时,通常采用的是标记-清除或标记-整理算法

Minor GC 和 Major GC

Minor GC(新生代GC)
Minor GC 主要负责清理新生代(包括 Eden 区和两个 Survivor 区)。当 Eden 区没有足够空间来分配新对象时,就会触发 Minor GC。在 Minor GC 过程中,存活的对象会被移动到其中一个 Survivor 区,或者直接晋升到老年代。Minor GC 比较频繁,一般不会停顿整个应用程序的运行。

Major GC(老年代GC,也称为Full GC)
Major GC 主要针对老年代进行回收。它通常会触发整个堆的垃圾回收,包括新生代和老年代。在执行 Major GC 时,会暂停整个应用程序的运行,直到完成垃圾回收为止。Major GC 的频率通常较低,因为老年代的对象通常生命周期较长,不会频繁触发垃圾回收。

Minor GC 与 Major GC 的关系
一般情况下,当触发 Full GC 时,不会自动触发 Minor GC。但是可以通过配置,在执行 Full GC 之前先执行一次 Minor GC,以加速老年代的回收过程。这种配置可以在一些特定场景下优化垃圾回收性能。

System.gc() 和 Runtime.gc()

这两个方法用来提示 JVM 要进行垃圾回收。但是,立即开始还是延迟进行垃圾回收是取决于 JVM 的。

finalize() 方法何时被调用及目的

finalize() 方法是一个在对象被垃圾回收器回收之前调用的方法。在对象被垃圾回收器确定为不再可达时,会调用该对象的 finalize() 方法来进行一些清理工作或资源释放操作。

目的:允许对象在被垃圾回收之前执行一些清理工作,例如关闭文件、释放网络连接、释放本地资源等。它通常用于确保非内存资源的释放,因为 Java 的垃圾回收器一般只负责管理内存,无法直接释放其他资源。

如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存

不会,在下一个垃圾回收周期中,这个对象将是可被回收的。

项目中出现内存溢出怎么解决

①误用线程池导致的内存溢出

解决方法:
构造线程池的时候,使用有大小限制的任务队列
监控线程池的状态,当队列长度接近限制时,应采取措施减缓任务提交速度或增加线程池大小。
设置拒绝策略,当队列满且线程池中的线程都在忙时,对新提交的任务执行拒绝策略,如丢弃任务、抛出异常或记录日志等。

②查询数据量太大导致的内存溢出

解决方法:sql语句查询加limit限制查询返回的记录数
使用分页查询,即LIMIT和OFFSET,限制每次查询返回的记录数。
只查询需要的字段,避免使用SELECT *。

③动态生成类过多导致的(元空间)内存溢出

解决方法:
对于生命周期较短的类,不应将其设置为静态变量,而是应该使用局部变量或随着其所属对象的销毁而自动回收。
监控元空间的使用情况,如果接近限制,应考虑调整JVM参数,增加元空间大小。

4 JVM调优

JVM 调优工具

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

jconsole:用于对 JVM 中的内存、线程和类等进行监控;
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

常用的 JVM 调优的参数

-Xms2g:初始化堆大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值