JVM的基本知识

JVM

JVM是java的虚拟机,是一个十分复杂的东西,所以掌握的要求比较高.本文主要是研究JVM的三大话题

  1. JVM内存划分
  2. JVM类加载
  3. JVM的垃圾回收

JVM内存划分

java程序要执行的时候,JVM会先申请一块空间,这里就涉及到JVM的内存划分

  • 堆 : 放的是new 出来的对象
  • 栈: 放的是方法之间的调用关系(栈中可以分为虚拟机栈和本地方法栈)
    1. 虚拟机栈: java中用来保存方法调用关系的内存空间
    2. 本地方法栈: jvm中C++写的代码,算是本地的方法的调用关系的内存空间
  • 方法区: 放的是类对象(加载好的类)
  • 程序计数器: 放的是下一个要执行的指令的地址

以上的内存划分都是在java1.7的时候

代码中的局部变量: 栈上

代码中的成员变量: 堆上

代码中的静态变量: 方法区

一个JVM进程中,堆和方法区都是只有一份的,栈和程序计数器每个线程都有自己的一份

常常会结合代码来判断内存划分

image-20230225142549220

t是局部变量,所以在栈上

x是成员变量,在堆上

y是静态变量,是在类对象中,就是在方法区中

一个误区: t是一个引用类型,是不是在堆上?其实不是的!,变量在哪个部分,主要就是看变量是什么变量(局部 成员 静态)

image-20230225143116038

这里的t2 是静态的,所以实在方法区中,new 后面的 Test2是在堆上的, xx是成员变量,所以是在堆上的

JVM的类加载

类加载是什么的

java程序在 运行之前需要先编译,也就是.java --> .class二进制字节码文件

在运行的时候,JVM会读取对应的.class文件,并解析内容,在内存在构造对象并进行初始化

简单来说,类加载是将类从文件中加载到内存中

类对象描述了这个类是什么样子的,有哪些属性(属性的名字 类型 访问权限)

有哪些方法(方法名字 参数个数 类型 返回值 访问权限)

继承自那个父类,实现了哪些接口

类对象是创建实例的具体依据

类加载的步骤

类加载大致能分成3个步骤

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化

加载: 找到.class文件,读取文件内容,并按照.class规范来解析

验证: 检查当前的.class里的内容格式是否符合要求

准备: 给类里的静态变量分配内存空间

解析: 初始化字符串常量,将符号引用替换成直接引用

所谓的符号应用就是占位符,直接引用就是内存地址

在.class文件中,会包含字符串常量,但是在类加载之前,字符串常量没有分配内存空间,得类加载之后才会有内存空间,没有内存空间也就没有字符创常量的真实地址,所以只能先使用一个占位符,等分配好内存之后再替换之前的占位符

初始化: 针对类进行初始化,初始化静态成员,并加载父类

何时触发一个类加载

使用到一个类的时候就会触发类加载(并不是程序一启动就会进行类加载,而是在使用的时候才会进行类加载) [类似于懒汉模式]

具体什么情况是使用呢?

  1. 创建这个类的实例
  2. 使用了这个类的静态属性或者静态方法
  3. 使用了类的子类(加载子类就会触发加载父类)

双亲委派模型

JVM加载类是由 类加载器 (class loader)来负责的

JVM自带了多个 类加载器的

Bootstrap ClassLoader

Extension ClassLoader

Application ClassLoader

这个三个类加载器负责不同的模块

Bootstrap ClassLoader 负责加载标准库中的类

Extension ClaaLoader 负责加载JVM拓展的库的类

Application ClassLoader 负责加载我们自己项目中的自定义类

描述上述三个类加载器如何相互配合的工作工程,就是双亲委派模型

  1. 上述的三个类加载器存在父子关系,其中Application ClassLoader是最小的子类
  2. 进行类加载时,输入的内容要是全限定类名(写完整的类名),比如: java.lang.Thread
  3. 加载的时候先从最小的子类Application ClassLoader开始,但是类加载器不会立刻扫描自己负责的路径,而是将任务委派给父 "类加载器"来处理

image-20230225162157597

image-20230225163324892

垃圾回收机制GC

GC是什么

在学习C语言的时候,创建内存有两种方式

直接定义变量,变量对应着内存空间,一旦出了作用域,就会释放

还有一种是malloc申请内存(动态内存申请),务必需要通过free来释放资源,要是真的忘了就会导致内存泄漏,十分危险

所以手动释放内存是很容易出事的

GC(垃圾回收机制)是一种主流的解决方案,在Java Python JS Go PHP中都存在GC垃圾回收机制

所谓的GC: 程序员只要负责申请内存,释放内存的工作直接交给JVM完成就行了

虽然GC很好用,但是GC也有问题,其中最大的问题就是GC会引入额外的开销(时间 + 空间)

时间上会存在STW问题(Stop The World),会导致卡顿

空间上会消耗额外的CPU和内存资源

GC回收哪部分

JVM的几个部分:

方法区: 类对象, 加载之后不太会卸载

栈: 出了作用域就会释放,所以不用回收

程序计数器: 固定的内存空间,不必回收

