当前位置: 首页 > news >正文

jvm汇总

JDK、JRE、JVM、Java的区别

JVM是Java虚拟机,JRE是Java运行环境,JDK是个Java开发的工具包,Java是门编程语言。 

JVM(Java Virtual Machine):是Java虚拟机,是Java程序运行的基础,它将Java程序编译后的字节码解释执行,并将其转换为机器码运行。
JRE(Java Runtime Environment):是Java运行环境,包括了JVM以及Java程序运行所需的类库等。
JDK:Java开发工具包,包括了JRE以及用于Java开发的工具,如编译器(javac)、调试器(jdb)、打包工具(jar)等。

JVM的了解

得分点

Java跨平台、HotSpot、热点代码探测技术、内存模型、垃圾回收算法、垃圾回收器

跨平台 

Java跨平台,JVM不跨平台。 

JVM是Java语言跨平台的关键,Java在虚拟机层面隐藏了底层技术的复杂性以及机器与操作系统的差异性。运行程序的物理机千差万别,而JVM则在千差万别的物理机上面建立了统一的运行平台,实现了在任意一台JVM上编译的程序,都能在任何其他JVM上正常运行。这一极大的优势使得Java应用的开发比传统C/C++应用的开发更高效快捷,程序员可以把主要精力放在具体业务逻辑,而不是放在保障物理硬件的兼容性上。通常情况下,一个程序员只要了解了必要的Java类库、Java语法,学习适当的第三方开发框架,就已经基本满足日常开发的需要了,JVM会在用户不知不觉中完成对硬件平台的兼容及对内存等资源的管理工作。

默认Java虚拟机HotSpot 

HotSpot是Sun/OracleJDK和OpenJDK中的默认Java虚拟机,也是目前使用范围最广的Java虚拟机。HotSpot既继承了Sun之前两款商用虚拟机的优点,也有许多自己新的技术优势,如它名称中的HotSpot指的就是它的热点代码探测技术。HotSpot的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知即时编译器以方法为单位进行编译。如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准即时编译和栈上替换编译行为。通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序,即时编译的时间压力也相对减小,这样有助于引入更复杂的代码优化技术,输出质量更高的本地代码。本地方法栈和Java方法栈是合并的。
 

JVM内存模型

得分点 

类加载子系统、运行时数据区、执行引擎

总结:JVM由三部分组成:类加载子系统、运行时数据区、执行引擎。

类加载子系统:根据指定的全限定名来载入类或接口。

运行时数据区:在程序运行时,存储程序的内容,例如:字节码、对象、参数、返回值等。而运行时数据区又可以分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。执行引擎:负责执行那些包含在被载入类的方法中的指令。

JVM由三部分组成:类加载子系统、运行时数据区、执行引擎

1、类加载子系统:通过类加载机制加载类的class文件,如果该类是第一次加载,会执行加载、验证、解析。只负责class文件的加载,至于是否可运行,则由执行引擎决定。

类加载过程是在类加载子系统完成的:加载 --> 链接(验证 --> 准备 --> 解析) --> 初始化

类加载过程:加载、链接(验证、准备、解析)、初始化。这个过程是在类加载子系统完成的。

加载:生成类的Class对象。

        1、通过一个类的全限定名获取定义此类的二进制字节流

                (即编译时生成的类的class字节码文件)
        2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

        包括创建运行时常量池,将类常量池的部分符号引用放入运行时常量池。
        3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区

                这个类各种数据的访问入口。注意类的class对象是运行时生成的,

                类的class字节码文件是编译时生成的。

链接:将类的二进制数据合并到JRE中。该过程分为以下3个阶段:

        

1、验证:确保代码符合JAVA虚拟机规范和安全约束。

                        包括文件格式验证、元数据验证、字节码验证、符号引用验证。
                文件格式验证:验证字节码文件是否符合规范。
                        魔数:是否魔数0xCAFEBABE开头
                        版本号:版本号是否在JVM兼容范围
                        常量类型:类常量池里常量类型是否合法
                        索引值:索引值是否指向不存在或不符合类型的常量。
                元数据验证:元数据是字节码里类的全名、方法信息、字段信息、继承关系等。
                        标识符:验证类名接口名标识符有没有符合规范
                        接口实现方法:有没有实现接口的所有方法
                        抽象类实现方法:有没有实现抽象类的所有抽象方法
                        final类:是不是继承了final类。
                指令验证:主要校验类的方法体,通过数据流和控制流分析,

                                        保证方法在运行时不会危害虚拟机安全。
                        类型转换:保证方法体中的类型转换是否有效。

                                                例如把某个类强转成没继承关系的类
                        跳转指令:保证跳转指令不会跳转到方法体以外的字节码指令上;
                                保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
                符号引用验证:确保后面解析阶段能正常执行。
                        类全限定名地址:验证类全限定名是否能找到对应的类字节码文件
                        引用地址:引用指向地址是否存在实例
                       引用权限:是否有权引用

  • 准备:为类变量(即static变量)分配内存并赋零值。
  • 解析:将方法区-运行时常量池内的符号引用(类的名字、成员名、标识符)转为直接引用(实际内存地址,不包含任何抽象信息,因此可以直接使用)。

初始化:类变量赋初值、执行静态语句块。

2、运行时数据区

在程序运行时,存储程序的内容(例如字节码、对象、参数、返回值等)。

运行时数据区包括本地方法栈、虚拟机栈、方法区、堆、程序计数器。

