JVM调优

JVM优化

课程内容-- jvm概述 结构 有问题 优化

  • jvm概述

  • Jvm组成(概要)

  • 对组成部分进行优化

  • 垃圾回收

  • jvm优化实战

一.JVM组成

1.JVM概述

1.1.什么是JVM

​ 与JVM的初次见面,是在我们Java SE课程的开始,讲解Java跨平台原理的时候.时隔多日,我们先来回顾一下.

Java的广告语是,”编写一次,到处运行”,而它凭借的就是JVM(Java Virtual Machine).而对于不同的平台Windows,Linux,Mac OS等,有具体不同的JVM版本.这些JVM屏蔽了平台的不同,提供了统一的运行环境,让Java代码无需考虑平台的差异,运行在不同的环境中.

而至于JRE和JDK,就不再赘述了,包含关系应该很清楚的,而今天我们的重点就在于对JVM的进一步认识以及对它进行优化调整.

jvm–>jre—>jdk

在这里插入图片描述

java跨平台原理??

​ java为各平台提供jvm,通过jvm屏蔽系统差异,进而我们可以编写统一代码. 到时候如果我们想要把字节码(java代理编译后得到的)运行到某平台,也就是要在对应的平台先安装对应jvm,再在jvm上面运行我们的java程序.

统一java配合各平台jvm,就可以让我们代码在各平台运行!!!

​ 简单说一下jvm?

​ jvm叫做java虚拟机,我们的java程序需要运行在jvm上.是实现跨平台关键. java为各平台提供jvm,通过jvm屏蔽系统差异,进而我们可以编写统一代码. 到时候如果我们想要把字节码(java代理编译后得到的)运行到某平台,也就是要在对应的平台先安装对应jvm,再在jvm上面运行我们的java程序.

你是怎么用spring? 用spring其实就是用spring两大核心,ioc,aop

1.2.为什么要优化JVM

​ 正如前面我们所回顾的,我们的Java代码都是运行在JVM中的,而部署的硬件及应用场景有所不同时,仍然采用默认的配置不见得能起到最好的效果,甚至可能会导致运行效率更差,又或者面临高并发情况下,想让程序平稳顺畅的运行,所以我们需要针对实际的需要来进行优化.

所谓优化就是配置一些jvm参数,让jvm运行时使用这些参数,让在jvm运行的程序更加良好的运行。

如果使用默认配置,资源得不到最有分配.

在这里插入图片描述

人---->房子(适合这个居住)

2.JVM分析的常用工具

jps : 查看java的进程 ps:查看进程

jconsole:java控制台,可以看到jvm的一些信息

jvisualvm: java可视化虚拟机

2.1.分析工具

我们只知道有JVM的存在,但它的运行对于我们来说感觉像是摸不着看不见的,所以我们需要借助工具来监控它的一个实时状态,就像Windows的性能监视器一样,JDK也有自己的可视化工具.Java提供了2个监视工具:

  • D:\opensource\jdk1.8\bin\jconsole.exe

  • D:\opensource\jdk1.8\bin\jvisualvm.exe

我们以管理员身份运行DOS ,输入jvisualvm,将Java VisualVM启动

在这里插入图片描述

本地列表中有多个条目,而一眼也可以看到我们SpringBoot项目的main方法,直接双击,经过短时间的加载后,得到这样一个界面

在这里插入图片描述

2.2.jvisualvm安装GC插件

自带的jvisualvm没有监视GC垃圾回收功能,我们需要额外安装插件:

打开工具 -> 插件 -> 选择“可用插件”页 : 我们在这里安装一个Visual GC,方便我们看到内存回收以及各个分代的情况 . 打上勾之后点击安装,就是常规的next以及同意协议等 ,网络不是很稳定,有时候可能需要多尝试几次。可以在设置中修改插件中心地址:

在这里插入图片描述

根据如下步骤修改地址:找到插件中心

http://visualvm.github.io/pluginscenters.html

在这里插入图片描述

找到对应的JDK版本:

http://visualvm.github.io/pluginscenters.html

复制插件地址:

在这里插入图片描述

安装插件:

在这里插入图片描述

然后再 可用插件中 找到 Visual GC

安装完成后我们将当前监控页关掉,再次打开,就可以看到Profiler后面多了一个Visual GC页。

在这里插入图片描述

在这里我们可以看到JIT活动时间,类加载活动时间,GC活动时间以及各个分代的情况。

需要注意的是,当前课件使用的JDK版本为1.8,仍然自带了VisualVM,从1.9开始的版本是没有自带的,需要额外下载,下载的github地址:

https://visualvm.github.io/download.html

另外,如果开发工具使用的是Intellij IDEA的话,可以下载一个插件,VisualVM Launcher,通过插件启动可以直接到上述页面,不用在左边的条目中寻找自己的项目.

当然也有其他的工具,但这个在可预见的未来都会是主力发展的多合一故障处理工具.所以我们后面将会使用这个工具来分析我们的JVM运行情况,进而优化.而需要优化我们还需要对JVM的组成有进一步的了解.接下来我们来看一下JVM的组成

3.JVM组成

3.1.JVM组成图 重点

在这里插入图片描述

从图上可以看到,大致分为以下组件:

  • 类加载器子系统 :字节码加载

  • 运行时数据区 : java程序运行涉及到的区域 jvm内存结构

  • 执行引擎 :程序执行的引擎 即时编译器 垃圾回收

  • 本地方法接口 :接入其他语言lib库

而本地库接口也就是用于调用本地方法的接口,在此我们不细说,主要关注的是上述的3个组件

3.2.类加载子系统 重点