堆: GC主要的回收对象

image-20230225193453512

GC是怎么执行的

GC其实主要就是两步骤

  1. 先找出垃圾(看看谁是垃圾)
  2. 在回收垃圾(释放内存)

判定对象是不是垃圾

如果一个对象再也不会使用了,就算是垃圾了

在Java中,对象的使用,需要借助引用,要是一个对象,已经没有任何引用指向它了,说明了这个对象再也无法被使用了,就算是垃圾了

所以,判断是不是垃圾的最重要的就是,存在引用说明对象不是垃圾,要是引用不存在了就说明是垃圾

判断对象是否存在引用的两种方法是引用计数(不是JVM采用的方法,但是Python和PHP使用) 和 可达性分析(JVM中使用的方法)

所以要注意审题 : 确实问的是Java的垃圾回收机制还是"垃圾回收的机制",这里的两种方法是用来判断是不是垃圾

引用计数

给每个对象加上一个计数器,这个计数器表示当前的对象有几个引用

每当多一个引用指向该对象,计时器+1

每当少一个引用指向该对象,计数器-1(比如引用是一个局部变量,出了作用域 或者引用是一个成员变量,所在的对象被销毁了)

当计数器变成0的时候,说明此时已经没有引用指向这个对象了,所以就可以释放内存了

引用计数的优点: 简单 容易实现 执行效率也比较高

应用计数的缺点:

  1. 空间利用率地,尤其是对于小对象而言. 要是一个小对象中只有一个int的成员,结果还有拿出一个int的空间给计数器
  2. 可能会出现循环引用的情况

image-20230225200445408

这两个对象实现了循环相互调用,这样子最后计数器就是1了,但是也没有引用能指向它们,所以就不能被释放

可行性分析[JVM采用的方法]

约定一些特定的变量来作为GC roots

每个一段时间,从GC roots出发,进行遍历,看看哪些变量能被访问到,能访问到的变量就算是"可达"

这里的GC roots可以是栈上的变量 / 常量池引用的对象 / 方法区,引用类型的静态变量

image-20230225201246133

在找到 垃圾之后该怎么回收垃圾?

具体回收垃圾有4中方法

  • 标记清除
  • 复制算法
  • 标记整理
  • 分代回收

标记清除

image-20230225202546689

在发现哪些是垃圾之后,键对象对应的内存空间释放

简单粗暴,但是有一个最大的问题,就是会导致产生内存碎片,加上上图中的深色的垃圾每个占1KB,清除完之后我想要申请一个2KB的的空间都申请不到,因为此时内存都是碎片的,没有连在一起的空间

复制算法

image-20230225204242741复制算法虽然能解决内存碎片的问题,但是缺点也是很明显的

复制算法的缺点:

  • 空间利用率更低了(每次都是只用一半的内存)
  • 一轮GC下来,万一大部分对象都是要保留的,只有少部分的对象要回收,这个时候复制的开销就会很大

标记整理

首先使用可达性分析,标记出所有需要回收的对象,标记之后不会立刻清理,而是将所有存活对象都移动到内存的一端,移动结束之后直接清理掉剩余部分

保证整理的方法类似于顺序表 删除/覆盖元素 主要是搬运操作

image-20230225205202666

标记整理的空间利用率提高了,也能解决内存碎片的问题,但是搬运操作也是比较耗时的

分代回收

分代回收将上面的复制算法和标记整理综合了一下,根据 对象的不同特点来采取不同的回收方式,这里的对象特点主要是指对象的年龄

对象的年龄是根据GC的轮次来的

GC 就是一组线程,周期性扫描代码中的所有对象,要是一个对象经历了一次GC,没有被回收,它的年龄就要+1

一个基本的经验规律:

如果一个对象的寿命比较长,大概率就还会活的根据(要死早就死了,能活下来,说明生命力还比较旺盛)

针对以上的经验,将对象分成新生代(minor GC)(GC扫描的评率更高)和老年代(full GC / major GC)(GC扫描的频率更高)

image-20230225212354849

新生代(minor GC): 刚刚被创建出来的新对象,往往很容易朝生夕死,很多对象都熬不过一轮GC

新对象会进入到伊甸区,要是新对象能坚持过一轮GC, 没挂,就会通过复制算法,复制到生存区

进入到生存区之后,每熬过一次GC,就会通过复制算法拷贝到另一个拷贝去,要是这个对象能一直不消亡 , 就会在两个生存区栈反复拷贝,每次GC都会筛选掉很多的对象

要是一个对象能在生存区中坚持了很多轮GC,还不挂,则进入到老年代(full GC / major GC )

当对象来到老年代,GC也还是会有的,只是频率低很多,这里每轮GC使用的是标记整理的方式来处理老年代对象

分代回收的过程,非常像找工作的情况

总结一下:

  1. 判断垃圾
    • 引用计数
    • 可达性分析
  2. 进行回收
    • 标记清除–>内存碎片
    • 复制算法–>浪费空间大
    • 标记整理–>类似于顺序表搬运元素,时间比较长
    • 分代回收–>因地制宜完成回收
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值