只有方法区和堆是各线程共享的进程内存区域,其他运行区都是每个线程可以独立拥有的。

       本地方法栈:存放本地方法调用过程中的栈帧。用于管理本地方法的调用,本地方法是C语言                              写的。不是所有虚拟机都支持本地方法栈,例如Hotspot虚拟机就是将本地方法                                栈和虚拟机栈合二为一。

                            栈解决程序的运行问题,即程序如何执行、如何处理数据

             栈帧:栈帧是栈的元素,由三部分组成,即局部变量表(存方法参数和局部变量)、

                        操作数栈(存方法执行过程中的中间结果,或者其他暂存数据)

                        和帧数据区(存方法返回地址、线程引用等附加信息)。
      虚拟机栈:存放Java方法调用过程中的栈帧。用于管理Java方法的调用,

                          Java方法是开发时写的Java方法。
         方法区:可以看作是一块独立于Java堆的内存空间,方法区是各线程共享的内存区域。
                 方法区和永久代、元空间的关系:方法区是一个抽象概念,永久代和元空间是方法区的                                                                      实现方式。
                 永久代:属于JVM方法区的内存,用来存储类的元数据,如类名、方法信息、字段信息                                 等一些静态的数据。JDK7及之前方法区也叫永久代。缺点是内存大小固定,

                               容易出现oom问题。可以通过-XX:PermSize设置永久代大小。永久代对象只能                                 通过Major GC(又称Full GC)进行垃圾回收。
                元空间:是Hotspot在JDK8引入的,用于取代永久代。元空间属于本地内存,由操作系                                 统直接管理,不再受JVM管理。同时内存空间可以自动扩容,避免内存溢出。                                默认情况下元空间可以无限使用本地内存,也可以通过-XX:MetaspaceSize 

                               限制内存大小。
            常量池:就是一张表,JVM根据这张常量表找到要执行的类信息和方法信息
                  类常量池:是.class字节码文件中的资源仓库,

                                主要存放字面量(表示字符串值和数值,例如字符串值"abc"、final常量、

                                静态变量

                        和符号引用(类和接口的全限定名、字段名、方法名)。
                运行时常量池:类加载的“加载”阶段会创建运行时常量池,统一存放各个类常量池

                                        去重后的符号引用。在类加载的“解析”阶段JVM会把运行时常量池的

                                        这些符号引用转为直接引用。类常量池。类常量池在字节码文件中的,
                                        运行时常量池在内存中。
                字符串常量池:专门针对String类型设计的常量池。是当前应用程序里所有线程共享

                                        的,每个jvm只有一个字符串常量池。存储字符串对象的引用。

                                        在创建String对象时,JVM会先在字符串常量池寻找是否已存在相同字

                                        符串的引用,如果有的话就直接返回引用,没的话就在堆中创建

                                           一个对象,然后常量池保存这个引用并返回引用。

  • 堆:存放对象实例、实例变量、数组,包括新生代(伊甸园区、幸存区S0和S1)和老年代。堆是垃圾收集器管理的内存区域。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。堆实际内存空间可以不连续,大小可以选择固定大小或可扩展,堆是各线程共享的内存区域。

堆是垃圾收集器管理的内存区域。

堆解决的是数据存储的问题,即数据怎么放、放在哪儿。堆实际内存空间可以不连续,大小可以选择固定大小或可扩展,堆是各线程共享的内存区域。

堆的GC流程:

 首先,任何新对象都分配到 eden 空间。两个幸存者空间开始时都是空的。
当 eden 空间填满时,将触发一个Minor GC(年轻代的垃圾回收,也称为Young GC),删除所有未引用的对象,大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年代。
所有被引用的对象作为存活对象,将移动到第一个幸存者空间S0,并标记年龄为1,即经历过一次Minor GC。之后每经过一次Minor GC,年龄+1。GC分代年龄存储在对象头的Mark Word里。
当 eden 空间再次被填满时,会执行第二次Minor GC,将Eden和S0区中所有垃圾对象清除,并将存活对象复制到S1并年龄加1,此时S0变为空。
如此反复在S0和S1之间切换几次之后,还存活的年龄等于15的对象(JDK8默认15,JDK9默认7,-XX:InitialTenuringThreshold=7)在下一次Minor GC时将放到老年代中。 
当老年代满了时会触发Major GC(也称为Full GC),Major GC 清理整个堆 – 包括年轻代和老年代。

  • 程序计数器(PC寄存器):存放下一条字节码指令的地址,由执行引擎读取下一条字节码指令并转为本地机器指令进行执行。是程序控制流(分支、循环、跳转、线程恢复)的指示器,只有它不会抛出OutOfMemoryError。每个线程有自己独立的程序计数器,以便于线程在切换回来时能知道下一条指令是什么。程序计数器生命周期与线程一致。

3、执行引擎:将字节码指令解释/编译为对应平台上的本地机器指令。充当了将高级语言翻译为机器语言的译者。执行引擎在执行过程中需要执行什么样的字节码指令依赖于PC寄存器。每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。

              字节码指令(JVM指令):字节码文件中的指令,内部只包含一些能够被JVM所                                                                       识别的字节码指令、符号表,以及其他辅助信息,
                                                    不能够直接运行在操作系统之上。
                        本地机器指令:可以直接运行在操作系统之上。

加分回答-运行时数据区

运行时数据区是开发者重点要关注的部分,因为程序的运行与它密不可分,很多错误的排查也需要基于对运行时数据区的理解。在运行时数据区所包含的几块内存空间中,方法区和堆是线程之间共享的内存区域,而虚拟机栈、本地方法栈、程序计数器则是线程私有的区域,就是说每个线程都有自己的这个区域。 

                        
原文链接:https://blog.csdn.net/qq_40991313/article/details/130232389

详细参考:

什么是JVM的内存模型?详细阐述Java中局部变量、常量、类名等信息

 JVM类加载机制    

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历

加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个部分统称为连接,而前五个阶段则是类加载的完整过程。

1. 在加载阶段JVM需要在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
2. 验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
3. 准备阶段是正式为类中定义变量(静态变量)分配到内存并设置类变量初始值的阶段,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域。
4. 解析阶段是Java虚拟机将常量池内的符号替换为直接引用的过程,符号引用以一组符号来描述所引用的目标,直接引用是可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。
5. 类的初始化阶段是类加载过程的最后一个步骤,直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。本质上,初始化阶段就是执行类构造器的过程。并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。

加分回答
关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”:


1. 使用new实例化对象、读写类的静态字段、调用类的静态方法时。
2. 使用java.lang.reflect包的方法对类型进行反射调用时。
3. 当初始化类时,若发现其父类还没有进行过初始化,则先初始化这个父类。
4. 虚拟机启动时,需要指定一个要执行的主类,虚拟机会先初始化这个主类。
5. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
 

详细参考:

JDK编译生成的.class字节码文件是什么?从底层结构到代码验证,深度解析Java字节码文件-CSDN博客

Java的类是怎样在虚拟机中加载的?详细阐述JVM的加载、验证和解析过程

JVM对象的实例化过程

得分点

类加载、分配内存(内存规整和不规整)、处理并发安全问题、设置对象头、成员变量赋初值、执行构造方法

   对象的实例化过程:

1、判断对应类是否加载过:首先JVM检查在方法区Metaspace(元空间)的常量池里能否定位到       该类的符号引用,能的话通过符号引用检查该类是否加载链接初始化过;若没有则在双亲委派       机制下,当前类加载器调用findClass()方法查找类的.class字节码文件,然后调用                         loadClass("类全限定名")方法遵循双亲委派机制加载链接初始化类到内存中,并生成类的             class对象,作为方法区这个类各种数据的访问入口。
2、创建对象:
    1、分配堆内存空间:如果内存规整:
(例如标记整理算法),采用指针碰撞法为新对象分配内            存。如果内存不规整:(有内存碎片,例如标记清除算法),在空闲列表里找到合适大小的           空闲 内存分配给新对象。现在主流虚拟机新生代都是使用标记复制算法,内存都是规整的。
    2、处理并发安全问题:CAS失败重试,区域加锁,每个线程分配一块TLAB内存缓冲区
    3 、设置对象头:
将哈希码、GC分代年龄、锁信息、GC标记等存在对象头的Mark Word中;
    4、成员变量赋初值:若指定了初值则赋指定的值。若未指定初值,则基本类型赋0或false、

           引用类型赋null。
3、执行构造方法:有父类的话,子类构造方法第一行会隐式或手动显式地加super()

指针碰撞法: 指针一直在空闲和已用内存中间,分配空间时,指针往空闲内存方向移动一段距离,使这段距离刚好满足新对象内存大小。

元空间:是Hotspot在JDK8引入的,用于取代永久代。元空间属于本地内存,由操作系统直                接管理,不再受JVM管理。同时也可以自动扩容内存空间,避免内存溢出。默认情                况下元空间可以无限使用本地内存,也可以通过-XX:MetaspaceSize限制内存大 小。

指针碰撞法: 所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界                          点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小

                相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法

                的,虚拟机采用这种分配方式。 一般使用带有compact( 整理)过程的收集器时,

                使用指针碰撞。

回顾synchronized用到的对象头:

前面多线程篇有提到,synchronized锁基于对象头的Mark Word,锁升级四个状态里,偏向锁和轻量级锁基于CAS原子替换,重量级锁基于Monitor对象。对象头里Mark Word存哈希码、GC标记、锁信息。对象头里类型指针指向当前对象所在的类。

锁信息:

锁标志位:01未锁定、01可偏向、00轻量级锁、10重量级锁、11垃圾回收标记

偏向锁线程ID、时间戳等

轻量级锁的指针:指向锁记录的指针

重量级锁的指针:指向Monitor锁的指针
                        【Java面试八股文】Java多线程篇_java多线程八股文_vincewm的博客-CSDN博客

JVM中,对象的创建遵循如下过程

当JVM遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。

内存分配完成之后,虚拟机必须将分配到的内存空间都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。

接下来,虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的`<init>()`方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说,new指令之后会接着执行`<init>()`方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

参考:https://blog.csdn.net/m0_51963973/article/details/131928560

一张图秒懂JVM中的对象创建过程_jvm bean new 流程 图-CSDN博客

JVM的双亲委派模型

得分点

三个默认类加载器、工作过程、作用

双亲委派模型:当一个类加载器接收到加载类的请求时,它首先会将这个请求委派给其父类加载器处理,只有在父类加载器无法完成加载任务时,才会由该类加载器自己去加载类。 

向上委托,向下加载

JVM三个默认类加载器:

   1、启动类加载器BootStrapClassLoader(最顶端):

           加载内容:负责加载java的核心类库,包括java.lang包中的类等。

                                底层使用C++实现(不会继承ClassLoader),是虚拟机自身的一部分。
           不能被直接引用:因为是C++实现的,所以无法被Java程序直接引用,

                                        只能加载委派过来的请求。这些类库存放在 JAVA_HOME\lib

                                        (具体解释看下文) 目录下,或者被 -Xbootclasspath 参数指定的
                        路径中。(启动类加载器主要加载java的核心类库,即加载lib目录下的所有class)

      2、扩展类加载器ExtClassLoader:
                 加载内容:负责加载JAVA_HOME\lib\ext目录中,或者被java.ext.dirs系统变量

                                所指定的路径中所有类库。
                  可以被直接引用:它可以直接用来加载类,也可以通过委派加载类。

                                                Ext是Extract缩写,译为扩展、提取。
        3、应用程序类加载器AppClassLoader(最低端):
                加载内容:负责加载类路径的所有类库,在大多数情况下,
                                        我们编写的 Java 程序都是由这个类加载器加载的。
                可以被直接引用:可以直接在代码中使用这个类加载器。          

双亲委派模型的工作过程:

工作过程:

1、检查父类加载器是否已经加载过这个类:JVM 会首先询问父类加载器是否已经加载了该类。如果已经加载过了,直接返回该类的 Class 对象。如果没加载过,则:

2、委派给父类加载器加载:如果父类加载器没有加载过该类,那么 JVM 将委托给父类加载器进行加载。每一层都是这样继续委派,直到达到最顶层的启动类加载器。

3、尝试加载类:如果父类加载器无法加载该类(即所有的父类加载器都无法加载),那么 JVM 将尝试使用自己的类加载器来加载类。

实际流程:

JVM在加载一个类时,会调用应用程序类加载器的loadClass()方法来加载这个类,不过在这方法中,会先使用扩展类加载器的loadClass()方法来加载类,同样扩展类加载器的loadClass()方法中会先使用启动类加载器来加载类;

如果启动类加载器加载到了就直接成功,如果启动类加载器没有加载到,那扩展类加载器就会自己尝试加载该类,如果没有加载到,那么则会由应用程序类加载器来加载这个类。 

双亲委派模型的作用:

避免类的重复加载:无论哪一个类加载器要加载某类,最终都是委派最顶端的启动类加载器。
防止核心API被篡改:如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

JAVA_HOME\lib:

是 JDK(Java Development Kit)安装目录下的一个子目录,其中包含了 Java 核心类库,包括一些 Java 的基础类和工具类等。这些类库是 Java 编程语言的基础,为 Java 程序的运行提供了必要的支持。

一些常见的在 JAVA_HOME\lib 目录下的重要文件包括:

  • rt.jar:Java 运行时的核心库,包含了 Java 核心类库的大部分内容,如 java.lang、java.util 等。
  • charsets.jar:包含了字符集支持的类库。
  • jfxrt.jar:JavaFX 运行时的核心库。
  • tools.jar:包含了一些 Java 开发工具的类库,如编译器、调试器等。
  • dt.jar:包含了 Java 开发工具包的类库,如图形界面工具等。


在编译和运行 Java 程序时,这些类库会被 JVM 的 Bootstrap ClassLoader 加载,以便程序能够使用 Java 核心类库提供的功能。 

JAVA_HOME\lib\ext:

是 JDK(Java Development Kit)安装目录下的一个子目录,用于存放 Java 的扩展类库。这些类库提供了一些 Java 平台的扩展功能,如 XML 解析、网络协议、加密解密等。

在 JAVA_HOME\lib\ext 目录下,通常会包含一些 JAR 文件,这些文件是扩展类库的实现。一些常见的扩展类库包括:

  • dnsns.jar:DNS 名称服务提供者实现。
  • jaccess.jar:Java 访问桥实现。
  • ldapsec.jar:LDAP 安全实现。
  • sunjce_provider.jar:Sun 的 JCE(Java Cryptography Extension)提供者实现。
  • sunpkcs11.jar:Sun 的 PKCS#11 提供者实现。

这些扩展类库提供了一些 Java 平台的高级功能,但并不是所有的 Java 运行时环境都会使用到。通常情况下,如果你需要使用这些扩展功能,你可以将相应的 JAR 文件添加到类路径中,以便 Java 程序能够访问到这些功能。

                       


                 

类路径:

classpath:类路径classpath是编译之后的target文件夹下的WEB-INF/class文件夹。内容等同于打包前的src.main.java和src.main.resource下的目录和文件classpath* :不仅包含class路径,还包括jar文件中(class路径)进行查找. 

      

对于JDK8及其之前版本的Java应用,都会使用到以下3个系统提供的类加载器来进行加载:

1.启动类加载器

这个类加载器负责加载存放在`<java_home>\lib`目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。注意,Java虚拟机会按照文件名识别类库,例如rt.jar、tools.jar,对于名字不符合的类库即使放在lib目录中也不会被加载。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可,即让java.lang.ClassLoader.getClassLoader()返回null。

2.扩展类加载器

这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载`<java_home>\lib\ext`目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。

3.应用程序类加载器

这个类加载器由sun.misc.Launcher$AppClassLoader来实现。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。

用户还可以加入自定义的类加载器来进行拓展,这些类加载器之间的协作关系“通常”如下图所示。图中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。

工作过程

双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。

作用:避免类的重复加载、防止核心API被篡改

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类

反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

加分回答-双亲委派模型的3次被破坏

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,双亲委派模型主要出现过3次较大规模的“被破坏”的情况。

1.双亲委派模型的第一次“被破坏”发生在双亲委派模型出现之前

双亲委派模型在JDK1.2之后才被引入,但是类加载器的概念和抽象类ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协。为了兼容这些已有代码,只能在之后的ClassLoader中添加一个protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。双亲委派的具体逻辑就实现在这里面,按照loadClass()的逻辑,如果父类加载失败,会自动调用自己的findClass()来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器符合双亲委派规则。

2.双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的

双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题,基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载,肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。

3.双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的

这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换、模块热部署等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。

早在2008年,在Java社区关于模块化规范的第一场战役里,由Sun/Oracle公司所提出的JSR-294、JSR-277规范提案就曾败给以IBM公司主导的JSR-291(即OSGi R4.2)提案。尽管Sun/Oracle并不甘心就此失去Java模块化的主导权,随即又再拿出Jigsaw项目迎战,但此时OSGi已经站稳脚跟,成为业界“事实上”的Java模块化标准。

OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。</java_home></java_home>

JVM中一次完整的GC流程是怎样的

堆分为哪几个区、GC流程、注意大对象和年龄15(JDK8)

  1. 首先,任何新对象都分配到 eden 空间。两个幸存者空间开始时都是空的。
  2. 当 eden 空间填满时,将触发一个Minor GC(年轻代的垃圾回收,也称为Young GC),删除所有未引用的对象,大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年代。
  3. 所有被引用的对象作为存活对象,将移动到第一个幸存者空间S0,并标记年龄为1,即经历过一次Minor GC。之后每经过一次Minor GC,年龄+1。GC分代年龄存储在对象头的Mark Word里。
  4. 当 eden 空间再次被填满时,会执行第二次Minor GC,将Eden和S0区中所有垃圾对象清除,并将存活对象复制到S1并年龄加1,此时S0变为空。
  5. 如此反复在S0和S1之间切换几次之后,还存活的年龄等于15的对象(JDK8默认15,JDK9默认7,-XX:InitialTenuringThreshold=7)在下一次Minor GC时将放到老年代中。 
  6. 如果老年代内存不足够存储新对象,则会执行Full GC(清空整个新生代和老年代)。
  7. 当老年代满了时会触发Full GC或者Major GC(老年代的垃圾回收,清理整个老年代空间)。具体是Full GC还是Major GC取决于用哪个垃圾回收器,传统垃圾回收器会Full GC,G1(优先使用Mixed GC,清空部分新生代和老年代)、ZGC(直接抛出内存溢出错误)等现代回收器会避免Full GC。

如何避免Full GC?


在触发以下条件时,JVM会执行Full GC, 将整个堆(包括整个新生代和老年代)内存清空。

触发条件:

  • 调用System.gc():调用 System.gc() (用于请求 JVM 执行垃圾回收)时,JVM 会建议执行 Full GC(具体是否执行取决于垃圾回收器实现的System.gc()逻辑,例如Parallel GC、CMS等传统垃圾回收器该方法会Full GC,而G1、ZGC等现代回收器不会Full GC)。

  • 空间分配担保失败:当对象从新生代晋升到老年代前,要检查老年代是否有足够空间容纳新对象,这叫做空间分配担保。如果空间不足,则空间分配担保失败,JVM会执行Full GC。

  • 老年代满了:当老年代满了时会触发Full GC或者Major GC(老年代的垃圾回收,清理整个老年代空间)。具体是Full GC还是Major GC取决于用哪个垃圾回收器,传统垃圾回收器会Full GC,G1(优先使用Mixed GC,清空部分新生代和老年代)、ZGC(直接抛出内存溢出错误)等现代回收器会避免Full GC。
  • 方法区满了:当方法区(Method Area)空间不足时,会触发Full GC。

避免Full GC:

Full GC 的执行时间最长,也是JVM调优的重点。

  • 调大老年代比例
  • 增加整个堆内存
  • 使用G1、ZGC等现代回收器。

JVM调优思路

JVM调优三步骤、性能监控、性能分析、性能调优

JVM调优三步骤:

  1. 监控发现问题
  2. 工具分析问题
  3. 性能调优 

监控发现问题:看服务器有没有以下情况,有的话需要调优:

  • GC频繁
  • CPU负载过高
  • OOM
  • 内存泄露
  • 死锁
  • 程序响应时间较长

工具分析问题:使用分析工具定位oom、内存泄漏等问题

  • 调优依据:吞吐量提高的代价是停顿时间拉长。如果应用程序跟用户基本不交互,就优先提升吞吐量。如果应用程序和用户频繁交互,就优先缩短停顿时间。
  • GC日志:使用GCViewer、VisualVM、GCeasy等日志分析工具打印GC日志;
  • JDK自带的命令行调优工具:
    • jps:查看正在运行的 Java 进程。jps -v查看进程启动时的JVM参数;
    • jstat:查看指定进程的 JVM 统计信息。jstat -gc查看堆各分区大小、YGC,FGC次数和时长。如果服务器没有 GUI 图形界面,只提供了纯文本控制台环境,它是运行期定位虚拟机性能问题的首选工具。
    • jinfo:实时查看和修改指定进程的 JVM 配置参数。jinfo -flag查看和修改具体参数。
    • jstack:打印指定进程此刻的线程快照。定位线程长时间停顿的原因,例如死锁、等待资源、阻塞。如果有死锁会打印线程的互相占用资源情况。
      • 线程快照:该进程内每条线程正在执行的方法堆栈的集合。
  • JDK自带的可视化监控工具:例如jconsole、Visual VM。Visual VM可以监视应用程序的 CPU、GC、堆、方法区、线程快照,查看JVM进程、JVM 参数、系统属性。
  • MAT:解析Heap Dump(堆转储)文件dump.hprof,查看GC Roots、引用链、对象信息、类信息、线程信息。可以快速生成内存泄漏报表。
  • MAT下载地址(JDK8对应1.10.0版本):Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation
  • 生成dump文件方式:
    • jmap
    • jmap -dump:live,format=b,file=heap_dump.hprof <你的PID>
  • JVM参数:OOM后生成、FGC前生成
  • Visual VM
  • MAT直接从Java进程导出dump文件

// 开启在出现 OOM 错误时生成堆转储文件
-Xmx1024m
-XX:+HeapDumpOnOutOfMemoryError
// 将生成的堆转储文件保存到 /tmp 目录下,并以进程 ID 和时间戳作为文件名
-XX:HeapDumpPath=/tmp/java_%p_%t.hprof
 
// 在进行 Full GC 前生成堆转储文件
// 注:如果没有开启自动 GC,则此参数无效。JDK 9 之后该参数已被删除。
-XX:+HeapDumpBeforeFullGC    

性能调优:

  • 排查大对象和内存泄漏:使用MAT分析堆转储日志中的大对象,看是否合理。大对象会直接进入老年代,导致Full GC频繁。具体排查步骤看下面OOM。
  • 调整JVM参数:主要关注停顿时间和吞吐量,两者不可兼得,提高吞吐量会拉长停顿时间。
    • 减少停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。 可以通过-XX:MaxGCPauseMillis参数进行设置,以毫秒为单位,至少大于1
    • 提高吞吐量:吞吐量=运行时长/(运行时长+GC时长)。通过-XX:GCTimeRatio=n参数进行设置,99的话代表吞吐量为99%, 一般吞吐量不能低于95%。吞吐量太高会拉长停顿时间,造成用户体验下降。
    • 调整堆内存大小:根据程序运行时老年代存活对象大小(记为x)进行调整,整个堆内存大小设置为X的3~4倍。年轻代占堆内存的3/8。
      • -Xms:初始堆内存大小。默认:物理内存小于192MB时,默认为物理内存的1/2;物理内存大192MB且小于128GB时,默认为物理内存的1/4;物理内存大于等于128GB时,都为32GB。
      • -Xmx:最大堆内存大小,建议保持和初始堆内存大小一样。因为从初始堆到最大堆的过程会有一定的性能开销,而且现在内存不是稀缺资源。
      • -Xmn:年轻代大小。JDK官方建议年轻代占整个堆大小空间的3/8左右。
  • 调整堆内存比例:调整伊甸园区和幸存区比例、新生代和老年代比例。Young GC频繁时,我们提高新生代比例和伊甸园区比例。默认情况,伊甸园区:S0:S1=8:1:1,新生代:老年代=1:2。
  • 调整升老年代年龄:JDK8时Young GC默认把15岁的对象移动到老年代。JDK9默认值改为7。当Full GC频繁时,我们提高升老年龄,让年轻代的对象多在年轻代待一会,从而降低Full GC频率。JDK8默认Young GC时将15岁的对象移动到老年代。
  • 调整大对象阈值:Young GC时大对象会不顾年龄直接移动到老年代。当Full GC频繁时,我们关闭或提高大对象阈值,让老年代更迟填满。默认是0,即大对象不会直接在YGC时移到老年代。
  • 调整GC的触发条件
    • CMS调整老年代触发回收比例:CMS的并发标记和并发清除阶段是用户线程和回收线程并发执行,如果老年代满了再回收会导致用户线程被强制暂停。所以我们修改回收条件为老年代的60%,保证回收时预留足够空间放新对象。CMS默认是老年代68%时触发回收机制。
    • G1调整存活阈值:超过存活阈值的Region,其内对象会被混合回收到老年代。G1回收时也要预留空间给新对象。存活阈值默认85%,即当一个内存区块中存活对象所占比例超过 85% 时,这些对象就会通过 Mixed GC 内存整理并晋升至老年代内存区域。
  • 选择合适的垃圾回收器:最有效的方式是升级,根据CPU核数,升级当前版本支持的最新回收器。
    • CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。
    • CPU多核,关注吞吐量 ,那么选择Parallel Scavenge+Parallel Old组合(JDK8默认)。
    • CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择ParNew+CMS,吞吐量降低但是低停顿。
    • CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。
  • 优化业务代码:绝大部分问题都出自代码。要尽量减少非必要对象的创建,防止死循环创建对象,防止内存泄漏,有些情景下需要以时间换空间,控制内存使用
  • 增加机器:增加机器,分散节点压力
  • 调整线程池参数:合理设置线程池线程数量
  • 缓存、MQ等中间件优化:使用中间件提高程序效率,比如缓存、消息队列等

JVM参数: 

//调整内存大小
-XX:MetaspaceSize=128m(元空间默认大小)
-XX:MaxMetaspaceSize=128m(元空间最大大小)
-Xms1024m(堆最大大小)
-Xmx1024m(堆默认大小)
-Xmn256m(新生代大小)
-Xss256k(栈最大深度大小)
 
//调整内存比例
 //伊甸园:幸存区
-XX:SurvivorRatio=8(伊甸园:幸存区=8:2)
 //新生代和老年代的占比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
 
//修改垃圾回收器
//设置Serial垃圾收集器(新生代)
//-XX:+UseSerialGC
 //设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
//-XX:+UseParallelOldGC
 //CMS垃圾收集器(老年代)
//-XX:+UseConcMarkSweepGC
 //设置G1垃圾收集器
-XX:+UseG1GC
 
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
 -XX:MaxGCPauseMillis
 
 //进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,JDK8默认值15,JDK9默认值7
 -XX:InitialTenuringThreshold=7
 //新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
  -XX:PretenureSizeThreshold=1000000
 
 //使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
 -XX:CMSInitiatingOccupancyFraction 
 //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
 -XX:G1MixedGCLiveThresholdPercent=65
 
 //Heap Dump(堆转储)文件
 //当发生OutOfMemoryError错误时,自动生成堆转储文件。
-XX:+HeapDumpOnOutOfMemoryError 
 //错误输出地址
-XX:HeapDumpPath=/Users/a123/IdeaProjects/java-test/logs/dump.hprof
 
 //GC日志
-XX:+PrintGCDetails(打印详细GC日志)
-XX:+PrintGCTimeStamps:打印GC时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps:打印GC时间戳(以日期格式)
-Xlog:gc:(打印gc日志地址)

项目中实际的JVM调优经验

 CPU飙升 


原因:CPU利用率过高,大量线程并发执行任务导致CPU飙升。例如锁等待(例如CAS不断自旋)、多线程都陷入死循环、Redis被攻击、网站被攻击、文件IO、网络IO。

定位步骤: 

  1. 定位进程ID:通过top命令查看当前服务CPU使用最高的进程,获取到对应的pid(进程ID)
  2. 定位线程ID:使用top -Hp pid,显示指定进程下面的线程信息,找到消耗CPU最高的线程id
  3. 线程ID转十六进制:转十六进制是因为下一步jstack打印的线程快照(线程正在执行方法的堆栈集合)里线程id是十六进制。
  4. 定位代码:使用jstack pid | grep tid(十六进制),打印线程快照,找到线程执行的代码。一般如果有死锁的话就会显示线程互相占用情况。
  5. 解决问题:优化代码、增加系统资源(增多服务器、增大内存)。

 GC调优
                        

最差情况下能接受的GC频率:Young GC频率10s一次,每次500ms以内。Full GC频率10min一次,每次1s以内。

其实一小时一次Full GC已经算频繁了,一个不错的应用起码得控制一天一次Full GC。

监控发现问题:上午8点是我们的业务高峰,一到高峰的时候,用户感觉到明显卡顿,监控工具(例如Prometheus和Grafana)发现TP99(99%请求在多少ms内完成)时长明显变高,有明显的的毛刺;内存使用率也不稳定,会周期性增大再降低,于是怀疑是GC导致。

命令行分析问题:通过jstat -gc观察服务器的GC情况,发现Young GC频率提高成原来的10倍,Full GC频率提高成原来的四倍。正常YGC 10min一次,FGC 10h一次。异常YGC 1min一次,FGC 3h一次;

所以主要问题是Young GC频繁,进而导致Full GC频繁。Full GC频繁会触发STW,导致TP99耗时上升。

解决方案:

  • 排查内存泄漏、大对象、BUG;
  • 增大堆内存:服务器加8G内存条,同时提高初始堆内存、最大堆内存。-Xms、-Xmx。
  • 提高新生代比例:新生代和老年代默认比例是1:2。-XX:NewRatio=由4改为默认的2
  • 降低升老年龄:让存活对象更快进入老年代。-XX:InitialTenuringThreshold=15(JDK8默认)改成7(JDK9默认)
  • 设置大对象阈值:让大于1M的大对象直接进入老年代。-XX:PretenureSizeThreshold=0(默认)改为1000000(单位是字节)
  • 垃圾回收器升级为G1:因为是JDK8,所以直接由默认的Parallel Scavenge+Parallel Old组合,升级为低延时的G1回收器。如果是JDK7版本,不支持G1,可以修改成ParNew+CMS或Parallel Scavenge+CMS,以降低吞吐量为代价降低停顿时间。-XX:CMSInitiatingOccupancyFraction
  • 降低G1的存活阈值:超过存活阈值的Region,其内对象会被混合回收到老年代。降低存活阈值,更早进入老年代。-XX:G1MixedGCLiveThresholdPercent=90设为默认的85
  • 调优效果:调优后我们重新进行了一次压测,发现TP99耗时较之前降低60%。FullGC耗时降低80%,YoungGC次数减少30%。TP99耗时基本持平,完全符台预期。

JDK自带的命令行工具,如:jstack,jstat,jmap,jps

Arthas诊断工具中的命令

# 下载`arthas-boot.jar`这种也是官方推荐的方式
        curl -O https://arthas.aliyun.com/arthas-boot.jar
# 启动arthas-boot.jar,必须启动至少一个 java程序,否则会自动退出。运行此命令会自动发现 java进程,输入需要 attach 进程对应的序列号,例如,输入1按回车则会监听该进程。
        java -jar arthas-boot.jar
# 比如输入JVM (jvm实时运行状态,内存使用情况等)
# 仪表盘命令,通过上面我们可以发现线程ID为29也即是线程名称为“cpu demo thread”占用的cpu较高
dashboard
# 当前最忙的前N个线程 thread -b, ##找出当前阻塞其他线程的线程 thread -n 5 -i 1000 #间隔一定时间后展示,本例中可以看到最忙CPU线程为id=45,代码行数为19
thread -n 5 

# jad查看反编译的代码
jad cn.itxs.controller.CpuController
# 运行arthas,查看线程
thread
# 查看阻塞线程
thread -b
# jad反编译查看代码
jad --source-only cn.itxs.controller.ThreadController

Java线上问题排查神器Arthas实战分析-阿里云开发者社区

------类、方法冲突、class文件、classloader继承等 ------

`thread`-----查看当前jvm线程堆栈信息

`sc`---------查看jvm已加载的类信息

`dump`-------dump已加载类的字节码到特定目录

`jad`--------反编译已加载类的源码

`classloader`---查看继承树,类加载信息

------查看方法执行参数、异常、返回值、耗时、调用路径等 ------

`monitor`-----方法执行监控

`watch`-------方法执行数据监控 参数 返回值等

`trace`-------方法内部调用路径,并输出方法路径上每个节点上耗时

`stack`-------输出当前方法被调用的调用路径

`tt`----------方法执行数据的时空隧道,就是可以回溯指定方法每次调用不同时间下的入参返回值


链接:https://juejin.cn/post/6980364237734412296

说说内存溢出

得分点

内存溢出、溢出原因、解决方案、mat定位

内存溢出: 申请的内存大于系统能提供的内存。

溢出原因:

  • 本地直接内存溢出:本地直接内存设的太小导致溢出。设置直接内存最大值-XX:MaxDirectMemorySize,若不指定则默认与Java堆最大值一致。

  • 虚拟机栈和本地方法栈溢出:如果虚拟机的栈内存允许动态扩展,并且方法递归层数太深时,导致扩展栈容量时无法申请到足够内存。

  • 方法区溢出:运行时生成大量动态类时会内存溢出。

    • CGlib动态代理:CGlib动态代理产生大量类填满了整个方法区(方法区存常量池、类信息、方法信息),直到溢出。CGlib动态代理是在内存中构建子类对象实现对目标对象功能扩展,如果enhancer.setUseCache(false);,即关闭用户缓存,那么每次创建代理对象都是一个新的实例,创建过多就会导致方法区溢出。注意JDK动态代理不会导致方法区溢出。

    • JSP:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)。

  • 堆溢出:
  • 死循环创建过多对象;
  • 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

               内存中加载的数据量过于庞大,如一次从数据库取出的数据集太大、第三方接口接口传                 输的大对象、接收的MQ消息太大;

  • Tomcat参数设置不当导致OOM:Tomcat会给每个线程创建两个默认4M大小的缓冲区,高并发情况下会导致缓冲区创建过多,导致OOM。
  • 程序计数器不会内存溢出。

         

