小蔡学Java

JVM虚拟机总结(二)

2023-06-25 17:15 1249 0 JVM / JUC JVMJava

二、垃圾回收(Garbage Collection)

1、垃圾判定

垃圾判定是指在编程中确定哪些内存中的对象是“垃圾”,即不再被应用程序使用的对象,因此可以被垃圾回收器回收的过程。

在Java中,垃圾回收(Garbage Collection, GC)主要采用两种基本方法:引用计数法可达性分析。下面分别对这两种方法进行说明:

(1)引用计数法(Reference Counting)

引用计数算法是一种最直观的垃圾收集技术。其基本思想是给每个对象分配一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。任何时刻计数器为0的对象就是不可能再被使用的,因此可以回收其占用的内存。

不过,Java并不采用引用计数法来进行垃圾回收,因为它存在循环引用的问题。在循环引用中,两个或多个对象相互引用,但它们可能都已经不再被其他活动部分的应用程序所引用。由于它们相互引用,因此它们的引用计数永远不会达到0,导致内存泄漏

public class ReferenceCounting {
    Object instance = null;

    public static void main(String[] args) {
        ReferenceCounting objA = new ReferenceCounting();
        ReferenceCounting objB = new ReferenceCounting();

        // 创建循环引用
        objA.instance = objB;
        objB.instance = objA;

        // 尝试手动置空以断开引用
        objA = null;
        objB = null;

        // 希望GC能回收objA和objB,但如果是采用引用计数法,则无法回收
        System.gc();
    }
}

(2)可达性分析(Reachability Analysis)

Java采用的是可达性分析算法来进行垃圾回收。在这种方法中,通过一系列的称为“GC Roots”的对象作为起点,然后向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达)时,则证明此对象是不可用的

在Java中,可作为GC Roots的常见对象包括:

虚拟机栈(栈帧中的局部变量表)中引用的对象:例如,正在执行的方法中的局部变量或参数

方法区中类静态属性引用的对象:这些静态变量所引用的对象也被称为 GC Roots。

方法区中常量引用的对象:例如,字符串常量池(String Table)的引用

本地方法栈中JNI(Java Native Interface)引用的对象:在使用 JNI 调用本地方法的过程中,会涉及到本地方法栈,其中引用的对象也是 GC Roots。

举一个简单的例子来描述可达性分析:

public class ReachabilityAnalysis {
    public static void main(String[] args) {
        ReachabilityAnalysis obj = new ReachabilityAnalysis(); // 对象obj是可达的,因为它被栈上的引用变量所引用

        // 现在让我们断开这个引用
        obj = null; // 此时对象不再可达

        // 垃圾回收可以执行了,它将使用可达性分析来确定obj的内存是否可以被释放
        System.gc();
    }
}

在JVM模型中,垃圾回收主要发生在堆内存(Heap)中,因为这里是存放对象实例的地方。当前主流的JVM使用分代垃圾收集算法,将堆内存分为年轻代(Young Generation)老年代(Old Generation),以及永久代(Permanent Generation,但在Java 8及之后被MetaSpace所替代)。不同代的对象会根据其生命周期的不同被相应的垃圾回收器回收,以提高回收效率。

垃圾回收算法、垃圾回收器的选择以及垃圾回收的时机,通常是由JVM自动管理的,但是开发者可以通过JVM参数来对其进行调优。

2、垃圾回收算法

(1)标记-清除

标记-清除算法分为两个阶段:标记阶段和清除阶段。

  • 标记阶段:从根对象(如活动线程的堆栈指针、静态对象等)开始,递归遍历所有可达的对象,并将它们标记为活动的。

  • 清除阶段:遍历堆内存中所有对象,对于没有被标记为活动的对象,释放其占用的内存空间。 缺点:

  • 整个过程中需要停止应用程序,导致停顿时间(STW,Stop-The-World)。

  • 会产生内存碎片

(2)标记-整理

标记-整理算法是标记-清除的改进版。在标记活动对象之后,它会将所有存活的对象移到内存的一端,然后清理掉端边界外的内存空间。

优点:

  • 解决了内存碎片问题,不需要复制活动对象。 缺点:

  • 需要移动存活对象,可能会造成较大的内存迁移开销

  • 需要较多的停顿时间,不适合对响应时间要求较高的应用。

(3)复制

