JVM原理理解——类的加载过程和GC原理

本文详细阐述了Java类加载的七个步骤,包括加载、验证、准备、解析、初始化、使用和卸载,强调了双亲委派模型和初始化阶段的区别。接着,讨论了JVM内存区域,包括栈、堆和元数据区的作用。最后,深入介绍了垃圾回收机制,包括各种垃圾回收算法(标记-清除、复制、标记-整理、分代收集)及其触发点,以及如何避免内存溢出。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、类开始加载

         类的加载过程大体上可以分为7个步骤:加载、验证、准备、解析、初始化、使用、卸载。下面对这7个步骤,详细的说下本人的理解:

1.1 加载

        jvm加载器根据类的签名(全路径名),以双亲委派的查找方式,读取class文件的二进制流到内存,存储与方法区,jdk1.8之后,修改为元数据区,并将其转化为一个与目标类型对应的java.lang.Class对象实例,存储于方法区,作为方法入口。

 1.2 验证

        验证类的信息,是否符合JVM的规范,是否是一个有效的字节码文件。

        验证的内容包括:格式信息,验证是否符合class的文件规范,比如jvm的魔法数验证;语义验证:java的一些修饰符验证,比如final。

1.3 准备

        主要是为static变量分配空间,并初始化值,是系统自身的初始化值,并非static变量的赋值final变量直接赋值

1.4 解析

        这个阶段将常量池中的符号引用,替换为直接引用,举个例子说明下,比如常量Constant.WZW,在常量池中的值为"wanggou",会直接将变量WZW的值赋为"wanggou"的引用地址。

1.5 初始化

        对类的静态变量进行赋值,执行静态方法的过程,可以理解为只执行类中static修饰的变量和方法,如果一个类中没有static变量或者方法,编译器不会初始化这个类,如果类中有多个static变量或者方法,自上而下执行。

1.6 使用

        使用说下线程的内存区域,两个区域需要关注:栈和堆。

        (1)每条线程都有一个私有区域,私有区域包括程序计数器、虚拟机栈(存储线程运行过程中的信息)、本地方法栈(存储JVM自身运行信息)。

        程序计数器,可以参考另一篇文章,本地方法栈不用太多关注,作为程序员,主要关注线程执行时,临时变量存储的虚拟机栈。

        虚拟机栈有如下特点:

  • 栈为线程私有,生命周期和线程一样;
  • 线程中每个方法,对应一个栈帧,存储在虚拟机栈中,栈帧也可以分为四个部分:本地变量表——存储方法的局部变量、对象引用、操作数栈——cpu的分片执行逻辑使用,存储方法执行位置、常量池的引用、方法出口

        (2)线程的共享区域:元数据区域+堆

        元数据区域,从存储的数据特点可以理解,至于堆,线程的栈中已经可以存储数据,为什么还需要开辟堆来存储对象信息?因为栈是纵向的,对于大小时长变化的变量对象,不好横向扩展,需要需要堆来存储线程中使用的对象。

1.7 卸载

        GC原理,可以参考第三节内容。

    如上图所示,一直到初始化部分,JVM完成了一个类的加载过程。过程中要点梳理如下:

  • 双亲委派加载

    类加载时,首先都不要自己尝试去加载,都是委托给父类加载,父类找不到,在尝试自己加载。好处是:使用不同的类加载器,最终得到的都是同样的Object对象。

    加载流程图如下:

  • 准备阶段的初始化和初始化阶段区别

    准备阶段的初始化,是JVM为static变量分配好内存空间后,系统的初始化值,而运行前的初始化阶段,是将static变量设置成程序中指定的值。举个例子

private static String name="cc";

    准备阶段的初始化值,name值为null,系统为String对象分配的默认值,到初始化阶段,才会被赋值“cc”;

二、JVM运行状态内存区域

    讨论完类的加载,所以进入到类的运行步骤,即【使用】阶段。首先需要清楚内存的布局。

    Java程序运行的最基本单位是线程,从线程的角度来看JVM内存区域的分配

    

三、垃圾回收机制

    Java程序的运行,由于没new一个对象,都会为其在堆中分配存储空间,如果一直不回收无用的对象,堆很快就会没有存储空间而OOM。

    所以Java后台服务线程中有垃圾回收线程,即GC线程。

    怎么来GC呢?第一步,需要确定堆中那些是垃圾对象。

3.1. 垃圾定位算法

    确定的方法有两种引用计数法和根搜索算法。由引用计数法迭代到根搜索算法,也是一个发现问题到解决问题的过程。因为引用计数法无法解决循环引用的问题。