使用JDK自带的命令行调优工具 ,判断是否有OOM:

  1. 使用jsp命令查看当前Java进程;
  2. 使用jstat命令多次统计GC,比较GC时长占运行时长的比例;
  3. 如果比例超过20%,就代表堆压力已经很大了;
  4. 如果比例超过98%,说明这段时期内几乎一直在GC,堆里几乎没有可用空间,随时都可能抛出 OOM 异常。

MAT定位导致OOM:示例代码:写死循环创建对象,不断添加到list里,导致堆内存溢出;

1、导出dump文件。
        方法一:OOM时自动导出:JVM参数设置,内存溢出后生成dump文件,

                   设置路径;- XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath。
        方法二:mat选择线程后手动导出:

        
        方法三:jmap命令导出:
        # 1.查看进程号
                jps
         # 2.根据进程号导出
                jmap -dump:format=b,file=D:\heapdump.hprof <pid>
2、MAT解析dump文件;
3、定位大对象:点击直方图图标(Histogram),对象会按内存大小排序,查看内存占用最大的对象;右键 “List Objects” -> “with outgoing references”,找到特定实例,选择 “Path to GC Roots” -> “Exclude all phantom/weak/soft etc. references”。这将显示从垃圾收集根(GC Roots)到该对象的引用路径。找到路径上的类,分析源代码


