JVM相关

1.JVM内存区域

一个运行起来的java进程就是一个Java虚拟机,就需要从操作系统中申请一大块内存。

内存中会根据作用的不同被划分成不同的区域:

(1)栈:存储的内容是代码在执行过程中,方法之间的调用关系(栈中每一个元素就是一个栈帧,代表一个方法的调用,包含方法的形参,返回值,局部变量)。

(2)堆:这里存储的内容是代码中new的对象

(3)方法区(1.8开始:元数据区):存储类对象(.class文件加载到内存就成为了类对象)

(4)程序计数器:存放每个线程,下一条指令执行的地址。空间较小主要就是用来存放一个"地址",表示下一条要执行的指令,在内存的哪个地方(在方法区中去找下一条指令,指令是以二进制的形式存储在类对象中)。

程序计数器中的值会随着指令的执行,从而自动进行更新,去指向下一条指令。

如果是顺序执行的代码,那么下一条执行的指令地址就是把指令地址进行递增。

如果是条件循环执行的代码,那么下一条执行的指令地址就可能会跳到比较远的地址。

ps:栈和程序计数器每一个java线程一份,而堆和方法区是每一个java进程一份。(仅限java,放在c++中就不一定是这样了)

2.JVM类加载

(1)基本流程:

java代码经过编译后生成.class文件,java程序要想运行起来就需要将jvm读取这个.class文件,并且把里面的内容构造成类对象,保存到内存的方法区中。

官方文档中把类加载的过程分成了5个步骤:

1)加载:找到.class文件,并且打开文件,读取文件中的内容(通过全限定名称查找)

2)验证:检查当前二进制文件是否符合格式的要求,具体格式

3)准备:给类对象分配内存空间,并且为不被final修饰的static变量赋初始默认值(被final修饰的变量在此时会被赋值成程序员给定的值)

4)解析:针对类对象中字符串常量的处理,进行了一些初始化的操作。(符号引用到直接引用)

字符串常量在经过编译之后,eg:final String s = "hello",s本来是存储内存地址,但在.class文件中会重新针对该字符串进行创建出一个引用,此时这个引用就不是存储内存地址了,而是存储文件偏移量(字符串长度),通过这个变量就可以知道目前字符串处于哪个位置。到解析的阶段(类加载的时候),会重新将这个引用替换成内存地址。

5)初始化:对类对象中的各种属性进行初始化,还需执行静态代码块和加载一下父类(子类中调用构造方法,会先帮助父类进行构造)

(2)双亲委派模型:

是在加载过程中的第一个步骤。

首先需要了解一下类加载器

是JVM中的一个模块,JVM内置了三个类加载器:

1)BootStrap ClassLoader

2)Extension ClassLoader

3)Application ClassLoader

这三个类加载器之间的关系就好似爷父子之间的关系,但不是靠继承构成的,而是由类加载中的一个属性(parent)来指向父类加载器。

加载的具体流程是:

①根据给定的全限定类名,找到对应的.class文件。

②从Application ClassLoader作为入口,开始执行查找的逻辑。

③Application ClassLoader不会立即扫描自己所负责的目录(负责搜索项目当前目录和第三方库的对应目录),而是会交给自己的父类加载器Extension ClassLoader。

④Extension ClassLoader不会立即扫描自己所负责的目录(负责搜索JDK中一些扩展库对应的目录,是JDK标准库之外的一些库,属于对JDK标准库的扩展),而是会交给自己的父类加载器BootStrap ClassLoader。

⑤BootStrap ClassLoader,也不会立即扫描自己所负责的目录(负责搜索JDK标准库对应目录),而是会交给自己的父类加载器,但是却发现没有父类加载器,因此只能区扫描自己负责的目录,一旦在标准库中查找到对应类的.class文件,此时加载的过程就完了,扫描结束过后如果没有扫描到,就会交给它的子类加载器(Extension ClassLoader)扫描。

⑥如果在Extension ClassLoader扫描到了就结束加载过程,没有扫描到就交给它的子类加载器(Application ClassLoader)扫描。

⑦如果在Application ClassLoader扫描到了就结束加载过程,没有扫描到此时应该交给它的子类加载器扫描,但是发现没有了,所以此时会抛出一个异常ClassNotFoundException

