硅基计划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

相关推荐
阿杰同学几秒前
Java 设计模式 面试题及答案整理,最新面试题
java·开发语言·设计模式
这样の我几秒前
java 模拟chrome指纹 处理tls extension顺序
java·开发语言·chrome
Genevieve_xiao5 分钟前
【数据结构与算法】【xjtuse】面向考纲学习(下)
java·数据结构·学习·算法
4311媒体网7 分钟前
php和c++哪个更好学?C++难学吗?
java·c++·php
仰泳的熊猫9 分钟前
1031 Hello World for U
数据结构·c++·算法·pat考试
毕设源码-朱学姐12 分钟前
【开题答辩全过程】以 基于SpringBoot的流行音乐网站的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
jiayong2315 分钟前
Spring 框架完全指南
java·后端·spring
高山上有一只小老虎16 分钟前
小红的正整数计数
java·算法
AnAnCode16 分钟前
【时间轮算法-实战】Java基于Netty的 `HashedWheelTimer`快速搭建时间轮算法系统
java·开发语言·算法·时间轮算法
liu****26 分钟前
12.C语言内存相关函数
c语言·开发语言·数据结构·c++·算法