4、这个对象被谁引用:点击支配树(dominator tree),看大对象被哪个线程调用。这里可以看到是被主线程调用。


5、定位具体代码:点击概述图标(thread_overview),看线程的方法调用链和堆栈信息,查看大对象所属类和第几行,定位到具体代码,解决问题。

解决方案:

  1. 通过jinfo命令查看并修改JVM参数,直接增加内存。如-Xmx256m
  2. 检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
  3. 对代码进行走查和分析,找出可能发生内存溢出的位置。
  4. 使用内存查看工具动态查看内存使用情况。

标准答案


   

内存溢出,简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出

引起内存溢出的原因有很多种,常见的有以下几种:

        1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

        2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

        3. 代码中存在死循环或循环产生过多重复的对象实体;

        4. 使用的第三方软件中的BUG;

        5. 启动参数内存值设定的过小。

加分回答

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OOM异常的可能。

1. Java堆溢出

Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

2. 虚拟机栈和本地方法栈溢出

HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

3. 方法区和运行时常量池溢出

方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。这类场景常见的包括:程序使用了CGLib字节码增强和动态语言、大量JSP或动态产生JSP文件的应用、基于OSGi的应用等。 在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,即常量池是方法区的一部分,所以上述问题在常量池中也同样会出现。而HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代,所以上述问题在JDK 8中会得到避免。

