决战圣地玛丽乔亚Day39 -----GC、内存模型、类加载

内存模型:

java内存模型定义了JVM虚拟机如何与计算机的内存进行交互。java内存模型把内存划分为两部分:主内存和工作内存。主内存共享,工作内存线程私有。

java内存模型的实现有两种:基于锁的同步和volatile、 基于锁的同步和synchronized

线程私有变量可以通过ThreadLocal来创建,实现线程隔离。

内存同步性问题?涉及知识点:

Volatile、synchronized。

展开讲一讲volatile的内存屏障,总线嗅探等。

内存逃逸分析?

在编译阶段对程序进行分析,判断对象的生命周期是否逃逸出当前的方法或线程。如果没有逃逸就可以把它分配在栈或寄存器上。而不是扔在堆上

由于堆满了就要GC, 所以减轻了GC的压力。

放在栈和寄存器上,线程私有,避免锁的竞争和线程同步的问题。

 

GC(垃圾回收):

GC(垃圾回收)的过程:

堆的结构:

新生代:
Eden:Survivor0:Survivor1 = 8:1:1 (标记复制算法)

Eden区:新对象被创建时初始分配区

Survivor0、Survivor1:两个相同大小的区域,交替使用。

 

过程:

Minor GC

1.创建对象,放入Eden区,Eden区到达一个阈值的时候,触发Minor GC
第一次Minor GC:垃圾回收器标记清除不再被引用的对象,同时把Eden区和S0区的存活对象移动到S1,如果S1满了会移动到老年代。

然后S1是作为源,S0作为目标。下一次MinorGC清S1和Eden存到S0。 始终保持一个干净的Survivor区域可以用来中转存放。

 

Major GC(老年代清理):

1.垃圾回收器标记所有存活对象

2.垃圾回收器会遍历整个堆内存,清除所有未被标记的垃圾对象,释放这些对象占用的内存空间

3.在清除阶段结束后,堆内存中可能会出现一些内存碎片,这些碎片会影响新对象的分配和分配速度。因此,垃圾回收器通常还需要对堆内存进行整理,将所有存活对象向一端移动,从而消除内存碎片,为新对象的分配提供更大的连续空间

4.在整理阶段结束后,堆内存中会留出一些可用的连续空间,用于分配新对象。

 

 

Full GC(清理整个堆,年轻代+老年代):

老年代:
进入条件:
  1.年龄满15岁,15轮minor gc -XX:MaxTurningThreshold
  2.特别大的对象(大于阈值设置) -XX:PretenureSizeThreshold
  3.如果幸存者区中相同年龄的所有对象大小的总和大于幸存者区空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄
  4.Eden区存活的对象已经超过了surcival区的大小(某一个区,超过就转不动了。)
空间分配担保(在minor gc时判断老年代是否有能力接受):
  在发生minor gc之前,虚拟机必须检查
  初步预判是否存的下。老年大最大可用连续空间是否大于新生代所有对象总空间
    如果大于,说明一定是安全的minorgc
    如果小于,看是否允许担保失败
      handlepromotionfailure:是否允许担保失败的标识符,boolean类型
        允许担保失败:
          检查老年代最大可用连续空间是否大于历次晋升到老年代对象大小的平均值
            大于:尝试一次minor gc
            小于:直接full gc
        不允许担保失败:直接fullgc

 

常见的垃圾回收器:

Serial垃圾回收器:单线程,垃圾收集的时候会stop the world暂停所有应用程序,只留一个线程进行垃圾回收操作。

Parallel垃圾回收器:

优:多线程的垃圾回收器,它可以使用多个线程并行执行垃圾回收操作,从而提高垃圾回收的效率。

缺:在执行垃圾回收操作时需要暂停所有的应用程序线程,可能会导致较长的停顿时间。

CMS(最短回收停顿时间):

多线程标记清除收集器,标记清除的过程:

1.初始标记:暂停所有的应用程序线程,标记所有根对象以及直接与根对象相关联的对象,将它们的标记位设置为已标记状态,并建立标记工作列表。