复制算法将堆内存分为两半:一半用于分配内存,另一半处于空闲状态。在垃圾收集期间,它将所有存活的活动对象从当前的内存区域复制到另一半,接着清除原有的内存区域中的所有对象

优点:

  • 解决了内存碎片问题,适合存活对象较少场景。 缺点:

  • 不适用于处理存活较多对象的场景

  • 会占用双倍内存空间

3、Minor GC 和 Full GC 的区别

Minor GC:对新生代的垃圾回收

Full GC :对堆(新生代、老年代)和方法区(永久代/元空间)的垃圾回收

推荐参考:JVM中 Minor GC 和 Full GC 的区别

4、空间担保策略

空间担保策略是指当触发 minor gc 时,会判断老年代剩余最大连续空间大于历次Minor GC晋升的平均大小 或者 大于新生代所有对象的大小总和 , 大于任意一个,就允许触发MinorGC,反之触发 Full GC

推荐参考:深入理解JVM内存空间的担保策略

5、垃圾回收器

JDK 8 中默认的垃圾回收器组合为Parallel Scavenge(用于Young Generation)加上Parallel Old(用于Old Generation)。

推荐参考:Java中常用的垃圾回收器

三、类的加载过程

Java类加载主要分为三个阶段:加载链接初始化

推荐参考:深入理解Java类加载过程

加载

  • 通过类的全名,获取类的二进制数据流

  • 解析类的二进制数据流为方法区内的数据结构(Java类模型)

  • 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

验证(链接)

  • (1)文件格式验证

  • (2)元数据验证

  • (3)字节码验证(1.2.3)格式检查,如:文件格式是否错误、语法是否错误、字节码是否合规

  • (4)符号引用验证 Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,检查它们是否存在

准备(链接)

为类变量分配内存并设置类变量初始值

  • static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
  • static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成
  • static变量是final的引用类型,那么赋值也会在初始化阶段完成

解析(链接)

把类中的符号引用转换为直接引用

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

初始化

对类的静态变量,静态代码块执行初始化操作

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

使用

JVM 开始从入口方法开始执行用户的程序代码

  • 调用静态类成员信息(比如:静态字段、静态方法)
  • 使用new关键字为其创建对象实例

卸载

四、双亲委派机制

双亲委派是 Java 类加载器的一种机制。当一个类加载器收到加载类的请求时,它会首先将这个请求委派给父类加载器去完成。只有当父类加载器无法完成这个加载请求时,子类加载器才会尝试加载

1、双亲委派机制特点

  • 避免重复加载:由于双亲委派机制,如果一个类已经被某个类加载器加载过了,那么其他的类加载器就没有必要再加载一次,可以直接复用已经加载的类。这样可以避免类的重复加载,节省内存。
  • 安全性:通过双亲委派机制,核心类库会被由启动类加载器加载,因此可以防止核心类库被恶意篡改。另外,由于类加载器可以通过双亲委派机制追溯到启动类加载器,所以可以确保核心类库不会被自定义的类所替代,从而保证了系统安全性

2、如何打破双亲委派机制

要打破双亲委派机制,可以自定义类加载器,并重写 ClassLoader 类中的 loadClass(String name, boolean resolve) 方法(或者是 findClass(String name) 方法,根据具体需求)。自定义的类加载器可以先尝试加载类,而不是直接委派给父加载器。

下面是一个简化的示例,说明如何自定义类加载器以打破双亲委派模型:

public class CustomClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 首先, 检查请求的类是否已经被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 尝试自己加载类,而不是委派给父类加载器
                c = findClass(name);
            } catch (ClassNotFoundException e) {
                // 如果自己无法加载类,那么调用父类加载器尝试加载
                c = super.loadClass(name);
            }
        }
        return c;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 在这里加入具体的类加载逻辑,比如从文件系统中读取.class文件的字节流
        // byte[] classBytes = ...;
        // return defineClass(name, classBytes, 0, classBytes.length);
        // 示例中没有具体实现,因为它通常需要读取文件或其他数据源中的类数据
        throw new ClassNotFoundException();
    }
}

在这个例子中,findClass(String name) 方法被重写用于尝试加载类。如果在 findClass 中没有找到类,则会抛出ClassNotFoundException 异常,然后调用父类加载器尝试加载。

注意,直接破坏双亲委派机制可能会导致各种问题,如类冲突、安全问题等。因此,在实际开发中,只有在真正需要时才应该打破双亲委派模型,并且必须非常小心地实现。

