JVM(内存划分+类加载+GC)

目录

🐲 1. JVM 内存划分

🐲 2. JVM 类加载

🦄 2.1 类型加载是干啥的

🦄 2.2 类加载的简略流程           

🦄 2.3 什么时候会进行类加载

🦄 2.4 双亲委派模型

🐲 3. GC 垃圾回收机制

🦄 3.1 GC 是什么

🦄 3.2 GC 回收哪部分内存

🦄 3.3 GC 具体是怎么回收的

🦖 3.3.1 怎么判定某个对象是否是垃圾 (引用计数/可达性分析) 

🦖 3.3.2 具体是怎么回收的 (标记清除/复制算法/标记整理/分代回收)


JVM是一个比较大的话题,但面试主要从这三个方面考

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

🐲 1. JVM 内存划分

Java程序, 就是一个名字为 Java 的进程. 这个进程就是所说的 "JVM"

JVM 就会先从操作系统这里申请一大块内存空间,在这个基础上再把这个内存空间划分成几个小的区域

区域划分:

  1. 堆: 放的是 new 的对象
  2. 方法区: 放的是 类对象 (加载好的类)
  3. 栈: 放的是方法之间的调用关系 (虚拟机栈: java 里面用来保存调用关系的内存空间;         本地方法栈: 本地方法,也就是 JVM 内部 C++ 写的代码,调用关系的内存空间)
  4. 程序计数器: 放的是下一个要执行的指令的地址 


🐲 2. JVM 类加载

🦄 2.1 类型加载是干啥的

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

运行的时候,   .java进程(JVM) 就会读取对应的   .class文件,并且解析内容,在内存中构造出类对象并进行初始化

类 从 文件 加载到 内存 中

 类对象: 在前面中类对象出现在反射,jackson,synchronized 中

主要就是描述了这个类是啥样子的

有哪些属性(属性名字,类型,private/public)

有哪些方法(方法名字,参数个数,类型,返回值类型,private/public)

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

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


🦄 2.2 类加载的简略流程           

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

2. 连接

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

                   (不同版本的 JDK 得到的 .class 是不兼容的)

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

     3. 解析: 初始化字符串变量,把 符号引用(占位符)  替换成 直接引用(内存地址) 

3. 初始化: 针对类进行初始化, 初始化静态成员, 执行静态代码块, 并且加载父类

🦄 2.3 什么时候会进行类加载

使用到一个类的时候,就触发类加载

(类并不一定是程序一启动就加载了,而是第一次使用才加载 [有点类似于懒汉模式])

使用一个类,这里指的是

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

🦄 2.4 双亲委派模型

决定了按照啥样的规则来在哪些目录里去找 .class 文件

也就是描述了加载器相互配合的工作过程,就是双亲委派模型

JVM 加载类, 是由 类加载器 (class loader) 这样的模块来负责的

JVM 自带了多个类加载器,每个类加载器各自负责一个区域

  1. Bootstrap ClassLoader (负责加载标准库中的类)
  2. Extension ClassLoader (负责加载 JVM 扩展库的类(语言规范中没有,但是JVM实现的))
  3. Application ClassLoader (负责加载我们自己项目中的自定义的类)

按照这样的顺序加载, 最大的好处在于

如果我们自己写个类,刚好 全限定类名和标准库中的类名冲突了,(比如我们自己写个类就叫,java.lang.Thread)

此时仍然可以保证类加载时,可以加载到标准库的类,防止代码加载错了带来问题 


🐲 3. GC 垃圾回收机制

🦄 3.1 GC 是什么

GC (垃圾回收) 是一个主流的内存回收的方案, Java/Python/JS/GO/PHP 都是使用GC

我们只需要负责申请内存, 而释放内存的工作, 交给 JVM 来完成, JVM 会自动判定当前的内容是啥时候需要释放, 当认为这个内存不再使用了, 就自动释放了


🦄 3.2 GC 回收哪部分内存

JVM 主要内存分为这几个部分

  • 堆 : 存放的时 new 的对象 (GC 主要就是针对堆来进行回收)
  • 栈 : 保存方法之间的调用关系(释放时机确定, 不必回收)
  • 方法区 : 放的是 类对象 (类加载, 加载之后也不太会卸载)
  • 程序计数器 : 保存下一条要执行的指令地址 (固定内存空间, 不必回收)


🦄 3.3 GC 具体是怎么回收的

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

🦖 3.3.1 怎么判定某个对象是否是垃圾 (引用计数/可达性分析) 

如果一个对象不再用了,就说明是垃圾了

在 Java 中,对象的使用,需要凭借 引用

假设如果有一个对象,已经没有任何引用能够指向他了,这个对象自然也就无法再被使用了

所以最关键的要点就是:

通过引用来判定当

前对象是否还能被使用了,没有引用指向就视为是无法被使用

两种常见的,判定对象是否存在引用的方法

1. 引用计数 [不是 JVM 采取的方法, 比如 Python,PHP]

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

当引用计数器, 数值为 0 时, 就说明当前这个对象已经无人能够使用了,此时就可以进行释放了

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

缺点:

  (1) 空间利用率比较低, 尤其是小对象

       (比如,计数器是个 int, 如果你的对象本身里面只有一个 int 成员,利用率低)

  (2) 可能会出现循环引用的情况 

2. 可达性分析 [是 JVM 采用的方法, java]

约定一些特定的变量,成为 "GC roots"

每隔一段时间,从 GC toots 出发, 进行遍历, 看看当前哪些变量是能够被访问到的

能被访问到的遍历就称为是 "可达" (否则就是 "不可达")


🦖 3.3.2 具体是怎么回收的 (标记清除/复制算法/标记整理/分代回收)

  1. 标记清除
  2. 复制算法
  3. 标记整理
  4. 分代回收

(1) 标记清除

这种方式有一个最大的问题: 内存碎片

会导致整个内存 "支离破碎"

比如假设上述每个深色的区域是 1k,此时整个有 4k 空闲空间,但是由于此时内存是分散的,导致如果想申请 2k 的内存空间,是申请不了的

(2) 复制算法

复制算法,可以很好的解决 标记清除 带来的 内存碎片问题

但是 复制算法 的缺点是:

  1. 空间利用率更低了 (用一半丢一半)
  2. 如果一轮 GC 过去,大部分对象要保留,只有少部分对象要回收,此时这个复制的开销就很大了

(3) 标记整理

类似于 顺序表 删除元素,   标记整理主要就是  搬运操作

这个方式,相对于上述的复制算法来说,空间利用率比之前高,同时也还是能够解决内存碎片问题,但是搬运操作是比较耗时的

(4) 分代回收

上面的三种方式,都是很非常好的方法,都有各自的特点

所以就需要根据实际的场景,选择对应的解决方法

这就有了 "分代回收" 的策略, 就是把上面的方法都综合在一起

根据对象不同的特点,采取不同的回收方式

这里的根据对象不同的特点: 是根据对象的年龄(依据 GC 的轮次来算的)来划分的

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

根据对象的年龄进行分类,把堆中的对象分为了,

新生代(年龄小的对象)    和    老年代 (年龄大的对象)

但是还有个特殊情况就是

如果对象是一个非常大的对象,则直接进入老年代

因为对一个大的对象进行复制算法,开销太大了

并且这是一个比较大的对象, 既然能创建这么大的对象,那肯定也不是立即就销毁的 

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>