Java虚拟机

image-20240923104114307

1.JVM由那些部分组成,运行流程是什么

  • 类加载器:用于装载字节码文件(.class文件)
  • 运行时数据区:用于分配存储空间
  • 执行引擎:执行字节码文件或本地方法
  • 本地库接口:本地方法

运行流程:

(1)类加载器(ClassLoader)用于装载字节码文件(.class文件)

(2)运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行

(3)执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。

2. 什么是程序计数器?

程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。

javap -verbose xx.class 打印堆栈大小,局部变量的数量和方法的参数。

image-20230506094602329

3. Java堆介绍一下

线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

image-20230506094803545

  • 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
  • 老年代主要保存生命周期长的对象,一般是一些老的对象
  • 元空间保存的类信息、静态变量、常量、编译后的代码

为了避免方法区出现OOM,所以在java8中将堆上的方法区【永久代】给移动到了本地内存上,重新开辟了一块空间,叫做元空间。那么现在就可以避免掉OOM的出现了。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

image-20230506094938843

4. 什么是虚拟机栈

Java Virtual machine Stacks (java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈,先进后出

  • 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

  1. 垃圾回收是否涉及栈内存?

    垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放

  2. 栈内存分配越大越好吗?

    未必,默认的栈内存通常为1024k

    栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半

  3. 方法内的局部变量是否线程安全?

    • 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的

    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

    • 比如以下代码:

image-20230506095306061

  1. 什么时候导致栈内存溢出?
    • 栈帧过多导致栈内存溢出,典型问题:递归调用
    • 栈帧过大导致栈内存溢出(一般不会出现)

image-20240926135644406

5. 解释一下方法区(元空间)?

  • 方法区(Method Area)是各个线程共享的内存区域

  • 主要存储类的信息、运行时常量池

  • 虚拟机启动的时候创建,关闭虚拟机时释放

  • 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace

image-20230506095504213

6. 运行常量池

  • **静态常量池:**可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

    下图,左侧是main方法的指令信息,右侧constant pool 是常量池

    main方法按照指令执行的时候,需要到常量池中查表翻译找到具体的类和方法地址去执行

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • **运行常量池:**当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

image-20240926143015534

7. 直接内存

  • 并不属于JVM的内存结构,不受 JVM 内存回收管理,是操作系统的系统内存;

  • 常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理

这个是跟我们的JVM的直接内存是有一定关系,如下图,是传统阻塞IO的数据传输流程

image-20230506100548455

下图是NIO传输数据的流程,在这个里面主要使用到了一个直接内存,不需要在堆中开辟空间进行数据的拷贝,jvm可以直接操作直接内存,从而使数据读写传输更快。

image-20230506100621146

8. 什么是类加载器,类加载器有哪些?

  • JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。

  • 种类:

    • 启动类加载器(BootStrap ClassLoader):

      该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。

    • 扩展类加载器(ExtClassLoader):

      该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。

    • 应用类加载器(AppClassLoader):

      该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。

    • 自定义类加载器:

      开发者自定义类继承ClassLoader,实现自定义类加载规则。

上述三种类加载器的层次结构如下如下:

image-20230506100746624

类加载器的体系并不是“继承”体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。

9. 什么是双亲委派模型?

如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。

image-20230506100920042

  • 例如自己写的类:Student
    • 首先找到AppClassLoader进行加载,由于其有父类,则找到ExtClassLoader加载,而它也有父类,就让BootStrap ClassLoader加载,但是由于Student不在/jre/lib中所以,向下委托,也不再/jre/lib/ext中,所以最终委托到AppClassLoader,让其加载
  • 例如String类:
    • 首先找到AppClassLoader进行加载,由于其有父类,则找到ExtClassLoader加载,而它也有父类,就让BootStrap ClassLoader加载,由于String类在/jre/lib中,所以BoostStrap将String加载。

10. JVM为什么采用双亲委派机制

  1. 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。

想象一下,假设你有两个不同的类加载器:ClassLoaderAClassLoaderB。如果这两个加载器都尝试加载一个名为 MyClass 的类:

  • 没有双亲委派机制:

    • ClassLoaderA 加载了一个版本的 MyClass
    • ClassLoaderB 也尝试加载 MyClass,并加载了一个不同版本(由于A和B是平级,也没有继承关系,所有B不知道A已经加载了)。

    结果:

    • 你可能在同一个应用中有两个版本的 MyClass,当你调用它的方法时,可能得到不一致的结果,导致程序崩溃或逻辑错误。
  • 有双亲委派机制:

    • ClassLoaderA 请求加载 MyClass 时,它先询问其父加载器(假设是系统类加载器),父加载器已加载 MyClass,直接返回,ClassLoaderA 不再加载,保证了只存在一个版本。

(2)为了安全,保证类库API不会被修改

在工程中新建java.lang包,接着在该包下新建String类,并定义main函数

public class String {

    public static void main(String[] args) {

        System.out.println("demo info");
    }
}

​ 此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法

image-20220903144547378

出现该信息是因为由双亲委派的机制,java.lang.String的在启动类加载器(Bootstrap classLoader)得到加载,因为在核心jre库中有其相同名字的类文件,但该类中并没有main方法。这样就能防止恶意篡改核心API库。

11. 说一下类装载的执行过程

image-20240927113326044

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 加载:

    • 通过类的全名,获取类的二进制数据流,把这个类加载到内存中,加载完毕创建一个class对象

      image-20240927111850925

  • 验证:

    • 检查文件格式、语法、字节码是否规范,并且检查符号引用是否存在(即调用的这些方法或者类是否存在),或者验证文件中的信息是否符合虚拟机规范有没有安全隐患
  • 准备:

    • 负责为类的类变量(被static修饰的变量)分配内存,并设置默认初始化值

      image-20240927112217199

  • 解析:

    • 将类的二进制数据流中的符号引用替换为直接引用

      比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。

      image-20230506102311951

  • 初始化:

    • 对类的静态变量,静态代码块执行初始化操作
    • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
    • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
  • 使用:

    • 调用静态类成员信息(比如:静态字段、静态方法)

    • 使用new关键字为其创建对象实例

  • 卸载:

    • 当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存

12.对象什么时候可以被垃圾器回收

为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC(Garbage Collection)。

有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。

在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收时机

换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。

当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。

简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。

  • 引用计数法:

    一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收

    String demo = new String("123");
    

    image-20230506111102825

    String demo = null;
    

    image-20230506111136231

    当对象间出现了循环引用的话,则引用计数法就会失效

    image-20230506111255401

    先执行右侧代码的前4行代码

    image-20230506111327590

    目前上方的引用关系和计数都是没问题的,但是,如果代码继续往下执行,如下图

    image-20230506111512450

    • 无法解决循环引用问题,会引发内存泄露。(最大的缺点)
  • 可达性分析法:

    现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。

    会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。

    根对象是那些肯定不能当做垃圾回收的对象,就可以当做根对象

    局部变量,静态方法,静态变量,类信息

    核心是:判断某对象是否与根对象有直接或间接的引用,如果没有被引用,则可以当做垃圾回收

    image-20220904010634153

    ​ X,Y这两个节点是可回收的,但是并不会马上的被回收!! 对象中存在一个方法【finalize】。当对象被标记为可回收后,当发生GC时,首先会判断这个对象是否执行了finalize方法,如果这个方法还没有被执行的话,那么就会先来执行这个方法,接着在这个方法执行中,可以设置当前这个对象与GC ROOTS产生关联,那么这个方法执行完成之后,GC会再次判断对象是否可达,如果仍然不可达,则会进行回收,如果可达了,则不会进行回收。

    ​ finalize方法对于每一个对象来说,只会执行一次。如果第一次执行这个方法的时候,设置了当前对象与RC ROOTS关联,那么这一次不会进行回收。 那么等到这个对象第二次被标记为可回收时,那么该对象的finalize方法就不会再次执行了。

    GC ROOTS:

    • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象

      public class GCRootsExample {
          public static void main(String[] args) {
              Object obj = new Object();  // obj 是局部变量,保存在栈帧中
              // 只要这个方法还在执行,obj 就作为 GC Roots 的一部分
              System.out.println(obj);
          }
      }
      
      • 解释: 在上面的例子中,obj 是在 main 方法的局部变量表中声明的对象引用,只要 main 方法还未结束,obj 引用的对象就不会被回收,因为它是从栈中的本地变量表直接引用的对象,属于 GC Roots。
    • 方法区中类的静态属性引用的对象

      public class GCRootsExample {
          private static Object staticObj = new Object();  // staticObj 是静态变量
      
          public static void main(String[] args) {
              System.out.println(staticObj);
          }
      }
      
      • 解释staticObj 是一个类的静态变量,它存储在方法区中。只要 GCRootsExample 类没有被卸载,staticObj 引用的对象就会一直存在,作为 GC Roots。
    • 方法区中常量引用的对象

      public class GCRootsExample {
          public static void main(String[] args) {
              String str = "Hello, World";  // "Hello, World" 是字符串常量
              System.out.println(str);
          }
      }
      
      • 解释: 字符串 "Hello, World" 是放在字符串常量池中的,它的引用也会作为 GC Roots 的一部分。字符串常量池在方法区中,池中的对象被全局引用,通常不会被回收。
    • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

      public class GCRootsExample {
          public native void nativeMethod();
      
          public static void main(String[] args) {
              GCRootsExample example = new GCRootsExample();
              example.nativeMethod();
          }
      }
      
      • 解释: 在调用本地方法时(如通过 JNI 调用的 C 或 C++ 代码),该方法可以持有对 Java 对象的引用,这些引用也作为 GC Roots。例如,在本地代码中如果有对 Java 对象的引用,该对象在 GC 时不会被回收。

13. 垃圾回收算法?

标记清除算法

标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除

1.根据可达性分析算法得出的垃圾进行标记

2.对这些标记为可回收的内容进行垃圾回收

image-20230506112047190

  • 优点:标记和清除速度比较快
  • 缺点:碎片化严重,内存不连贯
标记整理算法

​ 标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题。

image-20230506111957793

1)标记垃圾。