4. 本地直接内存溢出

直接内存的容量大小可通过`-XX:MaxDirectMemorySize`参数来指定,如果不去指定,则默认与Java堆最大值一致。如果直接通过反射获取Unsafe实例进行内存分配,并超出了上述的限制时,将会引发OOM异常。

                    

              

内存泄漏

得分点

内存泄漏、内存泄露的9种情况、性能分析工具判断是否有内存泄漏、解决办法

内存泄漏: 不再使用的对象仍然被引用,导致GC无法回收;

内存泄露的9种情况:

  1. 静态容器里的对象:静态集合类的生命周期与 JVM 程序一致,容器里的对象引用也将一直被引用得不到GC;Java里不准静态方法引用非静态方法也是防止内存泄漏。

  2. 单例对象引用的外部对象:单例模式里,如果单例对象如果持有外部对象的引用,因为单例对象不会被回收,那么这个外部对象也不会被回收

  3. 外部类跟随内部类被引用:内部类持有外部类,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
  4. 数据库、网络、IO等连接忘记关闭:在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。如果对 Connection、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

  5. 变量作用域不合理:例如一个变量只会在某个方法中使用,却声明为成员变量,并且被使用后没有被赋值为null,将会导致这个变量明明已经没用了,生命周期却还跟对象一致。

  6. HashSet中对象改变哈希值:当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则对象哈希值改变,找不到对应的value。

  7. 缓存引用忘删除:一旦你把对象引用放入到缓存中,他就很容易遗忘,缓存忘了删除,将导致引用一直存在。

  8. 逻辑删除而不是真实删除:监听器和其他回调:如果客户端在你实现的 API 中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为 软WeakHashMap 中的键。例如出栈只是移动了指针,而没有将出栈的位置赋值null,导致已出栈的位置还存在引用。

  9. 线程池时,ThreadLocal忘记remove():使用线程池的时候,ThreadLocal 需要在使用完线程中的线程变量手动 remove(),否则会内存泄漏。因为线程执行完后没有销毁而是被线程池回收,导致ThreadLocal中的对象不能被自动垃圾回收。 