2.并发标记:并发地标记所有已经被引用的对象,即从标记工作列表中取出一个对象并标记它所引用的其他对象,将它们的标记位设置为已标记状态,并将它们加入标记工作列表中。

3.重新标记:为了保证标记的准确性,需要在应用程序线程运行的同时重新标记所有被修改的对象和被遗漏的对象,确保所有被引用的对象都被正确标记。

4.并发清除:并发地清除所有未被标记的对象,即将它们所占用的内存空间标记为空闲状态,并将它们加入空闲列表中,以备后续的分配使用。

在CMS的垃圾回收过程中,需要控制最短回收停顿时间,一般通过以下两种方式来实现:

1.预留内存空间:为了避免由于频繁的垃圾回收操作导致堆内存不足而导致的停顿,CMS在运行时会预留一部分内存空间,用于存放垃圾对象。当堆内存快要达到预留空间的上限时,CMS会触发垃圾回收操作,从而避免因为内存不足而导致的停顿。

2.并发执行:CMS是一种并发的垃圾回收器,它可以在应用程序运行的同时执行垃圾回收操作。在标记和清除阶段,CMS只会暂停应用程序线程的执行,而不会暂停整个应用程序,从而减少了停顿的时间。

优点:可以在应用程序运行的同时执行垃圾回收操作,减少应用程序的停顿时间,适合对应用程序响应速度有较高要求的场景。

缺点:可能会导致堆内存碎片化,需要额外的内存来维护垃圾回收操作。

 

G1(年轻代、老年代 可预测时间停顿):

初始标记(Initial Mark):暂停所有的应用程序线程,标记所有根对象以及直接与根对象相关联的对象,并建立标记工作列表。

并发标记(Concurrent Mark):并发地标记所有已经被引用的对象,即从标记工作列表中取出一个对象并标记它所引用的其他对象,将它们的标记位设置为已标记状态,并将它们加入标记工作列表中。

最终标记(Final Mark):在应用程序线程运行的同时重新标记所有被修改的对象和被遗漏的对象,确保所有被引用的对象都被正确标记。

筛选回收(Live Data Counting and Evacuation):

把内存分为很多区域region,不再坚持固定大小和固定分代。

1.计算每个区域的回收价值,即空闲区域的大小和存活数量。存活少空闲多的回收价值就高。

2.根据回收价值从高到低排序,把存活对象复制到其他区域,清除这些回收价值高的区域变为空白。

举个例子来说明,假设G1将堆内存分成了10个大小相等的区域,其中第1个区域是Eden区,第2和第3个区域是Survivor区,剩下的7个区域是Old区。在筛选回收过程中,假设第4个区域中存活对象的数量最少、空闲内存最多,因此它的回收价值最高,G1会将这个区域的存活对象复制到其他空闲区域,并清空这个区域,以备后续的分配使用。

可预测时间停顿是指G1在垃圾回收过程中会将整个堆内存分成多个区域,每个区域的大小和回收效率都不同,通过预测每个区域的回收时间,G1可以控制垃圾回收的时间,并尽可能保持停顿时间的稳定性,从而避免因为频繁的垃圾回收操作而导致的不可预测的停顿时间。 G1垃圾回收器的优点是可以在堆内存非常大的情况下保证可预测的停顿时间,同时可以避免Full GC导致的长时间停顿,提高了应用程序的性能和可靠性。

优点:将堆内存分成多个小块,可以提高垃圾回收的效率和可控性,适合大型应用程序和服务端应用程序。

缺点:在执行垃圾回收操作时需要消耗额外的CPU和内存资源,不适合小型应用程序。

 

并发标记为什么会存在漏标的情况?

并发修改:由于并发执行的应用程序可能会修改一些对象的引用关系,例如将一个原本指向一个存活对象的引用修改为指向一个已经被回收的对象,或者反过来。如果这些修改发生在并发标记的过程中,那么标记器可能无法及时地发现这些变化,并及时更新对象的标记位。