2)需要清除向右边走,不需要清除的向左边走。

3)清除边界以外的垃圾。

优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。

复制算法

复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。

如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。

image-20230506111919008

优点:

  • 在垃圾对象多的情况下,效率较高
  • 清理后,内存无碎片

缺点:

  • 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低

image-20240927214401818

14. 分代收集算法

image-20240929165441909

在java8时,堆被分为了两份:新生代和老年代【1:2】,在java7时,还存在一个永久代。

image-20230506131229649

对于新生代,内部又被分为了三个区域。Eden区,S0(Survivor0)区,S1(Survivor1)区【8:1:1】

工作机制

  • 新创建的对象,都会先分配到eden区

    image-20230506131308654

  • 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象(假设A对象存活)

  • 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放

    image-20230506131415418

  • 经过一段时间后伊甸园的内存又出现不足,标记eden区域(假设1对象存活)to区存活的对象,将存活的对象复制到from区,然后清空Eden和to区

    image-20240929164907856

    image-20230506131442503

  • 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

    image-20230506131607645

MinorGC、 Mixed GC 、 FullGC的区别是什么

image-20230506131640893

  • MinorGC【young GC】发生在新生代的垃圾回收,暂停时间短(STW)

  • Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有

  • FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长(STW),应尽力避免

名词解释:

STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成

15. 说一下 JVM 有哪些垃圾回收器?

在jvm中,实现了多种垃圾收集器,包括:

  • 串行垃圾收集器

  • 并行垃圾收集器

  • CMS(并发)垃圾收集器

  • G1垃圾收集器

串行垃圾收集器

Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑

  • Serial 作用于新生代,采用复制算法

  • Serial Old 作用于老年代,采用标记-整理算法

垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。

image-20230506154006266

并行垃圾收集器

Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器

  • Parallel New作用于新生代,采用复制算法

  • Parallel Old作用于老年代,采用标记-整理算法

垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。

image-20230506154042673

CMS(并发)垃圾收集器

CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。

**初始标记:**标记GC Roots关联的节点,比如A

**并发标记:**继续向下探索,比如BCD

**重新标记:**再重新检查还有没有和GC Roots有间接关联的对象了

image-20230506154117857

image-20230506154107944

16. 详细聊一下G1垃圾回收器

3.6.1 概述
  • 应用于新生代和老年代,JDK9之后默认使用****G1

  • 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备

  • 采用复制算法

  • 响应时间与吞吐量兼顾

  • 分成三个阶段:新生代回收、并发标记、混合收集

  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

image-20230506154323950

3.6.2 Young Collection(年轻代垃圾回收)
  • 初始时,所有区域都处于空闲状态

    image-20230506154542687

  • 创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象

    image-20230506154607558

  • 当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程

    image-20230506154633118

    image-20230506154705088

  • 随着时间流逝,伊甸园的内存又有不足

  • 将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代

    image-20230506154759809

    image-20230506154826981

    image-20230506154859985

