Java后端八股笔记8 JVM

25 阅读7分钟

内存模型

运行内存区域五部分组成:虚拟机栈,堆,本地方法栈,元空间,程序计数器。

image-20260110143705404.png

程序计数器

线程当前执行方法的JVM字节码行号指示器。作用:

  • 字节码解释器通过改变这个计数器的值来指定下一个需要执行的字节码指令
  • 多线程时,通过程序计数器来记录当前线程执行位置。

存放程序创建的对象实例。

堆内存结构

堆内存通常分为:

  • 新生代内存(Young Generation):分为Eden SpaceSurvivor Space(S0,S1)。实例创建时进入Eden,当Eden满了执行一次新生代垃圾回收机制,仍存活进入Survivor ,并且年龄加1,当年龄达到一定程度(默认为15,可更改)就进入老年区
  • 老生代(Old Generation):生命周期较长,GC发生频率较低。空间为新生代的二倍。为避免频繁切换导致内存碎片过多,大对象直接进入老年代
  • 永久代(Permanent Generation):存储一些静态变量等,后被元空间(使用本地内存而非堆)代替。

字符串常量池

JVM针对字符串类(String类)专门创建的一块区域,避免了字符串的频繁创造。

1.7前在方法区内,之后在堆内,可以更加高效的回收。

存储局部变量(若为对象则为对象的引用,实际对象在堆),方法的参数与返回地址。

当程序调用某个方法时:

  1. 解析方法调用:JVM通过方法引用找到实际方法地址。
  2. 创建栈帧:在虚拟机栈中分配一个栈帧,用于存放方法的局部变量表,操作数栈等。
  3. 执行方法:执行方法内的字节码指令。
  4. 返回处理:返回结果,清理栈帧,回复调用者的执行环境。

直接内存

通过JNI在本地内存上分配的,不是虚拟机运行时内存的一部分。

内存泄漏与内存溢出

  • 内存泄漏:在运行过程中不被使用的对象仍然被引用,而无法被垃圾收集器处理,导致可用内存越来越少。

    • 静态属性导致的内存泄漏:尽量避免使用静态变量
    • 未关闭的资源:在finally中关闭资源
    • 使用TreadLocal:将TreadLocal同样视为需要关闭的资源
  • 内存溢出:申请内存时,没有新的内存可分配。

    • 大量对象建造
    • 持续引用
    • 递归调用

类初始化

Java对象的创建过程

  • 类加载检查:JVM收到一个new指令,先检查该指令的参数能否在常量池中定位到一个类的符号引用,且该类是否被加载过,如果没有则进行类加载。
  • 分配内存:根据类加载的大小分配堆空间。
  • 初始化零值:将分配到的空间置零值(各个类型中的零值,确保 Java 代码中未赋初始值的实例字段可直接使用对应类型的零值。)
  • 设置对象头:在对象头中执行元数据信息,哈希码,锁等数据
  • 执行init方法:执行初始化方法按程序员定义进行初始化

类加载器

  • 启动类加载器(Bootstrap Class Loader):最顶层的类加载器,负责加载Java的核心库,它是用C++编写的,是JVM的一部分。
  • 扩展类加载器(Extension Class Loader):Java语言实现,继承自ClassLoader类,负责加载Java扩展框架。扩展类加载器由启动类加载器加载,并且父加载器就是启动类加载器。
  • 应用程序类加载器(Application Class Loader):Java语言实现,负责加载用户类路径(ClassPath)上的指定类库,是我们平时编写Java程序时默认使用的类加载器。
  • 自定义类加载器(Custom Class Loader):开发者可以根据需求定制类的加载方式,比如从网络加载class文件、数据库、甚至是加密的文件中加载类等。

双亲委派模型

核心思想:当一个类加载器收到类加载的请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,最终都传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

类加载

  • 加载:将字节码数据从不同的数据源读取到JVM中,并映射为方法区的运行时数据结构(Class对象)

  • 链接:将原始类定义信息转化入JVM

    • 验证:确保加载的类符合当前虚拟机的要求(文件格式,字节码,元数据,符号引用)

    • 准备:为类中静态字段分配内存,并设置默认值。其中被final修饰的早在编译阶段就已经赋值。

    • 解析:将常量池中的符号引用转换为直接引用

      • 符号引用:使用一串符号描述所引用目标,只要无歧义定位即可
      • 直接引用:直接指向目标的指针
  • 初始化:执行类的构造器方法,为讲静态变量赋值,并执行静态代码块,在编译时期已经写好逻辑。

垃圾回收

Garbage Collection,GC,负责自动释放不再被程序引用的对象所占据的内存。触发方式:

  1. 内存不足时:自动触发
  2. JVM参数设置:比如:最大堆大小,初始堆大小等参数
  3. 对象数量或内存达到阈值:可自动设置
  4. 手动触发:调用System.gc()

判断垃圾

  • 引用计数法:设置一个引用计数器,被引用一次加1,引用失效时减1,为0时断定为垃圾。无法处理依赖循环(双方一直有引用)

  • 可达性分析算法:从GC root(垃圾收集根)出发,向下追溯他们引用的对象,以及引用对象引用的对象。若存在对象GC root不可达,则说明该对象为垃圾。

    • GC root:虚拟机栈或本地方法栈中正在引用的对象;静态属性引用的对象。

    • 别名:三色标记算法具体流程:

      1. 首先创建三个集合:白、灰、黑。将所有对象放入白色集合中。
      2. 然后从根节点开始遍历所有对象,把遍历到的对象从白色集合放入灰色集合
      3. 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
      4. 重复 4 直到灰色中无任何对象
      5. 通过write-barrier检测对象有变化,重复以上操作
      6. 收集所有白色对象(垃圾)

垃圾回收算法

  • 标记-清除算法:通过可达性分析标记垃圾并进行清除:内存碎片;效率低
  • 标记-整理算法:通过标记,并把存活的整理到内存一端,整理之后执行清除:
  • 复制算法:将内存分为两块,每次只使用一块,当满了之后将存活的一道另一块,把这块内存全清楚:内存利用率低
  • 分代算法:分为新生代与老年代,新生代使用复制算法,老年代使用标记算法。

GC类别

根据作用范围和触发条件分为三种类型:

  • Minor GC:只回收新生代区域,当Eden空间不足时,将Eden和一个Survivor中的数据放入另一个Survivor或老年代中。十分频繁,效率高。
  • Major GC:主要回收老年代,当老年代空间不足或新生代变老速度过快。
  • Full GC:回收全部,包括新老生代和元空间。当元空间空间不足或Minor GC时,老年代空间不足时触发。代价昂贵,停止全部线程(stop the world),应尽可能避免。

G1垃圾回收

JDK 1.9之后作为默认回收器

运作步骤:

  • 初始标记:短暂停顿(Stop The World,STW)标记从GC root出发可直达的活跃对象

  • 并发标记:与程序并发执行,标记所有可达对象。

  • 最终标记:STW,处理并发标记过程中少量引用发生更改的对象

  • 筛选回收:根据标记结果,选择回收价值高的区域,复制其中的存活对象到新区域。其中包含多个STW。

    • 回收价值高:在后台维护一个优先级队列,根据允许收集的时间设置回收价值。