RSet漏标:在使用SATB技术时,如果应用程序在并发执行的过程中不断地修改对象的引用关系,那么可能会导致一些引用关系没有被及时地添加到RSet中,从而出现了RSet漏标的情况。如果这些漏标的引用关系指向的是存活对象,那么这些存活对象就有可能被错误地回收。 为了解决这些问题,G1垃圾回收器采用了一系列的技术手段,例如增量更新RSet、卡表技术等,来减少漏标的发生,从而提高回收器的准确性和效率。

在并发标记的过程中可能会存在漏标,G1垃圾回收器通过STAB可以进行处理:

G1垃圾回收器引入了一种叫做“SATB”(Snapshot-At-The-Beginning)的技术,即在并发标记的开始时,生成一个快照,记录下当前所有的引用关系,然后在标记过程中,如果遇到一个对象被修改了(即它所指向的对象发生了变化),就将这个对象的引用添加到一个叫做“RSet”(Remembered Set)的集合中。在最终标记阶段,G1会遍历所有的RSet,将其中的引用关系加入到标记工作列表中,并进行标记。这样就可以避免因为漏标而导致的存活对象被错误地回收的问题。

 

 

CMS和G1的选择:

大量短时间存活对象的应用程序G1,因为这些对象会被根据回收价值尽快回收,不会占用过多的内存空间。

大量长时间存活对象的应用程序用CMS,因为这些对象会被标记为“存活”,不会被过早回收。标记清除算法。

 

 

最新的ZGC有什么改良?

大内存管理:ZGC可以管理非常大的内存,最大可达到4TB。

低延迟:ZGC可以实现非常低的停顿时间,最多只有10毫秒,不会对应用程序的性能产生明显影响。

并发性:ZGC是一种并发的垃圾回收器,可以在应用程序运行的同时执行垃圾回收操作,减少应用程序的停顿时间。

无需设定堆大小:ZGC无需事先设定堆的大小,它可以自动调整堆的大小以适应应用程序的内存需求。

无需分代:ZGC不需要分代,它可以同时管理新生代和老年代,减少了内存管理的复杂度。

跨平台:ZGC可以在不同操作系统和硬件平台上运行,具有很好的跨平台性和可移植性。

 

 

类加载机制:

双亲委派机制

 

 

 

启动类加载器(Bootstrap ClassLoader)

扩展类加载器(Extension ClassLoader)

应用程序类加载器(Application ClassLoader)

自定义类加载器(Custom ClassLoader)

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派

1.防止重复加载同一个.class

2.保证核心.class不能被篡改(由于是从启动类加载器开始加载)

为什么JDBC和Tomcat不遵守双亲委派机制?

因为他们需要的类都不在jre/lib目录下,所以实现了自定义的类加载器,为了确保能够加载特殊的类和资源。

如何自定义类加载器?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyClassLoader <strong>extends ClassLoader</strong> {
    private String classPath;
    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }
    @Override
    protected Class<?><strong> findClass</strong>(String name) throws ClassNotFoundException {
        // 从指定的classPath路径下加载指定名称的类
        byte[] data = <strong>loadClassData</strong>(name);
        if (data == null) {
            throw new ClassNotFoundException();
        }
        // 将字节数组转换为Class对象
        return <strong>defineClass</strong>(name, data, 0, data.length);
    }
    // 加载指定名称的类字节数组
    private byte[] <strong>loadClassData</strong>(String name) {
        // 读取指定路径下的类文件到字节数组中
        // ...
        return data;
    }
}

1.继承java提供的ClassLoader,并实现findClass方法来指定加载类。

loadClassData():从指定的classPath路径下加载指定名称的类的字节数组,然后调用defineClass把字节数组转换为Class对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
    public static void main(String[] args) throws Exception {
        String classPath = "/path/to/classes";
        // 创建MyClassLoader实例
        MyClassLoader myClassLoader = new MyClassLoader(classPath);
        // 加载指定名称的类
        Class<?> clazz = myClassLoader.loadClass("com.example.MyClass");
        // 执行加载的类中的方法
        Object instance = clazz.newInstance();
        Method method = clazz.getMethod("sayHello");
        method.invoke(instance);
    }
}

在上面的例子中,我们首先创建了一个MyClassLoader实例,并将需要加载的类的名称传递给它的loadClass()方法,它会根据自己的加载策略去加载指定的类。

