JVM简介

JVM跨平台的原理:

​ 不同操作系统上运行的JVM是不一样的。

其中java文件转化为字节码文件是在编译期间完成,而不是运行期间。提高运行效率。

JVM结构

1.类加载系统

参考链接:https://blog.csdn.net/qq_45272690/article/details/122443424

功能:根据指定的全限定名来加载类或接口(包名+类/接口名)

将符号引用解析为直接应用:将该class文件中import的其他java类的引用解析为其他java类在对应的在方法区中的class文件地址。

1.1 流程

1.1.1 加载:

  1. 通过一个类/接口的全限定名来获取定义此类/接口的二进制字节流
  2. 将这个字节流所代表的静态储存结构转换为方法区的运行时数据结构
  3. 在内存中(堆)生成一个代表这个类的java.ang.class对象,作为方法区这个类的访问入口

1.1.2 连接

1.1.2.1 验证

确保Class文件的字节流中包含的信息符合虚拟机规范的要求(),保证这些信息被当做代码运行后不会危害虚拟机自身的安全。

这个阶段决定了虚拟机的健壮性,使得虚拟机不那么轻易被攻击,因此在代码量和耗费的性能上来说,验证阶段的工作量在类加载过程中是占比非常大的。

验证的要求:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范,保证字节流的数据能够正确解析并存储到方法区的数据结构中,而且当前的虚拟机版本能够对其进行处理
  2. 元数据验证:类的元数据信息进行验证,比如对父类的信息检查,类字段方法定义,数据类型校验
  3. 字节码验证:整个验证过程最复杂的一步,通过数据流和控制流来分析程序的语义是合法,符合逻辑的。保证方法运行时不会做出错误或者危害虚拟机的行为。
  4. 符号引用验证:
    1. 符号引用中通过字符串描述的全限定名是否能找到对应的类。
    2. 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
    3. 符号引用中的类、字段、方法的可访问性(private、protected、public、package)是否可被当前类访问。
1.1.2.2 准备

静态变量分配内存,然后赋值(普通静态变量赋默认值,加上final的静态变量直接赋值,这个得益于ConstantValue属性)。对于实例变量(创建出对象才能访问)来说则会延迟到对象实例化的时候在进行分配内存。

1.1.2.3 解析

将符号引用转换为直接引用

符号引用就是一个字段/类/方法的属性表,是存在于Class文件中的,对于不同虚拟机来说符号引用是一样的,确定不变的。只需要能够准确的定位到目标就行。

直接引用就是将Class文件中的符号引用(也就是字段/类/方法的属性表)转换为真实的内存地址(访问读取修改就是基于真实的内存地址来操作的,为了之后的操作)。由于是内存地址,不同虚拟机的内存布局实现可能不同,对于不同的虚拟机来说直接引用是不一样的,不确定的。

1.1.3 初始化

对静态类型(默认值的静态变量)进行赋值操作,同时该阶段也会执行静态语句块中的内容。编译器整合这两个操作生成了一个方法叫做cinit,执行和赋值的顺序是根据用户写的java文件决定的

1.2 双亲委派机制

方式:如果一个类加载器收到了类加载器的请求.它首先不会自己去尝试加载
这个类.而是把这个请求委派给父加载器去完成.只有父类加载反馈自己
无法加载这个请求时.子加载器才会尝试自己去加载。

作用:

  1. 避免类的重复加载
  2. 防止核心api被修改

1.3 Tomcat为什么要自定义类加载器

2. 运行时数据区

2.1 程序计数器

PC Register,程序计数寄存器,简称为程序计数器:

  1. 是物理寄存器的抽象实现
  2. 用来记录执行的下一条指令的地址
  3. 它是程序控制流指示器,循环,if else,异常处理,线程恢复都依赖它来完成
  4. 解释器工作时就是通过它来获得下一条需要执行的字节码指令的
  5. 它是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域

2.2 java方法栈(java栈,虚拟机栈)

每个线程在创建时都会创建一个虚拟机栈,栈内会保存一个个的栈帧,每个栈帧对应一个方法:

