2023年 JVM学习笔记

一.简介:

Java虚拟机(英语:Java Virtual Machine,缩写为JVM),一种能够执行Java bytecode虚拟机,以堆栈结构机器来进行实做。最早由Sun微系统所研发并实现第一个实现版本,是Java平台的一部分,能够执行以Java语言写作的软件程序

JVM的组成

JVM大致可以分为一下五个组成部分:

在JDK1.8以后, 元数据区取代了永久代。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

二.各个组成部分详细了解:

程序计数器:

程序计数器是一块较小的线程私有的内存空间,是当前线程正在执行的那条字节码指令的地址。当线程切换回来时,可以知道上次线程执行到哪了。

Java 虚拟机栈:

Java 虚拟机栈是描述 Java 方法运行过程的内存模型。

Java 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息,如:

局部变量表

定义为一个数字数组,主要用于存储方法参数、定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及 return address 类型。

本地方法栈:

本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。它与 Java 虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。

堆:

堆是用来存放对象的内存空间,几乎所有的对象都存储在堆中。

堆的特点:

  • 线程共享,整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个。

  • 在虚拟机启动时创建。

  • 是垃圾回收的主要场所。

  • 堆可分为新生代(Eden 区:From Survior,To Survivor)、老年代。

  • Java 虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

  • 关于 Survivor s0,s1 区: 复制之后有交换,谁空谁是 to。

而我们在JVM优化的时候更多就是在优化这个堆的内存。

堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)

  1. 老生代(Old Generation)

  1. 永久代(Permanent Generation)

对象分配过程

1.new 的对象先放在 Eden 区,大小有限制

2.如果创建新对象时,Eden 空间填满了,就会触发 Minor GC,将 Eden 不再被其他对象引用的对象进行销毁,再加载新的对象放到 Eden 区,特别注意的是 Survivor 区满了是不会触发 Minor GC 的,而是 Eden 空间填满了,Minor GC 才顺便清理 Survivor 区

3.将 Eden 中剩余的对象移到 Survivor0 区

4.再次触发垃圾回收,此时上次 Survivor 下来的,放在 Survivor0 区的,如果没有回收,就会放到 Survivor1 区

5.再次经历垃圾回收,又会将幸存者重新放回 Survivor0 区,依次类推

6.默认是 15 次的循环,超过 15 次,则会将幸存者区幸存下来的转去老年区 jvm 参数设置次数 : -XX:MaxTenuringThreshold=N 进行设置

7.频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集

TLAB

  • TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区,是属于 Eden 区的,这是一个线程专用的内存分配区域,线程私有,默认开启的(当然也不是绝对的,也要看哪种类型的虚拟机)

  • 堆是全局共享的,在同一时间,可能会有多个线程在堆上申请空间,但每次的对象分配需要同步的进行(虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性)但是效率却有点下降

  • 所以用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自己的 TLAB,这样可以使得线程同步,提高了对象分配的效率

  • 当然并不是所有的对象都可以在 TLAB 中分配内存成功,如果失败了就会使用加锁的机制来保持操作的原子性

  • -XX:+UseTLAB 使用 TLAB,-XX:+TLABSize 设置 TLAB 大小

方法区:

Java 虚拟机规范中定义方法区是堆的一个逻辑部分。方法区存放以下信息:

  • 已经被虚拟机加载的类信息

  • 常量

  • 静态变量

  • 即时编译器编译后的代码

方法区的特点

  • 线程共享。 方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。

  • 永久代。 方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,把方法区称为“永久代”。

  • 内存回收效率低。 方法区中的信息一般需要长期存在,回收一遍之后可能只有少量信息无效。主要回收目标是:对常量池的回收;对类型的卸载。

  • Java 虚拟机规范对方法区的要求比较宽松。 和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收。

三.对象的创建过程

Step1:类加载检查

当虚拟机接收到New指令的时候,会首先根据指令的参数到方法区中查找是否存在相同的引用被加载。

Step2:分配内存

在类的检查通过之后会在堆内存中划分出一片区域。 分配方式有 “指针碰撞” 和 “空闲列表” 两种 。

指针碰撞 :

  • 适用场合 :堆内存规整(即没有内存碎片)的情况下。

  • 原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。

空闲列表 :

  • 适用场合 : 堆内存不规整的情况下。

  • 原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。

Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息 。

Step5:执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象在内存中的布局

对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。

对象头:对象头包括两部分信息,第一部分是 存储对象自身的运行时数据 (哈希码、GC 分代年龄、锁状态标志等等) 另一部分是类型指针, 即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据:对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充:不是必然存在的,也没有什么特别的含义,仅仅起占位作用 。

四.垃圾回收机制

JVM垃圾回收机制针对的是堆内存中的对象的回收

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;

  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;

  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

整堆收集 (Full GC):收集整个 Java 堆和方法区。

那么我们是如何判断对象是否要被回收呢?

在对象没有被使用的时候,就可以被回收,而判断对象是否被使用的方法我们常见的两种方法是:引用计数法,可达性分析法。

垃圾收集算法
标记-清除算法:

就是标记出不需要清除的对象, 在标记完成后统一回收掉所有没有被标记的对象。

它是最基础的垃圾收集方法,主要有两个弊端:

1.效率问题。

2.会产生内存碎片。

标记-复制算法:

它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

标记-整理算法

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:

根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

五.类的加载过程

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

Step1:加载

  1. 通过全类名获取定义此类的二进制字节流

  1. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构

  1. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

Step2:链接的验证

验证类的结构和语法是否符合规范

Step3:准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段

Step4:解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

Step5:初始化

初始化阶段是执行初始化方法 inti方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

六.类加载器详解

所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存。

JVM 中内置了三个重要的 ClassLoader :

BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。

ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。

AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

双亲委派机制就是当一个类要加载的时候先去父类加载器判断是否加再过,没有的话继续向上,到最顶层BootstrapClassLoader加载器,然后向下尝试加载。

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类 。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值