java中一切皆对象! 对象的创建依赖于类,也就是程序在运行时,要把相关的类的字节码(.class)加载进去.这时候就要用到类加载子系统.

3.2.1类加载过程

类加载的过程包括了加载,验证,准备,解析和初始化这5个步骤

  1. 加载**:找到字节码文件,读取到内存中**.类的加载方式分为隐式加载和显示加载两种。隐式加载指的是程序在使用new关键词创建对象时,会隐式的调用类的加载器把对应的类加载到jvm中。显示加载指的是通过直接调用class.forName()方法来把所需的类加载到jvm中。

  2. 验证:验证此字节码文件是不是真的是一个字节码文件,毕竟后缀名可以随便改,而内在的身份标识是不会变的.在确认是一个字节码文件后,还会检查一系列的是否可运行验证,元数据验证,字节码验证,符号引用验证等.Java虚拟机规范对此要求很严格,在Java 7的规范中,已经有130页的描述验证过程的内容.

  3. 准备**:为类中static修饰的变量分配内存空间并设置其初始值为0或null.可**能会有人感觉奇怪,在类中定义一个static修饰的int,并赋值了123,为什么这里还是赋值0.因为这个int的123是在初始化阶段的时候才赋值的,这里只是先把内存分配好.但如果你的static修饰还加上了final,那么就会在准备阶段就会赋值.

    static: 定义时赋值 static{} 构造函数

  4. 解析:解析阶段会将java代码中的符号引用替换为直接引用.比如引用的是一个类,我们在代码中只有全限定名来标识它,在这个阶段会找到这个类加载到内存中的地址.

  5. 初始化:如刚才准备阶段所说的,这个阶段就是对变量的赋初始值的阶段.

    static初始化:初始化语句(=)或者初始化代码快(static)

如上过程都是在JVM执行的过程中自己完成的,我们无需干涉。

非静态:

在这里插入图片描述

3.2.2类加载机制

​ 类的加载是由类加载器加载,先了解类加载器有哪些,再看他们是怎么协作加载.

1.JVM中内置的类加载器:

在这里插入图片描述

​ 每一个类,都需要和它的类加载器一起确定其在JVM中的唯一性.换句话来说,不同类加载器加载的同一个字节码文件,得到的类都不相等.我们可以通过默认加载器去加载一个类,然后new一个对象,再通过自己定义的一个类加载器,去加载同一个字节码文件,拿前面得到的对象去instanceof,会得到的结果是false.

类加载器一般有4种,其中前3种是必然存在的

  • 启动类加载器:加载<JRE_HOME>\lib下的

  • 扩展类加载器:加载<JRE_HOME>\lib\ext下的

  • 应用程序类加载器:加载Classpath下的 ,我们的代码,还有依赖jar都是在classpath,都是被它所加载

  • 自定义类加载器

1.类加载时使用了双亲委派模式:

加载规则,优先使用爷爷加载,如果没有加载到再使用它爹加载,如果他爹也没有加载到,才到自己加载,如果自己也没有加载到才报ClassNotFountException。在这过程中只要上一级加载到了,下一级就不会加载了,这麽做的目的:

  • 不让我们轻易覆盖java提供的类。 lib lib\ext不能改

  • 也要让我们扩展三方功能。 可以修改三方jar里面class,写一个和他一样!改一下就行

2.而双亲委派机制是如何运作的呢?

我们以应用程序类加载器举例,它在需要加载一个类的时候,不会直接去尝试加载,而是委托上级的扩展类加载器去加载,而扩展类加载器也是委托启动类加载器去加载.

​ 启动类加载器在自己的搜索范围内没有找到这么一个类,表示自己无法加载,就再让扩展类加载器去加载,同样的,扩展类加载器在自己的搜索范围内找一遍,如果还是没有找到,就委托应用程序类加载器去加载.如果最终还是没找到,那就会直接抛出异常了.

3.而为什么要这么麻烦的从下到上,再从上到下呢?

​ 这是为了安全着想,保证按照优先级加载.如果用户自己编写一个名为java.lang.Object的类,放到自己的Classpath中,没有这种优先级保证,应用程序类加载器就把这个当做Object加载到了内存中,从而会引发一片混乱.而凭借这种双亲委派机制,先一路向上委托,启动类加载器去找的时候,就把正确的Object加载到了内存中,后面再加载自行编写的Object的时候,是不会加载运行的.

总结: jvm类加载子系统是用来加载类的,类的加载要使用类加载器来加载.java提供种了多种类加载器,它使用双亲委派机制进行协调加载,主要有应用,扩展,启动三种类加载器,委派应用–>扩展–>启动.加载优先使用启动–>扩展–>应用.这样做的好处是不能轻易修改系统提供的类,但是可以扩展三方jar.但是可以扩展三方的. 加载过程是加载,验证,准备,解析,初始化.

3.3运行时数据区-jvm内存结构-重点
3.3.1.运行时数据区组成

jdk1.7

在这里插入图片描述

​ 注意:堆和方法区是线程共享的,其他3个区域是线程隔离的

在这里插入图片描述

JDK1.8及以后,方法区被元空间替代,没有方法区了,元空间直接使用本地内存而不是jvm内存

3.3.2.程序计数器

​ 程序计数器是线程私有的,虽然名字叫计数器,但主要用途还是用来确定指令的执行顺序,比如循环,分支,跳转,异常捕获等.而JVM对于多线程的实现是通过轮流切换线程实现的,所以为了保证每个线程都能按正确顺序执行,将程序计数器作为线程私有.程序计数器是唯一一个JVM没有规定任何OOM的区块.(out of memory)