性能分析工具判断是否有内存泄漏:

  • JDK自带的命令行调优工具:
    • 每隔一段较长的时间通过jstat命令采样多组 OU(老年代内存量) 的最小值;
    • 如果这些最小值在上涨,说明无法回收对象在不断增加,可能是内存泄漏导致的。

MAT监视诊断内存泄漏:

  • 生成堆转储文件:MAT直接从Java进程导出dump文件
  • 可疑点:查看泄漏怀疑(Leak Suspects),找到内存泄漏可疑点
  • 可疑线程:可疑点查看详情(Details),找到可疑线程
  • 定位代码:查看线程调用栈(See stacktrace),找到问题代码的具体位置。

GC详细日志:启动参数开启GC详细日志,设置日志地址;-XX:+PrintGCDetails;

编译器警告:查看Eclipse等编译器的内存泄漏警告;

Java基准测试工具:分析代码性能; 

解决办法:

  1. 牢记内存泄漏的场景,当一个对象不会被使用时,给它的所有引用赋值null,堤防静态容器,记得关闭连接、别用逻辑删除,只要用到了引用,变量的作用域要合理。
  2. 使用java.lang.ref包的弱引用WeakReference,下次垃圾收集器工作时被回收。
  3. 检查代码;

内存泄漏,是指不再使用的对象仍然被引用,导致垃圾收集器无法回收它们的内存。由于不再使用的对象仍然无法清理,甚至这种情况可能会越积越多,最终导致致命的OutOfMemoryError

