JVM 常识

内存区域划分

  1. Java虚拟机栈(Java Virtual Machine Stacks)
    • 作用:存储局部变量和部分方法信息,每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    • 特点:每个线程都有自己的栈,栈的大小可以动态调整。
  2. 本地方法栈(Native Method Stack)
    • 作用:与虚拟机栈类似,用于支持Native方法的执行,其中存储的是Native方法的信息。

Java堆(Java Heap)

  • 作用:存储对象实例。所有线程共享的一块内存区域。
  • 特点:Java堆的内存空间在程序启动时就被预先分配好,并且可以动态地扩展或缩小。

程序计数器

程序计数器(Program Counter Register)

  • 作用:用于线程之间的指令协同,记录当前线程执行的字节码指令地址。
  • 特点:每个线程都有独立的程序计数器,线程切换时切换到相应线程的计数器。

方法区

方法区(Method Area)

  • 作用:存储类的结构信息,如类的字段、方法信息、构造器信息,也包括运行时常量池(Runtime Constant Pool)。
  • 特点:方法区也是线程共享的。

类加载

Java 程序启动的时候,JVM 就会把 .class 文件读进内存并进行一系列后续工作

  1. 加载:找到 .class 文件->打开文件->读文件->创建空的类对象
  2. 链接
    • 验证:检查 .class 文件格式是否符合规范要求
    • 准备:给静态变量分配内存空间,空间内填充零值
    • 解析:把字符串常量进行初始化,把符号引用替换成直接引用(编译过程中,编译器会使用一些特殊的符号来分别表示字符串常量,等到类加载的时候,才就会把这些字符串常量放到内存中,用对应的内存地址替换刚才的特殊符号)
  3. 初始化:会对类的静态成员进行初始化,执行静态代码块,如果这个类的父类还没加载,也要去加载父类

双亲委派模型

描述的是类加载中的加载阶段,也就是上述提到的第1阶段,去哪些目录找 .class 文件

JVM 存在一个特殊的模块——类加载器,负责完成类加载工作

JVM 自带三个类加载器:

  • BootStrapClassLoader 负责加载标准库中的类
  • ExtensionClassLoader 负责加载一些扩展的类
  • ApplicationClassLoader 负责加载应用程序自己写的类

JVM 给这三个类加载器约定了父子关系:BootStrapClassLoader 是 ExtensionClassLoader 的父亲,ExtensionClassLoader 是 ApplicationClassLoader 的父亲。

双亲委派模型就是,一个类加载器在加载类时会先委托给它的父类加载器去尝试加载,只有在父类加载器无法加载时,才会由子类加载器自行加载。具体步骤如下

  1. 检查父类加载器是否已经加载过该类:当一个类加载器收到加载类的请求时,它首先会检查父类加载器是否已经加载过这个类。如果父类加载器已经加载过,就直接返回父类加载器加载的类,不再进行加载。
  2. 委派给父类加载器:如果父类加载器没有加载过该类,那么该类加载器将加载请求委派给其父类加载器。这一过程会一直持续,直到达到Bootstrap ClassLoader(引导类加载器)为止。
  3. 尝试自己加载:如果所有的父类加载器都无法加载该类,那么该类加载器将尝试自己加载这个类。如果加载成功,就返回给请求加载的类;如果加载失败,就抛出ClassNotFoundException异常。

这种层次化的类加载机制保证了类的唯一性,避免了类的重复加载

垃圾回收机制

简介

GC 是 “Garbage Collection”(垃圾回收)的缩写,它是一种自动内存管理的机制,用于自动检测和回收程序中不再使用的内存,防止内存泄漏和提高内存利用率。垃圾回收由 JVM 负责。

在程序执行过程中,对象被创建在堆内存中,而垃圾回收的目标是识别和清理不再被程序引用的对象,释放它们占用的内存空间。这样,程序就能够动态地管理内存,无需开发人员手动释放内存。

如何判断一个对象是不是垃圾

  1. 引用计数

    使用额外的计数器,记录某个对象有多少个引用指向它,如果引用为 0,就说明这个对象是垃圾了,C++ 中的 shared_ptr 用的就是这个方法

    缺点:

    • 多线程中,需要修改同一个引用计数,需要考虑线程安全问题。
    • 如果对象小,数量多,引用计数会造成不小空间开销
    • 可能会带来循环引用问题,比如 C++ 中的 shared_ptr 就有这个问题,引入 weak_ptr 才得以解决
  2. 可达性分析算法,这是 Java 采用的方案

    在 Java 虚拟机中,可达性分析算法通常采用根搜索算法(Root Search Algorithm),从一组称为"根"的起始对象出发,通过引用关系逐步遍历对象图,标记所有可以被直接或间接引用到的对象,在标记完活动对象之后,清理掉没有被标记的对象,将它们回收释放内存。

    什么样的对象被称为根?通常是程序中能直接访问的对象,如:

    • 栈里的引用所指向的对象
    • 常量池中对应的对象
    • 方法区中,静态引用类型的成员

    其实就是个暴力算法,把所有能访问的都访问一遍而已。

如何回收垃圾

  1. 标记-清除

    直接清除垃圾,但是会造成大量的内存碎片

  2. 复制算法

    将内存分为两半,用一半留一半,回收垃圾前,把要保留的对象复制到另一半,然后把之前的一半空间全部释放。

    虽然解决了内存碎片问题,但是可用空间减少了一半,空间利用率大大降低

  3. 标记-整理

    和顺序表删除元素一样,把要保留的对象都往前搬。

    虽然提高了内存利用率,也降低了内存碎片的情况,但是这种方法比较耗时

  4. 分代回收

    综合上述方案。在不同的场景下,使用不同的方案。

    给对象引入一个“年龄”概念,年龄即该对象被 GC 扫描过的轮次

    把年龄小的对象(新生代)和年龄大的对象(老年代)放到不同的内存区域中,并且使用不同的回收算法

    新生代的对象生命周期短(经验规律),采用复制算法能够迅速回收不再使用的对象;而老年代的对象生命周期较长,采用标记-整理的算法来处理。

    新生代的内存布局通常包括一个伊甸区、两个幸存区

    具体步骤

    1. 新的对象放到伊甸区

    2. 伊甸区中的对象绝大部分活不过一轮 GC,活下来的放到幸存区中。

      从伊甸区放到幸存区,使用复制算法,因为存活的对象少,复制的开销小,而且幸存区的大小也比较小,对空间利用率的影响也不大。

    3. 幸存区中的对象经过一轮 GC,使用复制算法把活下来的放到另一个幸存区中

    4. 当一个对象在幸存区中,经历了多轮 GC 后仍然存活,那么这个对象就会晋升为老年代

    5. 老年代对象,被 GC 扫描的频率会降低。如果老年代对象变成垃圾,那么就使用标记整理算法来处理,因为老年代对象被回收的频率不高,使用标记整理算法的开销就在可接受范围内了。

    例外:对于特别大的对象,不必经历上述分代过程,直接进入老年代。因为大对象不适合频繁复制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世真

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值