自定义类加载器可以用在很多场景中,例如热部署(hot deploy)一个正在运行的应用程序,这通常需要动态地加载和卸载类。在框架开发中,比如OSGI、JSP的servlet容器等,这样的需求也是很常见的。

五、直接内存

直接内存是操作系统中分配的一块内存,不受JVM管理,Java代码可以直接获取直接内存中的数据

推荐参考:直接内存(Direct Memory)

六、JAVA中的四种引用

Java 提供了四种不同的引用类型:强引用(Strong Reference)软引用(Soft Reference)弱引用(Weak Reference)虚引用(Phantom Reference)

推荐参考:Java 中的四种引用类型和它们的使用场景

七、JVM常用调优参数

JVM提供了一些常用调优参数:-Xms-Xmx-Xmn

推荐参考:JVM常用调优参数

八、JVM调优

正常情况下,JVM 是不需要额外调优的。默认的配置通常适用于许多应用程序,因为 JVM 实现考虑了大量的使用情况,并经过了在不同场景下的测试和优化。 除非是系统有特殊的性能需求或者存在特定的瓶颈,一般来说,在生产环境中使用默认参数是合适的。然而,在一些特殊场景下,可以对 JVM 进行一些微调,以获取更好的性能或者更好的资源利用率。这通常需要仔细评估和测试,以确保调整后的参数能够有效地改善系统的性能。

1、JVM调优方法

JVM调优通常涉及到调整内存设置、选择合适的垃圾回收器以及优化JVM参数等方面。

  • 堆内存设置:通过调整堆(heap)大小,你可以控制Java应用可用的内存数量。堆内存过小可能导致频繁的垃圾回收,降低应用性能;过大则可能导致垃圾回收停顿时间过长。比如:设置-Xms和-Xmx来定义堆的 初始大小 最大大小
  • 选择垃圾回收器:根据应用的需求选择合适的垃圾回收器(GC)。不同的垃圾回收器,比如Parallel GC、CMS、G1 GC,有着不同的特点和适用场景。

2、OOM发生区域

JVM中出现OOM的区域通常有:

  • 堆内存(Heap Memory):如果堆内存太小,或者应用程序中有内存泄漏,都可能导致堆内存OOM。
  • 永久代/元空间(PermGen/Metaspace):存储Java类元数据的地方。如果加载了大量的类或者大量的动态生成类的情形,可能导致这部分内存溢出。
  • 方法栈:比如方法递归调用,可能会导致这个区域内存溢出。

3、常见分析工具

  • JConsole:Java监控和管理控制台,是Java Development Kit (JDK)的一部分,可以用来监控JAVA应用运行时的资源消耗。
  • JVisualVM:集成了多个JDK命令行工具的可视化工具,提供了内存和CPU分析功能。
  • Memory Analyzer Tool (MAT):用于分析堆转储,可以帮助你找出内存泄漏和查看内存消耗的对象
  • jmap:命令行工具,可以用来生成堆转储文件,分析内存使用情况

4、模拟OOM和分析示例

推荐参考:OOM日志分析

下面是一个简单的Java代码片段,用于模拟堆内存溢出。

import java.util.ArrayList;
import java.util.List;

public class GenerateOOM {
    static final int SIZE = 2 * 1024 * 1024;

    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true) {
            list.add(new Object[SIZE]);
        }
    }
}

运行这个程序,很快就会因为堆内存溢出而出现 OutOfMemoryError

首先使用 jps 查看进程ID,或者top查看内存较高进程ID,然后使用jmap来生成堆转储文件:

jmap -dump:live,format=b,file=heapdump.dat<PID> 1 使用MAT打开堆转储文件(heapdump.dat),MAT将会对文件进行分析,并提供内存使用的概览。

5、cpu飙高,用什么方式快速排查?

推荐参考:[linux系统cpu飙高如何排查]

  1. 使用 top 命令查看占用过高的进程ID(pid)
  2. 使用 top -H -p <进程ID> 查看这个进程里面哪个线程导致的
  3. 使用 printf "%x\n" [tid]将十进制的线程ID转换为十六进制的线程ID
  4. 使用 jstack <进程ID> | grep <16进制线程ID> -A 20命令打印线程日志

评论( 0 )

  • 博主 Mr Cai
  • 坐标 河南 信阳
  • 标签 Java、SpringBoot、消息中间件、Web、Code爱好者

文章目录