可以按照如下的思路来分析和解决内存泄漏问题

1. 启用分析器

Java分析器是通过应用程序监视和诊断内存泄漏的工具,它可以分析我们的应用程序内部发生的事情,例如如何分配内存。使用分析器,我们可以比较不同的方法并找到可以最佳利用资源的方式。

2. 启用详细垃圾收集日志

通过启用详细垃圾收集日志,我们可以跟踪GC的详细进度。要启用该功能,我们需要将以下内容添加到JVM的配置当中:`-verbose:gc`。通过这个参数,我们可以看到GC内部发生的细节。

3. 使用引用对象

我们还可以借助java.lang.ref包内置的Java引用对象来规避问题,使用java.lang.ref包,而不是直接引用对象,即使用对象的特殊引用,使得它们可以轻松地被垃圾收集。

4. Eclipse内存泄漏警告

对于JDK1.5以及更高的版本中,Eclipse会在遇到明显的内存泄漏情况时显示警告和错误。因此,在Eclipse中开发时,我们可以定期地访问“问题”选项卡,并更加警惕内存泄漏警告。

5. 基准测试

我们可以通过执行基准测试来衡量和分析Java代码的性能。通过这种方式,我们可以比较执行相同任务的替代方法的性能。这可以帮助我们选择更好的方法,并可以帮助我们节约内存。