3.1.1 引用计数算法

    引用计数法比较简单,也是JVM最初使用的确定“垃圾”的算法,原理就是对象有一个引用,计数器就加1,减少一个引用,计数器就减1,当计数器为0时,就表示对象没有引用,对象死亡,成为垃圾。但是这种算法,不能解决两个对象相互引用的场景,所以被根搜索算法代替。

3.1.2 根搜索算法

    通过一些列GC Roots的点作为起点,向下搜索,当任何一个对象到任何GC Roots没有引用链相连,说明对象已经死亡,可以回收。

3.2 垃圾回收算法

    垃圾回收算法,介绍四个:标记-清除算法(Mark-Sweep),复制算法(coping)、标记-整理算法(Mark-Compact)、分代收集算法(Generational Collection)。主要介绍这四种垃圾回收算法,是因为目前主流的分代回收,是从之前的三种算法改进而来,吸取各个垃圾回收算法的优点、摒弃缺点,理解前三种算法的原理与不足,有助于理解分代回收算法的设计。

3.2.1 标记-清除算法(Mark-Sweep)

    此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。

    缺点:此算法需要暂停整个应用,同时,会产生内存碎片 。

3.2.2 复制算法(coping)

    此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。

    缺点:就是需要两倍内存空间。

3.2.3 标记-整理算法(Mark-Compact)

    此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

    缺点:比较耗时,而且做了很多无意义的工作,因为有的生命周期很长的对象,每次扫描都是“存活”状态,不需要多次扫描。基于此不足,大名鼎鼎的分代回收算法就出现了。

3.2.4 分代回收算法(Generational Collection)

    分代回收的主要思想,就是根据对象的生命周期不同,采用不同的回收算法。所以将JVM分为年轻代(Young Generation)、老年代(Old Generation)、持久代(Permanent Generation),对不同的代采用不同的垃圾回收算法。

  • 年轻代主要是快速的回收掉那些生命周期比较短的对象,所以会经常发生GC操作,采用复制算法作为垃圾回收算法,主要将年轻代分为三个区域:Eden Space(新生区)、两个Survivor Space(幸存区),具体原理如下:

    所有新生成的对象都是放在年轻代的。年轻代的目的就是快速收集掉那些生命周期比较短的对象。年轻代内存相对比较小,垃圾回收会比较频繁。年轻代一般分为三个区:一个Eden Space,两个Survivor Space(一般两个,可配置多个,为了便于说明,这里命名为A和B )。当对象在堆中创建后,会进入年轻代的Eden space,当Eden Space满时,复制对象到A Survivor Space,垃圾回收器会扫描Eden Space和A Survivor Space,如果对象还存活,则复制到B Survivor Space,如果B Survivor Space已满,则年老代(Old Gen)。在扫描Survivor Space时,如果对象经过几次扫描,仍然存活,JVM认为是持久化对象,则将其移动道Old Gen。扫描完毕,JVM将Eden Space和A Survivor Space清空,并且讲A和B的角色交换(即当Eden Space满时,会向B Survivor Space复制对象,扫描器会扫描Eden Space和B Survivor Space),这样可以避免内存碎片。

  • 老年代主要是生命周期比较长的对象(经过年轻代多次,默认5次,回收还存活的对象),或者是一些比较大的对象,会直接放到老年代。老年代采用标记整理算法作为垃圾回收算法,可以避免内存碎片(将存活的对象移动到内存的一边,即整理内存)。
  • 持久代主要存放类定义、字节码和常量等很少会改变的信息,JVM目前采用老年代的算法做持久代的垃圾回收策略。

3.3 垃圾回收触发点

    需要清楚垃圾回收的触发点,也会了解OOM的触发点,哈哈

    主流的垃圾回收算法为分代回收,所以讨论垃圾回收的触发点,需要分为年轻代和老年代来触发(持久代和老年代一起)。

3.3.1 Scavenge GC

    一般情况下,当新对象创建,想Eden Space申请空间失败时,就会出发Scavenge GC,对Eden Space进行GC,清除不存活的对象,并且把尚存活的对象移动到Survivor Space。然后整理两个Survivor Space,这种GC方式是对Younge Gen的Eden Space进行的,不会影响到Old Gen。大部分对象都是从Eden Space开始的,并且Eden Space不会分配的很大,所以Eden Space的GC会很频繁,并且需要使用速度快、效率高的算法,使Eden Space尽快空闲出来。

3.3.2 Full GC

    Full GC是对整个堆进行整理的,包括Young、Old Gen和Permanent Gen。因为Full GC是对整个堆进行空间整理,所以会比Scavenge GC慢很多,因此应该尽量减少Full GC的次数。触发Full GC的点包括:

·年老代(Old Gen)被写满
·持久代(Permanent Gen)被写满
·System.gc()强制触发
·上次GC后,Heap的各域分配策略动态变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值