​ 程序计数器是一块非常小的内存空间,可以看做是当前线程执行字节码的行号指示器,每个线程都有一个独立的程序计数器,因此程序计数器是线程私有的一块空间,此外,程序计数器是Java虚拟机规定的唯一不会发生内存溢出的区域

3.3.3.Java虚拟机栈

​ Java虚拟机栈也是线程私有的,虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应Java代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个Java方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程`。

在这里插入图片描述

在这里插入图片描述

  • 栈帧(方法执行形成栈帧):栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,线程私有。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程,栈帧随着方法调用而创建,随着方法结束而销毁

  • 局部变量表(储存方法参数和局部变量):局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。

  • 操作数栈(用于计算的临时数据存储区):操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO),当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

  • 动态链接(用来转化方法的内存地址直接引用的):在一个class文件中,一个方法要调用其他方法,

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

  • 返回地址:方法的返回地址

3.3.4.本地方法栈

本地方法栈与虚拟机栈的区别是,虚拟机栈执行的是Java方法,本地方法栈执行的是本地方法(Native Method),其他基本上一致,在HotSpot中直接把本地方法栈和虚拟机栈合二为一,这里暂时不做过多叙述。

https://xiaomogui.iteye.com/blog/857821

3.3.5方法区(1.6,1.7)-永久代

方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据。在jdk1.7及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间),但为了与堆做区分,方法区还有个名字叫“非堆”,也有人用“永久代”(HotSpot对方法区的实现方法)来表示方法区。

jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串常量池还有class常量池等),这里只是把字符串常量池移到堆内存中;在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。

在这里插入图片描述

去永久代的原因有:

  • 字符串存在永久代中,容易出现性能问题和内存溢出。 --字符串是用得最多数据类型

  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

3.3.6.堆内存

​ 堆和方法区一样(确切来说JVM规范中方法区就是堆的一个逻辑分区),就是一个所有线程共享的,存放对象的区域,也是GC的主要区域.其中的分区分为新生代,老年代.新生代中又可以细分为一个Eden,两个Survivor区(From,To).Eden中存放的是通过new 或者newInstance方法创建出来的对象,绝大多数都是很短命的.正常情况下经历一次gc之后,存活的对象会转入到其中一个Survivor区,然后再经历默认15次的gc,就转入到老年代.这是常规状态下,在Survivor区已经满了的情况下,JVM会依据担保机制将一些对象直接放入老年代。

​ 堆内存主要用于存放对象和数组,它是JVM管理的内存中最大的一块区域,堆内存和方法区都被所有线程共享,在虚拟机启动时创建。在垃圾收集的层面上来看,由于现在收集器基本上都采用分代收集算法,因此堆还可以分为新生代(YoungGeneration)和老年代(OldGeneration),新生代还可以分为Eden、From Survivor、To Survivor

在这里插入图片描述

3.3.7.元空间 (1.8)

​ 上面说到,jdk1.8中,已经不存在永久代(方法区),替代它的一块空间叫做“元空间”,和永久代类似,都是JVM规范对方法区的实现,但是元空间并不在虚拟机中,而是使用本地内存,元空间的大小仅受本地内存限制,但可以通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定元空间的大小

Jdk1.8去除了方法区,取而代之是元空间,直接使用本地内存

3.4.JVM内存溢出
3.4.1.堆内存溢出

堆内存中主要存放对象、数组等,只要不断地创建这些对象,并且保证GC Roots到对象之间有可达路径来避免垃圾收集回收机制清除这些对象,当这些对象所占空间超过最大堆容量时,就会产生OutOfMemoryError的异常。堆内存异常示例如下:

/**
* 设置最大堆最小堆:-Xms20m -Xmx20m
* 运行时,不断在堆中创建OOMObject类的实例对象,且while执行结束之前,GC Roots(代码中的oomObjectList)到对象(每一个OOMObject对象)之间有可达路径,垃圾收集器就无法回收它们,最终导致内存溢出。
*/
public class HeapOOMTest {
    static class OOMObject {
   }
   public static void main(String[] args) {
       List<OOMObject> oomObjectList = new ArrayList<>();
       while (true) {
           oomObjectList.add(new OOMObject());
       }
   }
}

运行后会报异常,在堆栈信息中可以看到 java.lang.OutOfMemoryError: Java heap space 的信息,说明在堆内存空间产生内存溢出的异常。

新产生的对象最初分配在新生代,新生代满后会进行一次Minor GC,如果Minor GC后空间不足会把该对象和新生代满足条件的对象放入老年代,老年代空间不足时会进行Full GC,之后如果空间还不足以存放新对象则抛出OutOfMemoryError异常。常见原因:内存中加载的数据过多如一次从数据库中取出过多数据;集合对对象引用过多且使用完后没有清空;代码中存在死循环或循环产生过多重复对象;堆内存分配不合理;网络连接问题、数据库问题等。

不会自己改ide的对空间,通过虚拟机参数修改该运行空间.

  • 修改所有

在这里插入图片描述

  • 单个修改

在这里插入图片描述

3.4.2.虚拟机栈/本地方法栈溢出
  1. StackOverflowError:当线程请求的栈的深度大于虚拟机所允许的最大深度,则抛出StackOverflowError,简单理解就是虚拟机栈中的栈帧数量过多(一个线程嵌套调用的方法数量过多)时,就会抛出StackOverflowError异常。最常见的场景就是方法无限递归调用,如下:
package cn.ronghuanet._03memory;
/**
 * 设置每个线程的栈大小:-Xss256k
 * 运行时,不断调用doSomething()方法,main线程不断创建栈帧并入栈,导致栈的深度越来越大,最终导致栈溢出。
 */
public class StackSOFTest {
    private int stackLength = 1;

    public void doSomething() {
        stackLength++;
        doSomething();
    }

    public static void main(String[] args) {
        StackSOFTest stackSOF = new StackSOFTest();
        try {
            stackSOF.doSomething();
        } catch (Throwable e) {//注意捕获的是Throwable--Exception,Error
            System.out.println("栈深度:" + stackSOF.stackLength);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
            throw e;
        }
    }
}

上述代码执行后抛出:Exception in thread “Thread-0” java.lang.StackOverflowError的异常。

  1. OutOfMemoryError:如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError。

在这里插入图片描述

栈上能够产生OutOfMemoryError的示例如下:

/**
 * 设置每个线程的栈大小:-Xss2m
 * 运行时,不断创建新的线程(且每个线程持续执行),每个线程对一个一个栈,最终没有多余的空间来为新的线程分配,导致OutOfMemoryError
 */
public class StackOOMTest {
    private static int threadNum = 0;

    public void doSomething() {
        try {
            Thread.sleep(100000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        final StackOOMTest stackOOM = new StackOOMTest();
        try {
            while (true) {
                threadNum++;
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        stackOOM.doSomething();
                    }
                });
                thread.start();
            }
        } catch (Throwable e) {
            System.out.println("目前活动线程数量:" + threadNum);
            throw e;
        }
    }
}

上述代码运行后会报异常,在堆栈信息中可以看到 java.lang.OutOfMemoryError: unable to create new native thread的信息,无法创建新的线程,说明是在扩展栈的时候产生的内存溢出异常。

总结:在线程较少的时候,某个线程请求深度过大,会报StackOverflow异常,解决这种问题可以适当加大栈的深度(增加栈空间大小),也就是把-Xss的值设置大一些,但一般情况下是代码问题的可能性较大;在虚拟机产生线程时,无法为该线程申请栈空间了,会报OutOfMemoryError异常,解决这种问题可以适当减小栈的深度,也就是把-Xss的值设置小一些,每个线程占用的空间小了,总空间一定就能容纳更多的线程,但是操作系统对一个进程的线程数有限制,经验值在3000~5000左右。在jdk1.5之前-Xss默认是256k,jdk1.5之后默认是1M,这个选项对系统硬性还是蛮大的,设置时要根据实际情况,谨慎操作。

3.4.3.方法区溢出-元空间

方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据,所以方法区溢出的原因就是没有足够的内存来存放这些数据。

由于在jdk1.6之前字符串常量池是存在于方法区中的,所以基于jdk1.6之前的虚拟机,可以通过不断产生不一致的字符串(同时要保证和GC Roots之间保证有可达路径)来模拟方法区的OutOfMemoryError异常;但方法区还存储加载的类信息,所以基于jdk1.7的虚拟机,可以通过动态不断创建大量的类来模拟方法区溢出。

3.4.4.本机直接内存溢出

本机直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但Java中用到NIO相关操作时(比如ByteBuffer的allocteDirect方法申请的是本机直接内存),也可能会出现内存溢出的异常。

3.5.执行引擎

​ 执行引擎包含即时编译器(JIT)和垃圾回收器(GC),对即时编译器我们简单介绍一下,主要重点在于垃圾回收器. 放到下个章节将

即时编译器

javac Hello.java(编译) java Hello(Hello.class-)–>二进制

​ 看到这个东西的存在可能有些人会感到疑问,不是通过javac命令就把我们的java代码编译成字节码文件了吗,这个即时编译器又是干嘛的?

​ 我们需要明确一个概念就是,计算机实际上只认识0和1,这种由0和1组成的命令集称之为”机器码”,而且会根据平台不同而有所不同,可读性和可移植性极差.我们的字节码文件包含的并不是机器码,不能由计算机直接运行,而需要JVM”解释”执行.JVM将字节码文件中所写的命令解释成一个个计算机操作命令,再通知计算机进行运算.

总结:Javac把源文件编译成字节码文件,即使编译JIT把字节码文件中的命令编译成机器码即计算机操作命令去执行。

二.垃圾回收 重点 识别垃圾 清洁工回收 怎么回收

它是垃圾(判断是否是垃圾),选择垃圾回收器来回收(如果没有选择使用默认),对应的垃圾回收器有对应垃圾回收算法

1.垃圾标记算法–识别垃圾

​ 判断对象是否已死就是找出哪些对象是已经死掉的,以后不会再用到的,就像地上有废纸、饮料瓶和百元大钞,扫地前要先判断出地上废纸和饮料瓶是垃圾,百元大钞不是垃圾。判断对象是否已死有引用计数算法和可达性分析算法。

1.1.引用计数算法

​ 给每一个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;每当有一个地方不再引用它时,计数器值减1,这样只要计数器的值不为0,就说明还有地方引用它,它就不是无用的对象。如下图,对象2有1个引用,它的引用计数器值为1,对象1有两个地方引用,它的引用计数器值为2 。

在这里插入图片描述

这种方法看起来非常简单,但目前许多主流的虚拟机都没有选用这种算法来管理内存,原因就是当某些对象之间互相引用时,无法判断出这些对象是否已死,如下图,对象1和对象2都没有被堆外的变量引用,而是被对方互相引用,这时他们虽然没有用处了,但是引用计数器的值仍然是1,无法判断他们是死对象,垃圾回收器也就无法回收。

在这里插入图片描述

1.2.可达性分析算法

​ 了解可达性分析算法之前先了解一个概念——GC Roots,垃圾收集的起点,可以作为GC Roots的有虚拟机栈中本地变量表中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。

当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,就说明此对象是不可用的,是死对象。如下图:object1、object2、object3、object4和GC Roots之间有可达路径,这些对象不会被回收,但object5、object6、object7到GC Roots之间没有可达路径,这些对象就被判了死刑。

在这里插入图片描述

在这里插入图片描述

上面被判了死刑的对象(object5、object6、object7)并不是必死无疑,还有挽救的余地。进行可达性分析后对象和GC Roots之间没有引用链相连时,对象将会被进行一次标记,接着会判断如果对象没有覆盖Object的finalize()方法或者finalize()方法已经被虚拟机调用过,那么它们就会被行刑(清除);如果对象覆盖了finalize()方法且还没有被调用,则会执行finalize()方法中的内容,所以在finalize()方法中如果重新与GC Roots引用链上的对象关联就可以拯救自己,但是一般不建议这么做.

final finally finalize

2.常用垃圾回收算法

常用的垃圾回收算法有四种:标记-清除算法、复制算法、标记-整理算法,分代回收(组合前面)

2.1.标记清除算法

分为标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象,如下图

在这里插入图片描述

缺点:标记和清除两个过程效率都不高;标记清除之后会产生大量不连续的内存碎片。

2.2.复制算法

把内存分为大小相等的两块,每次存储只用其中一块,当这一块用完了,就把存活的对象全部复制到另一块上,同时把使用过的这块内存空间全部清理掉,往复循环,如下图。

在这里插入图片描述

缺点:实际可使用的内存空间缩小为原来的一半,比较适合

2.3.标记整理算法

先对可用的对象进行标记,然后所有被标记的对象向一端移动,最后清除可用对象边界以外的内存,如下图。

在这里插入图片描述

2.4.分代收集算法

​ 把堆内存分为新生代和老年代,新生代又分为Eden区、From Survivor和To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此新生代采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。

在这里插入图片描述

3.Minor GC和Full GC(2.4进行补充)

3.1.Stop-The-World

​ 在说这两种回收的区别之前,我们先来说一个概念,“Stop-The-World”。如字面意思,每次垃圾回收的时候,都会将整个JVM暂停,回收完成后再继续。如果一边增加废弃对象,一边进行垃圾回收,完成工作似乎就变得遥遥无期了。

3.2.Minor GC

​ 新生代的回收称为Minor GC,新生代的回收一般回收很快,采用复制算法,造成的暂停时间很短

3.3.Full GC

​ 而Full GC一般是老年代的回收,并伴随至少一次的Minor GC,新生代和老年代都回收,而老年代采用标记-整理算法这种GC每次都比较慢造成的暂停时间比较长,通常是Minor GC时间的10倍以上。

所以很明显,我们需要尽量通过Minor GC来回收内存,而尽量少的触发Full GC。毕竟系统运行一会儿就要因为GC卡住一段时间,再加上其他的同步阻塞,整个系统给人的感觉就是又卡又慢。

3.4.GC的流程
  1. 大多数情况下,新的对象都分配在Eden区,当Eden区没有空间进行分配时,将进行一次Minor GC,清理Eden区中的无用对象。清理后,Eden和From Survivor中的存活对象如果小于To Survivor的可用空间则进入To Survivor,否则直接进入老年代);Eden和From Survivor中还存活且能够进入To Survivor的对象年龄增加1岁(虚拟机为每个对象定义了一个年龄计数器,每执行一次Minor GC年龄加1),当存活对象的年龄到达一定程度(默认15岁)后进入老年代,可以通过-XX:MaxTenuringThreshold来设置年龄的值。

  2. 当进行了Minor GC后,Eden还不足以为新对象分配空间(那这个新对象肯定很大),新对象直接进入老年代。

  3. 大对象(需要大量连续内存的对象)例如很长的数组,会直接进入老年代,

  4. 如果老年代没有足够的连续大空间来存放,则会进行Full GC。**

    Minor GC之前,如果预测老年代内存不够,就进行Full GC老年代,否则就Minor GC新生代

  5. 当在java代码里直接调用System.gc()时,会建议JVM进行Full GC,但一般情况下都会触发Full GC,一般不建议使用,尽量让虚拟机自己管理GC的策略。

4.常见垃圾收集器

4.1.垃圾回收器分类

GC 发展阶段:Serial(串行) => Parallel(并行)=> CMS(并发)=> G1 => ZGC

现在常见的垃圾收集器有如下几种

  • 新生代收集器:Serial、ParNew、Parallel Scavenge

  • 老年代收集器:Serial Old、CMS、Parallel Old

  • 堆内存垃圾收集器:G1

每种垃圾收集器之间有连线,表示他们可以搭配使用。

在这里插入图片描述

4.2.新生代:Serial

Serial是一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World)。就比如妈妈在家打扫卫生的时候,肯定不会边打扫边让儿子往地上乱扔纸屑,否则一边制造垃圾,一遍清理垃圾,这活啥时候也干不完。

如下是Serial收集器和Serial Old收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,Serial收集器以单线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。

在这里插入图片描述
适用场景:Client模式(桌面应用);单核服务器。可以用-XX:+UserSerialGC来选择Serial作为新生代收集器。

4.3.新生代:ParNew

ParNew就是一个Serial的多线程版本,其它与Serial并无区别。ParNew在单核CPU环境并不会比Serial收集器达到更好的效果,它默认开启的收集线程数和CPU数量一致,可以通过-XX:ParallelGCThreads来设置垃圾收集的线程数。

如下是ParNew收集器和Serial Old收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,ParNew收集器以多线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。

在这里插入图片描述

适用场景:多核服务器;与CMS收集器搭配使用。当使用-XX:+UserConcMarkSweepGC来选择CMS作为老年代收集器时,新生代收集器默认就是ParNew,也可以用-XX:+UseParNewGC来指定使用ParNew作为新生代收集器。

4.4.新生代:Parallel Scavenge(掌握)

Parallel Scavenge也是一款用于新生代的多线程收集器,与ParNew的不同之处是,ParNew的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge的目标是达到一个可控制的吞吐量

可以通过-XX:MaxGCPauseMillis来设置收集器尽可能在多长时间内完成内存回收,可以通过-XX:GCTimeRatio来精确控制吞吐量。

如下是Parallel收集器和Parallel Old收集器结合进行垃圾收集的示意图,在新生代,当用户线程都执行到安全点时,所有线程暂停执行,Parallel收集器以多线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行;在老年代,当用户线程都执行到安全点时,所有线程暂停执行,Parallel Old收集器以多线程,采用标记整理算法进行垃圾收集工作。

​ 停顿10s 收集5s---->停顿11s 收集1s

在这里插入图片描述

适用场景:注重吞吐量,高效利用CPU,需要高效运算且不需要太多交互。可以使用-XX:+UseParallelGC来选择Parallel Scavenge作为新生代收集器,jdk7、jdk8默认使用Parallel Scavenge作为新生代收集器。

4.5.老年代:Serial Old

Serial Old收集器是Serial的老年代版本,同样是一个单线程收集器,采用标记-整理算法。

如下图是Serial收集器和Serial Old收集器结合进行垃圾收集的示意图:

在这里插入图片描述

适用场景:Client模式(桌面应用);单核服务器;与Parallel Scavenge收集器搭配;作为CMS收集器的后备预案。

4.6.老年代:CMS

CMS收集器是一种以最短回收停顿时间为目标的收集器,以“最短用户线程停顿时间”著称。整个垃圾收集过程分为4个步骤

  • 初始标记:标记一下GC Roots能直接关联到的对象,速度较快
  • 并发标记:进行GC Roots Tracing,标记出全部的垃圾对象,耗时较长
  • 重新标记:修正并发标记阶段引用户程序继续运行而导致变化的对象的标记记录,耗时较短
  • 并发清除:用标记-清除算法清除垃圾对象,耗时较长

整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS收集器垃圾收集可以看做是和用户线程并发执行的。

在这里插入图片描述

CMS收集器也存在一些缺点:

  • 对CPU资源敏感:默认分配的垃圾收集线程数为(CPU数+3)/4,随着CPU数量下降,占用CPU资源越多,吞吐量越小

  • 无法处理浮动垃圾:在并发清理阶段,由于用户线程还在运行,还会不断产生新的垃圾,CMS收集器无法在当次收集中清除这部分垃圾。同时由于在垃圾收集阶段用户线程也在并发执行,CMS收集器不能像其他收集器那样等老年代被填满时再进行收集,需要预留一部分空间提供用户线程运行使用。当CMS运行时,预留的内存空间无法满足用户线程的需要,就会出现“Concurrent Mode Failure”的错误,这时将会启动后备预案,临时用Serial Old来重新进行老年代的垃圾收集。

  • 因为CMS是基于标记-清除算法,所以垃圾回收后会产生空间碎片,可以通过-XX:UserCMSCompactAtFullCollection开启碎片整理(默认开启),在CMS进行Full GC之前,会进行内存碎片的整理。还可以用-XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩(不进行碎片整理)的Full GC之后,跟着来一次带压缩(碎片整理)的Full GC。

适用场景:重视服务器响应速度,要求系统停顿时间最短。可以使用-XX:+UserConMarkSweepGC来选择CMS作为老年代收集器。

4.7.老年代:Parallel Old

Parallel Old收集器是Parallel Scavenge的老年代版本,是一个多线程收集器,采用标记-整理算法。可以与Parallel Scavenge收集器搭配,可以充分利用多核CPU的计算能力

在这里插入图片描述

适用场景:与Parallel Scavenge收集器搭配使用;注重吞吐量。jdk7、jdk8默认使用该收集器作为老年代收集器使用 -XX:+UseParallelOldGC来指定使用Paralle Old收集器。

4.8.堆收集:G1 收集器

G1 收集器是jdk1.7才正式引用的商用收集器,现在已经成为jdk1.9默认的收集器。前面几款收集器收集的范围都是新生代或者老年代,G1进行垃圾收集的范围是整个堆内存,它采用“化整为零”的思路,把整个堆内存划分为多个大小相等的独立区域(Region),在G1收集器中还保留着新生代和老年代的概念,它们分别都是一部分Region,如下图:

在每个Region中,都有一个Remembered Set来实时记录该区域内的引用类型数据与其他区域数据的引用关系(在前面的几款分代收集中,新生代、老年代中也有一个Remembered Set来实时记录与其他区域的引用关系),在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据

如下图所示,G1收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收,和CMS收集器前几步的收集过程很相似:

在这里插入图片描述

  1. 初始标记:标记出GC Roots直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行

  2. 并发标记:从GC Root开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行

  3. 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录

  4. 筛选回收:筛选回收阶段会对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是Garbage First的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程

适用场景:要求尽可能可控GC停顿时间;内存占用较大的应用。可以用-XX:+UseG1GC使用G1收集器,jdk9默认使用G1收集器。

Jdk1.7.1.8新生代使用Parallel Scavenge(复制),老年代使用Parallel Old(标记整理)

jdk1.9 默认G1,被它自己淘汰.

jdk14 zgc jdk17用的zgc. 不收费

https://blog.csdn.net/weixin_45925485/article/details/127580787

三.JVM的优化

1.JVM优化概述

1.1.JVM优化目标

JVM调优目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟

程序在上线前的测试或运行中有时会出现一些大大小小的JVM问题,比如cpu load过高、请求延迟、tps降低等,甚至出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃,因此需要对JVM进行调优,使得程序在正常运行的前提下,获得更高的用户体验和运行效率。

这里有几个比较重要的指标:

  • **内存占用:**程序正常运行需要的内存大小。

  • **延迟:**由于垃圾收集而引起的程序停顿时间。

  • **吞吐量:**用户程序运行时间占用户程序和垃圾收集占用总时间的比值。

当然,和CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。

[重要]调优可以依赖、参考的数据有系统运行日志(OOM)、堆栈错误信息、gc日志、线程快照、堆转储快照等。

1.2.优化手段
  • 系统运行日志

    系统运行日志就是在程序代码中打印出的日志,描述了代码级别的系统运行轨迹(执行的方法、入参、返回值等),一般系统出现问题,系统运行日志是首先要查看的日志。

  • 堆栈错误信息

    当系统出现异常后,可以根据堆栈信息初步定位问题所在,比如根据“java.lang.OutOfMemoryError: Java heap space”可以判断是堆内存溢出;根据“java.lang.StackOverflowError”可以判断是栈溢出;根据“java.lang.OutOfMemoryError: PermGen space”可以判断是方法区溢出等。

  • GC日志

    程序启动时用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 可以在程序运行时把gc的详细过程记录下来,或者直接配置“-verbose:gc”参数把gc日志打印到控制台,通过记录的gc日志可以分析每块内存区域gc的频率、时间等,从而发现问题,进行有针对性的优化

  • 线程快照

    顾名思义,根据线程快照可以看到线程在某一时刻的状态,当系统中可能存在请求超时、死循环、死锁等情况是,可以根据线程快照来进一步确定问题。通过执行虚拟机自带的“jstack pid”命令,可以dump出当前进程中线程的快照信息

  • 堆转储快照

    程序启动时可以使用 “-XX:+HeapDumpOnOutOfMemory” 和

    “-XX:HeapDumpPath=/data/jvm/dumpfile.hprof”,当程序发生内存溢出时,把当时的内存快照以文件形式进行转储(也可以直接用jmap命令转储程序运行时任意时刻的内存快照),事后对当时的内存使用情况进行分析。

2.JVM内存优化—通过一些工具或日志,找到问题所在,要么设置内存,要么跟换垃圾回收器.

2.1.使用jps,jmap分析内存快照
1.用jps`(JVM process Status)查看进程