6. 代码审查

最后,我们总是采用经典的老方式来进行简单的代码演练。在某些情况下,即使这种看似微不足道的方法也有助于消除一些常见的内存泄漏问题。

加分回答-没有一刀切的解决方案,具体问题具体分析

通俗地说,我们可以将内存泄漏视为一种疾病,它通过阻塞重要的内存资源来降低应用程序的性能。和所有其他疾病一样,如果不治愈,随着时间的推移,它可能导致致命的应用程序崩溃。内存泄漏很难解决,找到它们需要对Java语言有很深的理解并掌握复杂的命令。在处理内存泄漏时,没有一刀切的解决方案,因为泄漏可能通过各种不同的事件发生。 但是,如果我们采用最佳实践并定期执行严格的代码演练和分析,那么我们就可以将应用程序中内存泄漏的风险降到最低。

JVM的垃圾回收机制


得分点

新生代收集、老年代收集、混合收集、整堆收集

依据分代假说理论,垃圾回收可以分为: 新生代收集、老年代收集、混合收集、整堆收集

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则。而分代收集理论,建立在如下三个分代假说之上,即弱分代假说、强分代假说、跨代引用假说。依据分代假说理论,垃圾回收可以分为如下几类:

1. 新生代收集:目标为新生代的垃圾收集。

2. 老年代收集:目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为。

3. 混合收集:目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。

4. 整堆收集:目标为整个堆和方法区的垃圾收集。

加分回答-垃圾收集器

HotSpot虚拟机内置了很多垃圾收集器,其中针对新生代的垃圾收集器有Serial、ParNew、Parallel Scavenge,针对老年代的垃圾收集器有CMS、Serial Old、Parallel Old。此外,HotSpot还内置了面向整堆的G1收集器。

在上述收集器中,常见的组合方式有:

1. Serial + Serial Old,是客户端模式下常用的收集器。

2. ParNew + CMS,是服务端模式下常用的收集器。

3. Parallel Scavenge + Parallel Old,适用于后台运算而不需要太多交互的分析任务。
 

GC的可达性分析算法

得分点
概念、GC Roots、引用链、非可达对象两次标记

可达性分析算法:

以根对象集合(GC Roots)的每个跟对象为起始点,根据引用关系向下搜索,将所有与GC Roots直接或间接有引用关系的对象在对象头的Mark Word里标记为可达对象,即不需要回收的有引用关系对象。搜索过程所走过的路径称为“引用链” 。

GC Roots:即GC根节点集合,是一组必须活跃的引用。可作为GC Roots的对象:

  • 栈引用的对象:Java方法栈、本地方法栈中的参数引用、局部变量引用、临时变量引用等。临时变量是方法里的中间操作结果。
  • 方法区中常量、静态变量引用的对象
  • 所有被同步锁持有的对象
  • 所有线程对象
  • 所有跨代引用对象;
  • JVM内部的引用:如基本数据类型对应的Class对象,常驻的异常对象,以及应用程序类类加载器; 

非可达对象被回收需要两次标记:

1、第一次标记后筛选非可达对象:第一次被标记后,会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,也就是是否有机会自救。假如对象没有覆盖或者已被JVM调用过finalize()方法,也就是说不想自救或已自救过,那么此对象需要被回收;假如对象覆盖并没被JVM调用过finalize()方法,该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。
2、第二次标记F-Queue里的未自救对象:稍后,收集器将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个引用类型的类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的F-Queue。如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
finalize()方法: 

finalize()方法是对象逃脱死亡命运的最后一次机会,需要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。

另外,finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。

当前主流的商用程序语言的内存管理子系统,都是通过可达性分析算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

GC Roots 到底是什么东西呢,哪些对象可以作为 GC Root 呢?

 是一组必须活跃的引用。在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

在虚拟机栈中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量;
在方法区中常量引用的对象,譬如字符串常量池里的引用;
在本地方法栈中引用的对象;
JVM内部的引用,如基本数据类型对应的Class对象,常驻的异常对象,以及系统类加载器;
所有被同步锁持有的对象;
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

加分回答-宣告对象死亡要经历两次标记


真正宣告一个对象死亡,至少要经历两次标记过程:
1. 第一次标记
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。反之,该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。
2. 第二次标记
稍后,收集器将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合。如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
    finalize()方法是对象逃脱死亡命运的最后一次机会,需要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。另外,finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。


                        
原文链接:https://blog.csdn.net/qq_40991313/article/details/130232389

待完善

参考:
原文链接:https://blog.csdn.net/qq_40991313/article/details/130232389

相关文章:

  • el-input 不可编辑,但是点击的时候出现弹窗/或其他操作面板,并且带可清除按钮
  • C++特性——RAII、智能指针
  • 在VMware上部署【Ubuntu】
  • Linux信号的处理
  • vue学习八
  • Apache Tomcat漏洞公开发布仅30小时后即遭利用
  • 技术进阶:Open WebUI与Ollama的跨平台整合秘籍
  • 一、人工智能开发入门
  • 贪心算法(9)(java)最优除法
  • Apache Paimon 在抖音集团多场景中的优化实践
  • 昆仑万维开源R1V:38B参数多模态推理模型开启AI新纪元
  • 网络编程中的黏包和半包问题
  • [AI]实现简易AI Agent — — Transformers库
  • HTTP+DNS综合实验
  • Java面试黄金宝典3
  • 链表操作:分区与回文判断
  • (超详细) ETL工具之Kettle
  • ai应用开发代码
  • QT日志级别设置
  • xlsx.utils.json_to_sheet函数详解
  • 人民论坛:是民生小事,也是融合大势
  • 王宝强谈《棋士》:饰演这个灰度人物有一种被割裂的痛苦
  • 海关总署牵头部署开展跨境贸易便利化专项行动
  • 乌代表团与美特使在伦敦举行会谈,双方同意继续对话
  • 宁德时代与广汽等五车企发布10款巧克力换电新车型:年内将完成30城1000站计划
  • 民生访谈|让餐饮店选址合规性可查、社区妙趣横生,上海有实招