JavaEE初阶复习(JVM篇)

发布于:2025-04-05 ⋅ 阅读:(14) ⋅ 点赞:(0)

JVM 

Java虚拟机

jdk java开发工具包

jre java运行时环境

jvm java虚拟机(解释执行 java 字节码)

java作为一个半解释,半编译的语言,可以做到跨平台. java 通过javac把.java文件=>.class文件(字节码文件)

字节码文件, 包含的就是java字节码, jvm把字节码进行翻译转化为不同系统上可以识别的cpu指令.

JVM的内存划分(面试题)

JVM本质上是一个进程

进程运行中, 要从操作系统这里申请一些资源(内存就是核心的资源)

JVM作为一个进程, 从系统中申请了一大块内存, 这一大块内存给java程序使用的时候, 又会根据实际的使用用途来划分出不同的空间(比如java定义变量的时候,就是使用JVM从系统这边申请到的内存)

JVM划分各区域的解释

1> 堆 代码中new出来的对象就都是在堆里面. 对象中持有的非静态成员变量, 也就是在堆里面(后面GC主要就是回收这里的引用)

2> 栈 

本地方法栈: jvm内部, 通过c++代码的调用关系和局部变量

虚拟机栈: 记录了java代码的调用关系和java里面的局部变量

此处的堆和栈和数据结构里面的不一样

3> 程序计数器: 主要存储下一条要执行的 java 指令的地址(每个线程都有自己的程序计数器和栈)

4> 元数据区(之前叫做方法区): 这里放一些辅助性,描述性质的属性(比如在硬盘上保存数据的本体,还有一些辅助信息: 文件的大小, 文件的位置,文件的拥有者,文件的修改时间...).比如类的信息,方法的信息(一个程序有哪些类, 每个类里面又包含哪些方法, 每个方法里面包含哪些指令.

堆, 元数据区(整个进程有一份) 栈, 程序计数器(每个线程有一份)

常考的笔试题

JVM的类加载机制(面试题)

类加载: java进程运行的时候, 需要把.class文件从硬盘读取到内存, 并进行一系列解析的校验解析的过程.

类加载的过程: 

1> 加载 把硬盘上的 .class 文件找到, 打开文件, 读取到文件的内容.(二进制的数据)

2> 验证 确认当前读到的文件内容是合法的 .class 文件(字节码) 格式.(里面有jvm开发的版本信息, 高版本的可以运行低版本的 .class)(校验.class 文件的格式是否符合 JVM 规范要求

3> 准备 给类对象, 申请内存空间(此时申请到的内存空间, 里面的默认值, 全都是0)

4> 解析 针对类中的字符串常量进行管理(java虚拟机将常量池里面的 符号引用 替换为 直接引用

的过程) 

class Test{ private String s = "hello"}  s里面包含的是"hello"的内存地址, 地址存的是内存的地址, 但是此时我们访问的是.class文件, 文件里面不存在地址的概念,  文件是放在硬盘上的, 因此我们.class文件里面的s存的是"hello"的相对偏移量,此时文件中填充的s的"hello"偏移量就是 符号引用 ,后续我们把.class放在内存里面,就会把"hello"加载到内存中, 此时"hello"就有地址了,此时内存中的s保存的是"hello"的内存地址, 此时就是 直接引用

5> 初始化: 针对类对象完成后续的初始化(执行静态代码的逻辑), 对对象的各个部分的属性进行赋值填充

双亲委派模型(加载环节)

描述怎么查找 .class 文件的策略

JVM的几个类加载器

BootstrapClassLoader: 负责查找标准库的目录

ExtensionClassLoader: 负责查找扩展库的目录( java 规范里面描述了标准库中应该有哪些功能)

ApplicationClassLoader: 负责查找当前项目的代码目录, 以及第三方库的目录

从下面开始,逐层先把搜索任务交给上层,直到没有上层为止, 然后从最上层开始进行搜索,如果找到了就进入打开文件, 读文件的操作.如果没有搜索到就去下层目录开始找.以此往下到最后一层, 如果没有找到就抛出 ClassNotFoundException 异常

JVM的垃圾回收算法(GC)面试题

我们主要GC的区域就是堆(new 对象的区域)

垃圾回收,主要回收的是对象, 每次垃圾回收的时候, 释放若干个对象

 垃圾回收机制

1> 识别出垃圾: 哪些是垃圾, 哪些不是垃圾(对象没有引用了,匿名对象除外)

判定整个对象后续是否需要继续使用(看整个对象是否被引用),如果一个对象都没有引用指向他, 就视为无法被代码中使用, 就可以视作垃圾.

创建的对象是放在堆, 引用的关系存放在栈

多个对象引用

如何计算引用?

1. 引入计数器 给每个对象安排一个额外的空间, 空间里面要保存当前整个对象有几个引用.

此时的垃圾回收机制,就是看整个引用计数是否为0,是0就可以释放了

问题1: 消耗额外的内存空间, 我们需要给每个对象都安排一个计数器

问题2: 引用计数可能会产生" 循环引用的问题 " . 此时, 引用计数就无法正确工作了

2. 可达性分析(JVM用的是这个)

相比于消耗一个空间来计算引用数目, 我们用时间来换空间

在写代码的时候, 会写很多的变量, 此时我们以这些对象变量为起点, 根据引用关系向下搜索,所有能够被搜索到的对象就不是垃圾了,搜索一圈也没有访问到的对象,就是垃圾.

比如我有若干个结点, 通过引用关系来构成二叉树, 我们从根结点开始遍历, 遍历它的左子树, 遍历完后遍历右子树, 直到叶子结点没有子节点为止, 此时我们遍历到的结点都不是垃圾 ,如果我们把某个结点的 left设置为null, left之前结点就遍历不到了, 此时就是不可达, 就是垃圾

2> 把标记为垃圾的对象的内存空间进行释放

释放的方式

a> 标记-清除

把标记为垃圾的对象直接释放掉(会产生内存碎片的问题: 产生很多小的 离散的 空闲内存空, 我们申请内存空间是申请的一块连续的内存空间)

b> 复制算法

不直接释放内存, 而是把不是垃圾的对象复制到内存的另一半里面, 然后释放掉原先一半的内存空间(总的内存变少了, 每次复制的对象如果很多, 那么复制的开销就会很大)

c> 标记 - 整理 

类似于 顺序表 删除中间元素(搬运)

分带回收

引入概念, 对象的年龄

JVM 中有专门的线程负责周期性扫描/释放

一个对象, 如果被线程扫描了一次, 就不是垃圾, 年龄+1(初始年龄是0)

JVM 中会根据对象年龄的差异, 把整个堆的内存分成俩大部分: 新生代(年龄小的对象) / 老年代(年龄大的对象)

新生代又分为伊甸区,生存区, 幸存区

1> 当new出一个新的对象, 就放在伊甸区.(伊甸区就会有, 很多的对象)

2> 第一轮GC: 之后大部分的对象都没了, 还存在的对象会被使用复制算法拷贝放在生存区

后续的GC扫描线程伊甸区和生存去都会扫描, 然后进行垃圾清理, 在生存区存活下来的对象会使用复制算法放在幸存区里面,每一次GC的扫描,对象的年龄都会+1

3> 如果在生存区里面经历若干轮GC, 还存在的对象, 就会被拷贝到老年区

4> 老年区扫描的频次大大低于其他区域, 扫描线程主要扫描的还是新手区

5> 在老年区里面的对象没有引用后, 就会被JVM按照标记整理的方式进行搬运处理掉.

垃圾收集器