3.6.3 Young Collection + Concurrent Mark (年轻代垃圾回收+并发标记)

当老年代占用内存超过阈值(默认是45%)后,触发并发标记,这时无需暂停用户线程

image-20230506155000503

  • 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。

  • 这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。

    image-20230506155047765

3.6.4 Mixed Collection (混合垃圾回收)

复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集

image-20230506155116267

其中H叫做巨型对象,如果对象非常大,会开辟一块连续的空间存储巨型对象

image-20230506155146370

17. 强引用、软引用、弱引用、虚引用的区别?

1. 强引用(Strong Reference)

定义:

强引用是最普通、最常见的引用方式,默认情况下所有新建的对象都是强引用。强引用指向的对象在 JVM 垃圾回收时绝对不会被回收,除非手动解除引用或将引用设置为 null。即使内存不足,JVM 也不会回收有强引用指向的对象。

特点:
  • 只要某个对象有强引用存在,垃圾回收器就永远不会回收该对象。
  • 这是 Java 中最常见的引用方式,平常定义对象时创建的都是强引用。
适用场景:

大部分情况下,当我们需要确保对象始终存活且不会被回收时,使用强引用。比如核心数据结构的引用。

示例:
public class StrongReferenceExample {
    public static void main(String[] args) {
        Object strongRef = new Object(); // 创建强引用
        System.out.println("Strong reference: " + strongRef); // 强引用指向的对象不会被垃圾回收

        strongRef = null; // 手动解除强引用
        System.gc(); // 触发垃圾回收,此时对象可以被回收
    }
}

2. 软引用(Soft Reference)

定义:

软引用比强引用要弱一些。软引用指向的对象在内存充足时不会被回收,但在内存不足时,JVM 会回收这些对象。软引用常用于缓存,当内存充足时保留对象,当内存紧张时回收这些不重要的对象。

特点:
  • 在内存不足时,JVM 会回收软引用对象,避免内存溢出(OutOfMemoryError)。
  • 在垃圾回收前,只有当 JVM 确认内存不足时,才会回收软引用。
  • 软引用适合实现缓存,在系统内存压力大时清理缓存对象。
适用场景:

适用于缓存场景,比如当我们缓存数据时,允许在内存紧张时释放这些缓存以节省空间。

示例:
import java.lang.ref.SoftReference;

public class SoftReferenceExample {
    public static void main(String[] args) {
        Object obj = new Object(); // 创建一个对象
        SoftReference<Object> softRef = new SoftReference<>(obj); // 创建软引用

        System.out.println("Soft reference: " + softRef.get()); // 软引用能正常访问对象

        obj = null; // 清除强引用,软引用仍然引用该对象

        // 触发垃圾回收,只有在内存不足时,软引用会被回收
        System.gc();

        // 如果系统内存充足,软引用的对象不会被回收
        System.out.println("After GC: " + softRef.get()); 
    }
}

3. 弱引用(Weak Reference)

定义:

弱引用的引用强度比软引用更弱。当垃圾回收器发现某个对象只有弱引用指向它时,无论当前内存是否充足,都会立即回收该对象。弱引用通常用于那些不重要的、非必需的对象。

特点:
  • 垃圾回收器在任何时候都会回收只有弱引用的对象,不论系统内存是否紧张。
  • 通过弱引用获取的对象随时可能被回收,因此返回对象的 get() 方法可能返回 null
  • 常用于防止内存泄漏,比如在 WeakHashMap 中,键如果没有其他强引用,就会被回收。
适用场景:

适用于引用非必需对象,比如典型场景是 WeakHashMap,避免内存泄漏问题。

示例:
import java.lang.ref.WeakReference;