1. 虚拟机栈是线程私有的
2. 一个线程开始执行就让栈帧入栈,方法执行完栈帧就出栈,所以虚拟机栈不需要垃圾回收
3. 虚拟机栈存在OutOfMemoryError,StackOverFlowError(如果线程的堆栈大小超过分配的内存限制,则会 StackOverFlowError 抛出该错误)
4. 线程太多,就可能出现OutOfMemoryError,线程创建时没有足够的内存去创建虚拟机栈。
5. 方法的调用层次太多,就可能出现StackOverFlowError
6. 可以通过-Xss来设置虚拟机栈的大小

2.2.1 栈帧

C:\Users\陈禹衡\AppData\Roaming\Typora\typora-user-images\1677114215167.png

2.2.1.1 局部变量表

储存方法中的局部变量(注意:main()方法的话0位置是args),局部变量在给其赋值时才分配内存空间(创造)

2.2.1.2 操作数栈

Operand Stack ,也叫做操作栈,是栈帧的一部分,操作数栈是在执行字节码指令过程中用来进行计算的。

2.3 本地方法栈

本地方法:native method,在java中定义的方法,但是由其他语言实现

虚拟机栈存的是JAVA调用过程的栈帧,本地方法栈存的是本地方法调用过程中的栈帧。

也是线程私有的,有可能出现OOM , SOF

2.4 堆

2.4.1 介绍

堆是JVM中最重要的一块区域,JVM规范中规定所有的对象和数组都应该存放在堆中,在执行字节码指令时,会把创建的对象存入堆中,对象对应的引用地址存入JAVA方法栈中的栈帧中,不过当方法执行完后,刚刚所创建的对象并不会立马被回收,而是要等JVM后台执行GC后,对象才会被回收。

2.4.2 大小设置

-Xms: ms(memory start),指定堆的初始化内存大小,等价于-XX:IntialHeapSize

-Xmx:mx(memory max),指定堆的最大内存大小,等价于-XX:MaxHeapSize

一般会把-Xms和-Xmx设置为一样,这样JVM就不需要在GC后去修改堆的内存大小了,提高了效率,

默认情况下,初始化内存大小=物理内存大小/64

​ 最大内存大小=物理内存大小/4

2.4.3 新生代与老年代

新生代:刚刚创建出的对象

老年代:经过很多次垃圾回收后还存在的对象

可以通过-XX:NewRatio参数来设置新生代和老年代的比例,默认是2,表示新生代占有1,老年代占有2,也就是新生代占堆区总大小的1/3。

一般不需要调整,只有明确知道存活时间比较长的对象偏多,那么就需要调大NewRatio,从而调整老年代占比

Eden:伊甸园区,新对象都会先放到Eden区(除非对象的大小都超过了Eden区,那么就只能直接进入老年代)

SO,S1: Survivor0,Survivor1区,也可以叫做from区,to区,用来存放MinorGC(YGD,新生代垃圾清理,速度较快)后存在的对象

默认情况下比例关系为 Eden: S0:S1 8:1:1

可以通过-XX:SurvivorRatio来调整

2.4.3.1 GC种类

  1. Young GC/Minor GC:负责对新生代进行垃圾回收
  2. Old GC/Major GC:负责对老年代进行垃圾回收,目前只有CMS垃圾回收器会单独对老年代进行垃圾收集,其他垃圾回收器基本上都是整堆回收的时候对老年代进行垃圾收集
  3. Full GC:整堆回收,也会从堆方法区进行垃圾收集

2.5 方法区

与永久代的区别:不在虚拟机中,而是使用本地内存

元空间(metaspace)是方法区在Hotspot中的实现。方法区主要用于存储类的信息,常量池,方法数据,方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫做“非堆 ”

储存内容:

存储已被虚拟机加载的类型信息,常量,静态变量,及时编译器编译后的代码缓存

  1. 类型信息:对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
    ①这个类型的完整有效名称(全名=包名.类名)
    ②这个类型直接父类的完整有效名(对于interface或是java.lang.0bject,都没有父类)
    ③这个类型的修饰符(public, abstract,final的某个子集)
    ④这个类型直接接口的一个有序列表

