硅基计划6.0 柒 JavaEE 浅谈JVM&GC垃圾回收


文章目录

  • 一、初始
  • 二、Java内存区域划分
    • [1. 运行时数据区划分](#1. 运行时数据区划分)
    • [2. 内存溢出](#2. 内存溢出)
    • [3. 补充](#3. 补充)
  • 三、Java类加载机制
    • [1. 类加载流程](#1. 类加载流程)
      • [1. 加载](#1. 加载)
      • [2. 验证](#2. 验证)
      • [3. 准备](#3. 准备)
      • [4. 针对字符串常量进行初始化](#4. 针对字符串常量进行初始化)
      • [5. 初始化](#5. 初始化)
    • [2. 类加载时机](#2. 类加载时机)
    • [3. 双亲委派模型](#3. 双亲委派模型)
    • [4. GC垃圾回收------重点](#4. GC垃圾回收——重点)
      • [1. GC步骤第一步------找垃圾](#1. GC步骤第一步——找垃圾)
        • [1. 方案壹------引入计数机制(非Java)](#1. 方案壹——引入计数机制(非Java))
        • [2. 方案贰------引入周期性可达性分析](#2. 方案贰——引入周期性可达性分析)
      • [2. GC步骤第二步------回收垃圾](#2. GC步骤第二步——回收垃圾)
        • [1. 方案壹------标记清除法](#1. 方案壹——标记清除法)
        • [2. 方案贰------复制算法](#2. 方案贰——复制算法)
        • [3. 方案叁------标记整理法](#3. 方案叁——标记整理法)
        • [4. 综合方案------分代回收](#4. 综合方案——分代回收)
        • [5. 其他方案------其他垃圾回收器](#5. 其他方案——其他垃圾回收器)

一、初始

为什么我们Java要引入Java虚拟机JVM呢,Java虚拟机又称为Java解释器/Java执行引擎

因为它充当着翻译官,可以很好的实现跨平台功能,并且更好地兼容操作系统和CPU

二、Java内存区域划分

在每一个Java的进程 中,都包含了一个JVM

JVM在启动的时候,就会向操作下申请一块内存空间,因此我们Java程序就可以利用这块内存空间去执行代码逻辑了

1. 运行时数据区划分

  1. 程序计数器:它是一片很小的内存区域,用来存放下一个Java字节码指令的地址

  2. 虚拟机栈:主要服务于Java程序,明确方法之间的调用关系、明确方法内部的局部变量,明确方法结束后返回上一层方法的位置、明确方法返回值,比如

    |fuc3|
    |fuc2|
    |fuc1|
    |main|<----每个方法就是一个栈帧

  3. 本地方法栈:给C++代码使用的,因为JVM底层是C++实现的,并且在有些方法底层调用的就是C++代码

  4. 堆:存储new出来的对象

  5. 元数据区/方法区:存储一些类的对象或者是静态成员和方法

2. 内存溢出

  1. 栈溢出:栈帧(方法)太多导致的,比如死递归
  2. 堆溢出:new的对象太多了

3. 补充

一个进程中只存在一份堆和元数据区,这就说明一个线程中new的对象可以被另一个线程引用

但是每个线程都有自己的程序计数器、本地方法栈、虚拟机栈

三、Java类加载机制

本质上就是把class文件读取到内存中并且构建的过程

1. 类加载流程

1. 加载

把class文件根据"全限定类名"找到对应的class文件,并且把文件数据读取到内存中

2. 验证

根据读取到的二进制内容判断其是否是一个正确的格式,我们打开Java官方文档中的虚拟机规范
文档链接

好,我们来逐个参数简单看下

  1. U4 maginc指的就是开头四个字节为魔幻数字,就是一种二进制文件的格式
  2. U2 minor_version&major_version指的就是主版本号和副版本号,描述class文件是经过哪个版本的Java编译器生成的
  3. U2 constant_poll_count&cp_info constant_pool指的就是常量池数量以及内部的数据格式cp_info
  4. U2 access_flags指的就是类访问权限
  5. U2 this_class指的就是当前类的编号
  6. U2 super_class指的就是父类的编号
  7. U2 interfaces_count&U2 interfaces[interfaces_count]指的就是描述实现类的接口以及其编号
  8. U2 fields_count&field_info fields[fields_count]指的就是描述类的属性、属性访问权限等等
  9. U2 methods_count&method_info methods[methods_count]指的就是方法的属性、方法的访问权限等等
  10. U2 attributes_count指的就是关于类的一些注解

3. 准备

给要去创建类的对象在JVM的元数据去分配一块内存空间,且默认把新申请的未初始化的设置为0

4. 针对字符串常量进行初始化

要把class文件中的字符串常量池加载到内存,此时我们的字符串就有了起始地址,后续在调用的时候就可以把字符串取出来

5. 初始化

初始化类静态成员、执行静态代码块、加载类等等

2. 类加载时机

本质上是一个懒汉模式,仅需加载一次并且在JVM中是单例存在的,那么什么时候会触发
new实例的时候、调用这个类的静态方法或者是静态成员、针对子类的父类加载

3. 双亲委派模型

在我们类加载流程第一步的时候,对于寻找class文件,涉及到三个模块的类加载器,之间存在父子关系
BootStrapClassLoader(爷):加载Java标准库的类
ExtensionClassLoader(父):加载扩展库类,一般是JDK厂商内置的,但现在少用了
ApplicaitonClassLoader(子):加载第三方库和项目中的类,也是整体的入口

它们之间的关系就是,对于一个类,先直接丢给爷爷去处理,处理不过就向下委派,如果最后还是没找到这个类,就会抛出类加载异常

4. GC垃圾回收------重点

在C语言中,内存泄露是一个非常严重的问题

如果我们进行malloc操作不及时进行释放,就会导致后续我们再进行内存分配的时候产生无内存可用的情况

而在我们Java中,通常会指派一些线程,周期性地对堆上的对象进行扫描,自动判定这个内存空间(对象)是不是不再使用

并且我们回收是针对一整个对象回收,并不存在回收半个的情况

如果不再使用就会自动释放,但是就是因为GC这种机制会有额外的内存和时间的开销,像C/C++这种追求效率的语言就不会采用

下面我们就来谈谈整体的回收流程

1. GC步骤第一步------找垃圾

GC是如何去判断这个对象不再进行使用的呢,答案就是看这个对象是否被强引用

那么如何去判断有没有被强引用呢,这里有几种方案,我们一一阐述

1. 方案壹------引入计数机制(非Java)

给每个对象安排一个空间,存放一个整数,围绕对象引用个数进行计数器更新

但是这会暴露出两个很大的问题

  1. 消耗更多哦内存空间
  2. 循环引用出现误判

我们针对循环引用的误判来做下解读

java 复制代码
class Test{
	Test t;
}

main{
	Test a = new Test();
	Test b = new Test();
	
	a.t = b;
	b.t = a;
	
	a = null;
	b = null;
}

如果我们画个图,就是这样子

当我想使用 0x100对象,发现是 0x200 引用的,那我去找0x200

但是我想使用0x200对象,发现是0x100引用的,那我去找0x100

嗯?不是死循环了吗,我这两个对象明明都使用不了

并且由于其计数器不为0,就不能被释放掉,就会持续占用越来越多的内存资源

2. 方案贰------引入周期性可达性分析

在Java的代码中,一系列的对象都存在类似于树形结构的关系

java 复制代码
class Test{A a = new A();B b = new B();}

也就是可能类似于这种结构

复制代码
  Test
 /    \
A      B
      / \
     C   D

我们所谓的周期性可达性分析,就是从一个或者多个根节点出发尽可能地遍历这棵树,凡事能经过的对象都标记为可达

结合JVM知道对象数量,减去可达到对象数量,剩下的就是不可达的对象了,并且这种扫描会周期性的进行

对于根节点GCRoots可能有一下几种

  1. 栈的局部变量,因为栈有很多个,每个栈的栈帧也有很多个,每个栈帧中局部变量也有很多个
  2. 常量池引用所指向的对象,可能也有很多个
  3. 所有引用类型的静态成员所指向的对象,也可能有很多个

2. GC步骤第二步------回收垃圾

1. 方案壹------标记清除法

如果采用直接释放,会导致内存碎片问题,当我们想申请一大片连续的内存空间时候

明明总的空闲内存加起来够,但是由于是碎片化的,并没有集中在一起,因此就申请失败

2. 方案贰------复制算法

把内存区域分成两份,同一时刻只使用一份

如果是那种无效对象(垃圾)就会留在原地,其余有效对象就被拷贝到内存的另一部分区域,最后再把原来的那一部分区域整体释放

下一次就会从右边开始,看哪些是"垃圾",把"垃圾"留下,有效对象就拷贝到内存的另一部分区域,最后再把原来的那一部分区域整体释放

我们画个图就是这样的

我们这两部分内存就会被反复使用,但是这样会导致空间利用效率很低,并且对象复制开销也很大

3. 方案叁------标记整理法

这就类似于我们顺序表中的删除中间元素的方法,即搬运

但是这么搞对象搬运的开销也可能会很大

4. 综合方案------分代回收

JVM会根据对象的情况/特点结合存活时间进行有针对性的回收

我们普遍认为,根据GC扫描轮数,如果某个对象年龄(存活时间)比较久,有很多概率会继续存活下去

因此我们把内存划分为了以下区域

  1. new的对象首先会在伊甸区中,大多数对象经过第一轮GC都会被淘汰
  2. 在伊甸区未淘汰的对象通过复制算法进入幸存区,同一时刻只使用一个幸存区
  3. 在幸存区会进行第二轮GC扫描,进一步淘汰一大批对象
  4. 如果在第二轮未被淘汰,则会进入另一个幸存区,重复GC扫描
  5. 当在两个幸存区都没有被GC扫描所淘汰,达到一定阈值,就会进入老年代
  6. 在老年代同样会进行GC扫描,只不过频次慢了些
  7. 这里说个例外情况:如果对象内存非常大则可能会直接进行老年代
5. 其他方案------其他垃圾回收器
  1. CMS------在多线程中尽可能扫描回收
  2. G1(G ONE)------针对内存空间特别大的情况,会把内存划分出更多区域,一次GC只针对一部分区域
  3. 2GC------目前处在实验性阶段,它会使得垃圾回收对于业务逻辑的响应时间更短,即开销更小,据说能达到0.1ms!

本篇文章属于是纯纯的八股文了


面试爱考,实际开发作用不大


END QAQ

相关推荐
whatever who cares2 小时前
在Java/Android中,List的属性和方法
android·java
不穿格子的程序员2 小时前
从零开始刷算法——二分-搜索旋转排序数组
数据结构·算法
原来是好奇心2 小时前
Spring Boot缓存实战:@Cacheable注解详解与性能优化
java·spring·mybatis·springboot
java_logo2 小时前
TOMCAT Docker 容器化部署指南
java·linux·运维·docker·容器·tomcat
麦克马2 小时前
Netty和Tomcat有什么区别
java·tomcat
程序员小假2 小时前
SQL 语句左连接右连接内连接如何使用,区别是什么?
java·后端
怕什么真理无穷2 小时前
C++_面试题_21_字符串操作
java·开发语言·c++
Lxinccode2 小时前
docker(25) : 银河麒麟 V10离线安装docker
java·docker·eureka·银河麒麟安装docker·银河麒麟安装compose
遇见火星2 小时前
LINUX的 jq命令行处理json字段指南
java·linux·json·jq