可以查看虚拟机启动的所有进程、执行主类的全名、JVM启动参数,比如当执行了JPSTest类中的main方法后(main方法持续执行),执行 jps -l可看到下面的OOMTest类的pid为7480,加上-v参数还可以看到JVM启动参数。

在这里插入图片描述

在这里插入图片描述

2.用jstat(JVM Statistics Monitoring Tool)监视虚拟机信息

jstat -gc pid 500 10 :pid是进程ID,每500毫秒打印一次Java堆状况(各个区的容量、使用容量、gc时间等信息),打印10次

在这里插入图片描述

在这里插入图片描述

jstat还可以以其他角度监视各区内存大小、监视类装载信息等,具体可以google jstat的详细用法。

3.用jmap(Memory Map for Java)查看堆内存信息

执行jmap -histo pid可以打印出当前堆中所有每个类的实例数量和内存占用,如下,class name是每个类的类名([B是byte类型,[C是char类型,[I是int类型),bytes是这个类的所有示例占用内存大小,instances是这个类的实例数量:

在这里插入图片描述

执行jmap -dump 可以转储堆内存快照到指定文件,比如执行

jmap -dump:format=b,file=/data/jvm/dumpfile_jmap.hprof PID ,可以把当前堆内存的快照转储到dumpfile_jmap.hprof文件中,然后可以对内存快照进行分析。

4.分析堆转储快照

前面说到配置了 “-XX:+HeapDumpOnOutOfMemory” 参数可以在程序发生内存溢出时dump出当前的内存快照,也可以用jmap命令随时dump出当时内存状态的快照信息,dump的内存快照一般是以.hprof为后缀的二进制格式文件。

可以直接用 jhat(JVM Heap Analysis Tool) 命令来分析内存快照,它的本质实际上内嵌了一个微型的服务器,可以通过浏览器来分析对应的内存快照,比如执行 jhat -port 9810 -J-Xmx4G /data/jvm/dumpfile_jmap.hprof 表示以9810端口启动 jhat 内嵌的服务器:

在这里插入图片描述

在控制台可以看到服务器启动了,访问 http://127.0.0.1:9810/ 可以看到对快照中的每个类进行分析的结果。

在这里插入图片描述

2.2.jvisualvm分析内存快照

jvisualvm也可以分析内存快照,在jvisualvm菜单的“文件”-“装入”,选择堆内存快照,快照中的信息就以图形界面展示出来了,

或者直接使用监视 - 堆Dump :如下,主要可以查看每个类占用的空间、实例的数量和实例的详情等:

在这里插入图片描述

在这里插入图片描述

2.3.内存溢出问题定位
1.设置堆大小,以及捕获jvm日志
-Xms50m
-Xmx50m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:\

在这里插入图片描述

2.制造内存溢出
new Thread(new Runnable() {
    List list = new ArrayList<>();
    @Override
    public void run() {
        while(true){
            list.add(new Object());
            System.out.println(Thread.currentThread().getName()+"线程执行.....");
        }
    }
}).start();

在这里插入图片描述

3.使用Java Visual VM 分析日志

文件 -> 载入 -> 文件类型选择堆 -> 打开日志

在这里插入图片描述

概要中可以看到内存溢出异常,Thread-61 ,点击进去

在这里插入图片描述

这里可以看到是哪一行,哪个对象造成的内存溢出

在这里插入图片描述

3.常用JVM参数参考

在这里插入图片描述

  • jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
  • jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
  • jdk1.9 默认垃圾收集器G1
  • jdk14以后途径zgc

-XX:+PrintCommandLineFlags jvm参数可查看默认设置收集器类型

-XX:+PrintGCDetails亦可通过打印的GC日志的新生代、老年代名称判断

在里面公司里面一般都会有一些启动项目的参数模板,拷贝过去先运行!

java -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m(年轻代) -Xss256k(线程栈) -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC newframe-1.0.0.jar

4.设置jvm参数的几种方式 面试点

4.1 开发时

1、springboot项目-改一个

idea中配置

在这里插入图片描述

2、集成开发环境下启动并使用JVM,如eclipse需要修改根目录文件

eclipse.ini; idea**,idea64.exe.vmoptions,** 单个修改通过vm参数

4.2 上线的时候
Linux: java
初始值:  如果你们公司没有...可以使用参考值
java -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m(年轻代) -Xss256k(线程栈) -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC newframe-1.0.0.jar

docker:
NTRYPOINT ["java", "-jar -XX:MetaspaceSize=128m -XX:MaxMe    xxx.jar]



5.调优经验

我们依据Java Performance这本书的建议的设置原则进行设置,

​ Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍,Xmx和Xms的大小设置为一样,避免GC后对内存的重新分配。而Full GC之后的老年代内存大小,我们可以通过前面在Visual VM中添加的插件Visual GC查看。先手动进行一次GC,然后查看老年代的内存占用。

​ 新生代Xmn的设置为老年代存活对象的1-1.5倍。

​ 老年代的内存大小设置为老年代存活对象的2-3倍。

​ JVM配置方面,一般情况可以先用默认配置(基本的一些初始参数可以保证一般的应用跑的比较稳定了),在测试中根据系统运行状况(会话并发情况、会话时间等),结合gc日志、内存监控、使用的垃圾收集器等进行合理的调整,当老年代内存过小时可能引起频繁Full GC,当内存过大时Full GC时间会特别长。

​ 那么JVM的配置比如新生代、老年代应该配置多大最合适呢?答案是不一定,调优就是找答案的过程,物理内存一定的情况下,新生代设置越大,老年代就越小,Full GC频率就越高,但Full GC时间越短;相反新生代设置越小,老年代就越大,Full GC频率就越低,但每次Full GC消耗的时间越大。建议如下:

​ -Xms和-Xmx的值设置成相等,堆大小默认为-Xms指定的大小,默认空闲堆内存小于40%时,JVM会扩大堆到-Xmx指定的大小;空闲堆内存大于70%时,JVM会减小堆到-Xms指定的大小。如果在Full GC后满足不了内存需求会动态调整,这个阶段比较耗费资源。

​ 新生代尽量设置大一些,让对象在新生代多存活一段时间,每次Minor GC 都要尽可能多的收集垃圾对象,防止或延迟对象进入老年代的机会,以减少应用程序发生Full GC的频率。

​ 老年代如果使用CMS收集器,新生代可以不用太大,因为CMS的并行收集速度也很快,收集过程比较耗时的并发标记和并发清除阶段都可以与用户线程并发执行。

​ 方法区大小的设置,1.6之前的需要考虑系统运行时动态增加的常量、静态变量等,1.7只要差不多能装下启动时和后期动态加载的类信息就行。

代码实现方面,性能出现问题比如程序等待、内存泄漏除了JVM配置可能存在问题,代码实现上也有很大关系:

避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发Full GC。

避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从Excel中读取大量记录,可以分批读取,用完尽快清空引用。

当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空,这些无用对象尽快回收避免进入老年代。

可以在合适的场景(如实现缓存)采用软引用、弱引用,比如用软引用来为ObjectA分配实例:SoftReference objectA=new SoftReference(); 在发生内存溢出前,会将objectA列入回收范围进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出的异常。

避免产生死循环,产生死循环后,循环体内可能重复产生大量实例,导致内存空间被迅速占满。

尽量避免长时间等待外部资源(数据库、网络、设备资源等)的情况,缩小对象的生命周期,避免进入老年代,如果不能及时返回结果可以适当采用异步处理的方式等。

四.总结

1.重点内容

  • JVM组成
  • jvm内存结构
  • 类加载子系统
  • 垃圾标记/回收算法
  • 常见的垃圾回收器
  • JVM常用参数
  • 内存溢出解决

2.面试必备

  • 你们用什么工具监控JVM
  • JVM类加载流程
  • JVM类加载器有几种类型,分别加载什么东西,用到什么设计模式?
  • JVM组成,以及他们的作用
  • 在JVM层面,一个线程是如何执行的?
  • 程序内存溢出了,如何定位问题出在哪儿?
  • 垃圾标记算法, 垃圾回收算法
  • 说说分带回收算法,(新生代,老年代)
  • JVM优化的目的是什么?
  • 堆大小怎么调,栈大小怎么调
  • 线程在JVM中是怎么执行的,涉及到哪些区域?
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值