垃圾回收

1. 为什么JVM要进行垃圾回收

垃圾是指在JVM中没有任何引用指向它的对象,如果不清理这些垃圾对象,那么它们就一直占用着内存,而不能给其他对象使用。最终垃圾对象会越来越多,就会出现OOM。

2. 垃圾回收阶段

2.1 垃圾标记阶段

也就是找到JVM(主要是堆中)有哪些垃圾对象,两种方式:

2.1.1 引用计数法

每个对象都保存一个引用计数器属性,用户记录对象被引用的次数。

优点:实现简单,计数器为0则表示是垃圾对象

缺点:

1. 需要额外的空间来存储引用计数
2. 以及需要额外的时间来维护引用计数
3. 无法处理循环引用问题![](https://img2023.cnblogs.com/blog/3097192/202303/3097192-20230301090228808-564601856.png)

2.1.2 可达性分析法

可达性分析法会以GC Roots作为起始点,然后一层一层再到所引用的对象,被找到的对象就是存活对象,那么其他不可达的对象就是垃圾对象

2.1.2.1 GC Roots

GC Roots是一组引用:

  1. 线程中java方法栈中正在执行的方法中的方法参数,局部变量所对应的对象引用
  2. 线程中本地方法栈中正在执行的方法中的方法参数,局部变量所对应的对象引用
  3. 方法区中保存的类信息中静态属性所对应的对象引用
  4. 方法区中保存的类信息中常量属性所对应的对象引用

3. 垃圾回收算法

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

一种非常基础和常用的垃圾回收算法,针对某块内存空间,比如新生代,老年代,如果可用内存不足后,就会STW(Stop-The-World,是在垃圾回收算法执行过程中,将JVM冻结,停顿的一种状态),暂停用户线程的执行,然后执行垃圾回收算法:(常用于新生代)

  1. 标记阶段:从GC Roots开始遍历,找到可达对象,并在对象头中记录
  2. 清除阶段:堆内存空间进行线性遍历,如果发现对象头中没有记录是可达记录,则回收它

缺点:

  1. 效率不高
  2. 内存碎片

优点:思路简单

3.2 复制算法(Copying)

将内存空间分为两块,每次指向一块,在进行垃圾回收时,将可达对象复制到另外没有被使用的内存块中,然后清除当前内存块中的所有对象,后续再按照同样的流程进行垃圾回收,交换着来。(常用于老年代)

缺点:

  1. 需要更多的内存,始终有一半内存空闲
  2. 对象复制后,对象存放的内存地址发生了变化,需要额外的时间修改栈帧中记录的引用地址。
  3. 如果可达对象比较多,垃圾对象比较少,那么复制算法的效率就会比较低。所以垃圾对象多的情况下,复制算法比较合适

优点:

  1. 没有标记和清除阶段,通过GC Roots找到可达对象,直接复制,不需要修改对象头,效率高
  2. 不会出现内存碎片

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

第一阶段和标记-清除算法一样,从GC Roots找到并标记可达对象

第二阶段将所有存活的对象移动到内存的一端,最后清理边界外的所有空间

相当于标记-清除算法执行完一次后再进行一次内存整理

缺点:

  1. 效率要低于标记-清除算法,复制算法
  2. 也需要修改栈帧中的引用地址

优点:

  1. 不会出现内存碎片
  2. 也不需要利用额外的内存空间

3.4 三种算法比较

3.5 分代收集算法

不同对象的存活时长是不一样的,也就可以针对不同的对象采取不同的垃圾回收算法

默认几乎所有的垃圾收集器都是采用分代收集算法进行垃圾回收的

  1. 新生代中的对象存活时间比较短,那么就可以利用复制算法,它适合垃圾对象比较多的情况。

  2. 老年代中垃圾对象存活时间比较长,所以不太适合用复制算法,可以使用标记-清除算法或标记-整理算法,比如:

    1. CMS(Content Management System 内存管理系统)垃圾收集器采用的就是标记-清除算法,
    2. Serial Old垃圾收集器采用的就是标记-整理算法。

4. 垃圾回收器

4.1 垃圾回收机制

目前商业虚拟机的垃圾收集器,大多遵循“分代收集”的理论进行设计,分代收集名为理论,实际上是一套符合大多数程序实际运行情况的经验法则。而分代收集理论,建立在三个分代假说之上:

  1. 弱分代假说
  2. 强分代假说
  3. 跨代引用假说

依据分代假说理论,垃圾回收可以分为以下几类:

  1. 新生代收集:目标为新生代的垃圾收集
  2. 老年代收集:目标为老年代的垃圾收集
  3. 混合收集:目标为整个新生代和部分老年代的垃圾收集
  4. 整堆收集:目标为整个堆和方法区的垃圾收集

在上述收集器中,常见的组合方式有:

1. Serial + Serial Old,是客户端模式下常用的收集器。 
2. ParNew + CMS,是服务端模式下常用的收集器。 
3. Parallel GC + Parallel Old GC,适用于后台运算而不需要太多交互的分析任务。

4.2 CMS

CMS整个垃圾收集过程更长了,但是STW的时间变短了,而且在垃圾收集过程中大部分时间用户线程也还在执行,所以用户体验变好了,但是吞吐量更低(单位时间内执行的用户线程更少)

4.2.1 流程:初始标记

  1. STW 暂停所有工作线程
  2. 然后标记出GC Roots能直接可达的对象
  3. 一旦标记完,就恢复工作,线程继续执行
  4. 这个阶段时间比较短

4.2.2 流程:并发标记

  1. 从上一个阶段标记出的对象,开始遍历整个老年代,标记出所有可达对象

  2. 耗时会比较长

  3. 但是不需要STW,用户线程和垃圾收集线程一起执行

  4. 三色标记

4.2.3 流程:重新标记

  1. 上个阶段标记的对象,可能有误差,需要进行修正
  2. 需要STW,但是时间也不是很长
  3. 增量更新

4.2.4 流程:并发清除

  1. 删除垃圾对象
  2. 由于不需要移动对象,这个阶段也可以和用户线程一起执行,不需要STW

4.2.5 存在问题

如果在并发标记,并发清理过程中,由于用户线程同时在执行,如果有新对象要进入老年代,但是空间又不够,那么就会导致"concurrent mode failture",此时就会利用Serial Old来做一次垃圾收集,就会中一次全局STW

在并发清理的过程中,可能产生新的垃圾,这些就是"浮动垃圾",只能等到下一次GC来清理

由于采用标记-清除算法,所以会产生内存碎片,

可以通过参数 -XX:+UseCMSCompactAtFullCollection可以让JVM在执行完标记-清除后再做一次整理,

也可以通过 -XX:CMSFullGCsBeforeCompaction来指定多少次GC后来做整理,默认是0,表示每次GC后都整理

4.3 G1(Garbage-First)

每一个方块叫做region,堆内存会分为2048个region,每个region的大小等于堆内存除以2048.还是分了Eden区,S0区,S1区,老年区,只不过空间是不连续的。Humongous区是专门用来存放大对象的(如果一个对象大小超过一个region50%,那么就是大对象)

流程-筛选回收:

  1. 需要STW,来清除垃圾对象
  2. 可以通过 -XX:MaxGCPauseMills来指定GC的STW停顿时间,所以可能并不会回收所有垃圾对象,默认200ms
  3. 采用复制算法,不会产生碎片(会把某个region里的垃圾对象复制到另外空闲的region区域,比如相邻的)

种类:

  1. YoungGC:Eden区满,就会触发G1的YoungGC,对Eden区进行gc
  2. MixedGC:老年代的占用率达到-XX:InitiatingHeapOccupancyPercent指定的百分比,回收部分新生代以及部分老年代,以及大对象区
  3. FullGC:在进行MixedGC过程中,采用的复制算法,如果复制过程中内存不够,则会触发FullGC,会STW,并采用单线程来进行标记-整理算法进行GC,相当于用了一次Serial GC
posted @   瓜洲渡雪  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示