public class WeakReferenceExample {
    public static void main(String[] args) {
        Object obj = new Object(); // 创建一个对象
        WeakReference<Object> weakRef = new WeakReference<>(obj); // 创建弱引用

        System.out.println("Weak reference: " + weakRef.get()); // 弱引用能正常访问对象

        obj = null; // 清除强引用

        System.gc(); // 触发垃圾回收

        // 弱引用的对象将会被回收,因为只有弱引用指向它
        System.out.println("After GC: " + weakRef.get()); // 返回 null,说明对象已被回收
    }
}

4. 虚引用(Phantom Reference)

定义:

虚引用是最弱的一种引用类型,甚至比弱引用还要弱。虚引用的存在不会影响对象的生命周期,无法通过虚引用访问对象。它唯一的作用是在对象被回收时收到系统通知。虚引用通常和**引用队列(ReferenceQueue)**一起使用,当对象被回收时,虚引用会被加入到引用队列中,便于后续清理工作。

特点:
  • 虚引用的 get() 方法总是返回 null,无法通过虚引用获取对象。
  • 主要用于跟踪对象被垃圾回收的过程,可以用来做一些资源释放工作,比如清理本地内存或文件句柄。
  • 需要与 ReferenceQueue 配合使用,垃圾回收时虚引用会被加入到队列中,可以通过队列检测对象回收状态。
适用场景:

适用于监控对象的垃圾回收过程,比如在大型对象被回收后,做一些资源清理操作。

示例:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceExample {
    public static void main(String[] args) {
        Object obj = new Object(); // 创建一个对象
        ReferenceQueue<Object> refQueue = new ReferenceQueue<>(); // 创建引用队列
        PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue); // 创建虚引用

        System.out.println("Phantom reference: " + phantomRef.get()); // 永远返回 null

        obj = null; // 清除强引用

        System.gc(); // 触发垃圾回收

        // 检查虚引用是否已被放入引用队列中
        if (refQueue.poll() != null) {
            System.out.println("Object has been garbage collected and phantom reference is enqueued.");
        }
    }
}

18. JVM 调优的参数可以在哪里设置参数值?

4.1.1 tomcat的设置vm参数

修改TOMCAT_HOME/bin/catalina.sh文件,如下图

JAVA_OPTS="-Xms512m -Xmx1024m"

image-20220904151948778

4.1.2 springboot项目jar文件启动

通常在linux系统下直接加参数启动springboot项目

nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &

nohup : 用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行

参数 & :让命令在后台执行,终端退出后命令仍旧执行。

19. 用的 JVM 调优的参数都有哪些?

image-20241014151608179

image-20241014152539301

image-20241014152744794

image-20241014152808022

20. 说一下 JVM 调优的工具?

image-20241014154006109

4.3.1.1 jps(Java Process Status)

输出JVM中运行的进程状态信息(现在一般使用jconsole)

image-20220904104739581

4.3.1.2 jstack

查看java进程内线程的堆栈信息,其中pid就是利用jps查到的进程信息。

jstack [option] <pid>  

java案例

package com.heima.jvm;

public class Application {

    public static void main(String[] args) throws InterruptedException {
        while (true){
            Thread.sleep(1000);
            System.out.println("哈哈哈");
        }
    }
}

使用jstack查看进行堆栈运行信息

image-20220904111059602

4.3.1.3 jmap

用于生成堆转存快照

jmap [options] pid 内存映像信息

jmap -heap pid 显示Java堆的信息

jmap -dump:format=b,file=heap.hprof pid

​ format=b表示以hprof二进制格式转储Java堆的内存
​ file=用于指定快照dump文件的文件名。

例:显示了某一个java运行的堆信息

C:\Users\yuhon>jmap -heap 53280
Attaching to process ID 53280, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.321-b07

using thread-local object allocation.
Parallel GC with 8 thread(s)   //并行的垃圾回收器