线程上下文类加载器:

问题产生:一些接口,有第三方实现代码,一般被放在classpath下。需要使用不同的类加载器去加载类的时候。由于双亲委派机制,父类无法加载子类的问题,造成问题。

在Java中,类Thread中的getContextClassLoader()和setContextClassLoader(ClassLoader cl)分别用来获取和设置上线文类加载器。

线程上下文类加载器可以在运行时为线程动态指定一个类加载器。

如果当前线程没有设置类加载器,则从父线程获取上下文类加载器。它可以打破双亲委托机制,父ClassLoader可以使用当前线程的Thread.currentThread().getContextClassLoader()所指定的classLoader来加载类,

这就可以改变父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。

这样可以先加载子类,而不是从父类开始找。

类加载的过程:
1.加载

查找并加载类的二进制数据。在这个阶段,Java虚拟机需要完成以下任务:

  • 通过类的全限定名获取类的二进制数据流。
  • 将二进制数据流转换成Java虚拟机中的Class对象。
  • 将Class对象放入方法区(也称为永久代)中。

2.验证

确保被加载的类符合Java虚拟机规范和安全规范。在这个阶段,Java虚拟机需要完成以下任务:

  • 文件格式验证:验证二进制数据是否符合Class文件格式规范。
  • 元数据验证:验证类的元数据信息是否符合Java虚拟机规范。
  • 字节码验证:验证类的字节码是否符合Java虚拟机规范。
  • 符号引用验证:验证类中引用的其他类是否存在,并且是否有权限访问这些类。

3.准备

为类的静态变量分配内存,并设置默认初始化值。在这个阶段,Java虚拟机需要完成以下任务:

  • 为类的静态变量分配内存空间,并设置默认初始化值(0或null)。

4.解析

将符号引用转换为直接引用。在这个阶段,Java虚拟机需要完成以下任务:

  • 将类中的符号引用(如类名、方法名、字段名)转换为直接引用(内存地址)。
  • 解析阶段可以进一步分为类解析、接口解析和字段解析等。

5.初始化

为类的静态变量赋值,并执行静态代码块。在这个阶段,Java虚拟机需要完成以下任务:

  • 为类的静态变量赋初值(即在准备阶段分配的值),或者执行静态代码块中的代码,以及执行其他初始化操作。
  • 如果一个类有父类,则需要先初始化父类,再初始化子类。
  • 如果一个类实现了某个接口,需要先初始化这个接口。

为什么解析可以在初始化之后?

解析是符号引用转为直接引用。

如果解析在初始化之后执行,符号引用在初始化的时候被赋值。可以进行转换。

如果在初始化之前进行解析,需要满足:

1.要确保解析的符号引用已经存在,即在编译期或者运行时已经定义了这个符号引用。

2.要确保在解析操作执行之前,已经完成了类加载、验证和准备等前置操作,即类已经被正确地加载到了内存中,符号引用已经被赋予了默认值和具体值。

如果不满足这两个条件,进行解析操作就可能会抛出LinkageError类型的异常

 

堆和栈的溢出:

1.堆溢出:

java.lang.OutOfMemoryError

  1)设置问题,分配大小不够

  2)对象太多或太大,放不下

排查:

  调整JVM的-xms和xmx大小。

  对于内存占用的大对象,可以考虑使用缓存等方式减少对象的创建。

  通过工具查看堆内存使用情况,例如使用jmap命令来生成内存快照,然后使用jhat或者MAT等工具来分析内存快照,从而找出内存占用大的对象。

 

栈溢出:

java.lang.StackOverflowError

  1)方法调用的层数过多,导致栈空间不足。

  2)方法中使用了大量的局部变量,导致栈空间不足

排查:

减少递归调用的层数,或者使用非递归方式来实现对应的算法

减少方法中使用的局部变量,或者将局部变量转换为实例变量或静态变量。

使用JVM参数-Xss来调整栈内存大小,以满足程序的需求。

 

执行耗时高如何排查?

 

posted @   NobodyHero  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示