总结:所谓的双亲委派模型其实就是一个按照优先级查找.class文件的一个过程,之所以有这么一套流程,是为了确保JDK标准库扫描的优先级最高,其次是扩展库,最后才是项目当前目录和引入的第三方库对应目录。就好比:你在自己的代码中使用String(导入的包是java.lang.String),在类加载的时候加载的JDK标准库中的类,而不是自己写的这个类。

3.JVM垃圾回收机制(GC垃圾回收)

在java中new对象,是动态申请内存(运行时分配),如果一个资源申请了内存空间,长时间不使用但是不释放,就可能会造成内存泄漏。

在java中给出了一个方案,也就是垃圾回收机制,让JVM,自动判定某个内存是否不再使用了,

如果后面这个内存确实不用了,JVM就会自动回收把这个内存给回收掉了,此时就不需要手动回收了。

GC是垃圾回收,GC回收的目标是内存中的对象。对于java来说就是释放堆上的new出的对象,栈上的对象是随着栈帧的生命周期(方法执行结束,栈帧自然销毁,内存自然释放),静态变量,生命周期是整个程序,这个始终存在就意味着静态变量无需释放的,真正释放的就是堆上的对象。

GC回收垃圾的过程主要有两个步骤:

(1)找到垃圾

有两种主流的方案:

①引用计数

new出来的对象,会单独划分空间,来保存有一个计数器,计的是当前有多少引用指向该对象。

如果一个对象没有引用指向了(即引用计数为0),就可以将该对象视为垃圾了。

但是使用该种方式可能会存在两种问题:

**·**比较浪费内存:

计数器怎么说也得有两个字节,如果对象本身比较小,那么此时这个计数器所占空间比例就比较大了。

**·**存在循环引用问题:

②可达性分析:

有一组线程,周期性扫描代码中的所有对象,把所有可以访问到的对象,都给标记成"可达",反之,如果经过扫描后,不可达的对象,就成垃圾了,需要被回收。

eg:

当然这里的遍历不一定是二叉搜索树,这里可达的实现大概率是靠N叉搜索树实现,这一步就是针对当前对象,看对象中有多少不同引用类型的成员,然后再对每一个类型的成员进行进一步的遍历。

通过上述过程,不难看出可达性分析是比较耗费资源的(开销较大)。

(2)回收垃圾

有三种基本的回收思路:

①标记删除:

扫描到一个不可达的对象,就直接进行释放,这个方案非常不好,那就是会产生很多的内存碎片。释放代码,是为了让其他代码能够申请一块连续的内存空间,随着时间的推移,内存碎片的情况就会愈演愈烈,就会导致后续申请连续内存空间变得困难。

②复制算法:

通过复制的方式,把有效的对象,归类到一起,在同一释放剩下的对象。

也就是把内存一分为二,一次只用其中一半。但这种方式有两个明显的问题:

a.内存利用率不高

b.如果有效的对象很多,那么复制的开销将会很大

③标记整理:

既能解决内存碎片的问题,又能够解决上述内存利用率不高的问题。

eg:

类似于顺序表删除元素的搬运操作,使用该种方式搬运开销仍然很大。

而JVM中GC垃圾回收主要思想是分代回收(具体实现可能有一些调整和优化),是对上述思路的集合,让不同的方案,扬长避短。

分代回收有一个很重要机制就是:对象能活过的GC扫描轮次越多,就是越老(代表当前对象是暂时不会被释放的)

eg:下图是整个分代回收的全部过程:

相关推荐
找不到、了23 分钟前
JVM核心知识整理《1》
jvm
L.EscaRC2 小时前
面向 Spring Boot 的 JVM 深度解析
jvm·spring boot·后端
学到头秃的suhian19 小时前
JVM-类加载机制
java·jvm
NEFU AB-IN1 天前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海1 天前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗1 天前
JVM整理
jvm
echoyu.1 天前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考2 天前
JVM中内存管理的策略
java·jvm
thginWalker2 天前
深入浅出 Java 虚拟机之进阶部分
jvm
沐浴露z2 天前
【JVM】详解 线程与协程
java·jvm