Heap Configuration:  //堆配置
   MinHeapFreeRatio         = 0   //空闲堆空间的最小百分比
   MaxHeapFreeRatio         = 100  //空闲堆空间的最大百分比
   MaxHeapSize              = 8524922880 (8130.0MB) //堆空间允许的最大值
   NewSize                  = 178257920 (170.0MB) //新生代堆空间的默认值
   MaxNewSize               = 2841640960 (2710.0MB) //新生代堆空间允许的最大值
   OldSize                  = 356515840 (340.0MB) //老年代堆空间的默认值
   NewRatio                 = 2 //新生代与老年代的堆空间比值,表示新生代:老年代=1:2
   SurvivorRatio            = 8 //两个Survivor区和Eden区的堆空间比值为8,表示S0:S1:Eden=1:1:8
   MetaspaceSize            = 21807104 (20.796875MB) //元空间的默认值
   CompressedClassSpaceSize = 1073741824 (1024.0MB) //压缩类使用空间大小
   MaxMetaspaceSize         = 17592186044415 MB //元空间允许的最大值
   G1HeapRegionSize         = 0 (0.0MB)//在使用 G1 垃圾回收算法时,JVM 会将 Heap 空间分隔为若干个 Region,该参数用来指定每个 Region 空间的大小。

Heap Usage:
PS Young Generation
Eden Space: //Eden使用情况
   capacity = 134217728 (128.0MB)
   used     = 10737496 (10.240074157714844MB)
   free     = 123480232 (117.75992584228516MB)
   8.000057935714722% used
From Space: //Survivor-From 使用情况
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
To Space: //Survivor-To 使用情况
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
PS Old Generation  //老年代 使用情况
   capacity = 356515840 (340.0MB)
   used     = 0 (0.0MB)
   free     = 356515840 (340.0MB)
   0.0% used

3185 interned Strings occupying 261264 bytes.
4.3.1.4 jhat

用于分析jmap生成的堆转存快照(一般不推荐使用,而是使用Ecplise Memory Analyzer)

4.3.1.5 jstat

是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。

常见参数

①总结垃圾回收统计

jstat -gcutil pid

image-20220904114511854

字段含义
S0幸存1区当前使用比例
S1幸存2区当前使用比例
E伊甸园区使用比例
O老年代使用比例
M元数据区使用比例
CCS压缩使用比例
YGC年轻代垃圾回收次数
YGCT年轻代垃圾回收消耗时间
FGC老年代垃圾回收次数
FGCT老年代垃圾回收消耗时间
GCT垃圾回收消耗总时间

②垃圾回收统计

jstat -gc pid

image-20220904115157363

4.3.2.1 jconsole

用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具

打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行

image-20220904115936095

可以内存、线程、类等信息

image-20220904120057211

4.3.2.2 VisualVM:故障处理工具

能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈

打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行

image-20220904120356174

监控程序运行情况

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查看运行中的dump

image-20220904132134095

查看堆中的信息

image-20220904132346495

21.java内存泄露的排查思路?

原因:

如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量的时候,java虚拟机将抛出一个StackOverFlowError异常

如果java虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个OutOfMemoryError异常

如果一次加载的类太多,元空间内存不足,则会报OutOfMemoryError: Metaspace

image-20230506155704119

1、通过jmap指定打印他的内存快照 dump

有的情况是内存溢出之后程序则会直接中断,而jmap只能打印在运行中的程序,所以建议通过参数的方式的生成dump文件,配置如下:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/app/dumps/ 指定生成后文件的保存目录

2、通过工具, VisualVM(Ecplise MAT)去分析 dump文件

VisualVM可以加载离线的dump文件,如下图

文件–>装入—>选择dump文件即可查看堆快照信息

如果是linux系统中的程序,则需要把dump文件下载到本地(windows环境)下,打开VisualVM工具分析。VisualVM目前只支持在windows环境下运行可视化

image-20220904132925812

3、通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题

image-20220904133722905

4、找到对应的代码,通过阅读上下文的情况,进行修复即可

22. CPU飙高排查方案与思路?

1.使用top命令查看占用cpu的情况

image-20220904161818255

2.通过top命令查看后,可以查看是哪一个进程占用cpu较高,上图所示的进程为:30978

3.查看当前线程中的进程信息

ps H -eo pid,tid,%cpu | grep 40940

pid 进行id

tid 进程中的线程id

% cpu使用率

image-20220904162117022

4.通过上图分析,在进程30978中的线程30979占用cpu较高

注意:上述的线程id是一个十进制,我们需要把这个线程id转换为16进制才行,因为通常在日志中展示的都是16进制的线程id名称

转换方式:

在linux中执行命令

printf "%x\n" 30979

image-20220904162654928

5.可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号

执行命令

jstack 30978   此处是进程id

image-20220904162941977

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值