JVM基本机制

JVM内存划分

JVM也就是Java进程,这个进程一旦跑起来之后机会向操作系统这里申请一大片空间,JVM会进一步对这个空间进行划分,划分成不同的区域,从而每个区域都有不同的功能作用。

1.堆(heap)

整个内存区域中,最大的区域,放的是代码中new出来的对象(成员变量)。

2.栈(stack)

JVM虚拟机栈:保存了方法的调用关系。

本地方法栈:方法的调用关系,使用栈来维护。

3.元数据区

以前叫方法区,Java8改名。

存放类对象:代码中写的每个类,在JVM上运行的时候,都会有对于类对象。

方法相关信息:类有一些方法,每个方法都代表了一系列指令合集。

常量池:字面常量值。

4.程序计数器

内存区域中最小的区域,保存了当前要执行的下一条指令(JVM字节码)的地址,这个地址就是元数据区里的一个地址。

基本原则:

一个对象在那个区域,取决于对应变量的形态:

局部变量: 堆上

成员变量: 堆上

静态成员变量:方法区/元数据区

上述四个区域中,堆和元数据区,是整个进程只有一份,栈和程序计数器,是每个线程都有一份的。

类加载过程

编写java代码后硬盘上存储的是.java文件,一个java进程要运行起来(执行cpu指令,都是通过字节码让JVM翻译出来),就需要把.java文件变成.class文件,加载到内存中,得到类对象。

类加载的几个环节:

1.加载:在硬盘上,找到对应的.class文件,读取文件内容。

2.验证:检查.class里的内容,是否符合要求(.class文件格式在java的官方文档中有明确定义)。

3.准备:给类对象分配内存空间(类加载最终得到的就是类对象),会把元数据区中的这块空间全部填充成0。

4.解析:针对字符串常量来初始化,把刚才.class文件中的常量内容取出来,放到元数据区。

5.初始化:针对类对象进行初始化,给静态成员进行初始化,执行静态代码块。

后续代码就可以使用这个类对象,创建实例,或者使用里面的静态成员了。

双亲委派模型

描述了JVM加载.class文件过程中寻找 文件的过程。双亲委派模型出现在类加载的第一步------加载,根据代码中给出的全限定类名找到对应的.class文件。

类加载器:在JVM中包含的一个特定的模块/类,这个类负责完成后续的加载工作。

JVM中内置了三个类加载器,负责加载不同的类。

BootstrapClassLoader:负责加载标准库的类。(爷爷)

ExtentionClassLoader:负责加载JVM拓展库的类。(父亲)

ApplicationClassLoader:负责加载第三方库的类和程序员自己写的类。(儿子)

此处的"父子关系"不是通过继承表示的,而是通过类加载器中存在一个"parent"这样的字段,指向自己的父亲。类似于二叉树的三叉实现形式。

加载过程

加载在全限定类名ava.Test中,自己写的一个类

***1.工作从ApplicationClassLoader开始进行,*它不会立即去搜索第三方库的相关目录,而是把任务交给自己的 父亲进行处理。

***2.工作来到了ExtentionClassLoader,*它也不会立即去搜索自己负责的拓展库的目录,也是把任务交给自己的父亲去处理。

***3.工作最后到了BootstrapClassLoader,*BootstrapClassLoader的parent指向null,只能自己去处理,BootstrapClassLoader尝试在标准库的路径中搜索上述类,如果这个类在标准库中找到了,于是搜索过程就完成了,类加载器负责打开文件,读取文件等后续操作就行了。如果没找到,任务就丢给自己的儿子去处理。

***4.工作回到了ExtentionClassLoader,*此时搜索拓展库对应目录,找到该目录 ,就由当前类加载器负责打开文件,读取文件等后续操作,如果没找到,继续还给儿子去处理。

***5.工作目录回到ApplicationClassLoader,*此时搜索第三方库/用户项目代码目录,找到了,也是由当前类加载器负责处理。没找到则会抛出异常ClassNotFoundException。

双亲委派模型,拿到任务后先交给父亲处理,处理不了再自己处理。

GC垃圾回收机制

GC回收的是"内存",更准确的来说是堆上的内存。每次回收都是一个完整的对象,不能回收帮对象(一个 对象有几个成员就要全部回收,不能只回收部分)。

找到需要回收的垃圾(不被使用的对象)。

一个对象的创建时机是明确的,但什么时候不再使用,往往是模糊的,在编程中一定要确保代码中使用的每个对象都得是有效的,不能出现"提前释放 "的情况。因此判断一个对象是否是垃圾,判定的方式是比较保守的,判定某个对象,是否存在引用指向它

使用对象都是通过引用的方式来使用的,如果没有引用指向这个对象,意味着这个对象注定无法在代码中被使用。如果没有引用指向的对象就可以视为垃圾了。

对于上述代码来说,修改了m的指向,此时new Main()的对象就没有引用指向了,这个对象就可以被认为是垃圾。

判定某个对象是否有引用指向,这里介绍两种方法:

1.引用计数(PHP/PPython的方案)

在对象本体上引入计数器,给引用赋值之前,先根据引用找到对象,并更新计数器,判断计数器是否是0。

引入计数为0,此时对象就是垃圾。

缺陷:

1.额外消耗存储空间。如果对象比较大,浪费一些空间可以忽略,如果对象比较小,或者对象数目比较多,空间浪费的就多了。

2.存在循环引用的问题。

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

**此时两个引用互相赋值,这两个对象相互指向对方,导致两个对象的引用计数都为1,你外部的代码无法访问这两个对象,**这便是循环引用问题。

2.可达性分析(JVM采取的方案)

解决了空间的问题,也解决了循环引用的问题,但是付出了时间上的代价。

遍历

JVM把对象之间的引用理解为一个"树形结构",JVM就会不停的遍历这样的结构,把所有能够遍历访问到的对象标记成可达,剩下的就是不可达。这个树形结构在写代码的时候,把类定义好,对象实例化好后,这样的树形结构就已经存在了。在java的代码中:

栈上的局部变量,引用类型的,就都是GC Root,包括常量池中引用的对象,方法中的静态成员。会有很多的树形结构,JVM会周期性的对所有的树进行遍历,不停的标记可达,也不停的把不可达的对象回收掉。由于可达性分析需要一定的时间,java的垃圾回收没办法做到"实时性",JVM提供了一组专门的负责GC的线程,不停的扫描工作

释放对应的内存。

1.标记清除

直接把标记为垃圾的对象对应的内存释放掉,被释放掉的内存其他代码可以反复利用。

这样的做法有会存在"内存碎片"问题,空闲的内存被分成一个个的碎片,后续很难申请到大的内存。申请的内存都是要申请连续内存空间的。

2.复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,通过拷贝将不需要释放的内存单独存放到另一片空间中,解决了内存碎片的问题,带来了浪费更多空间的问题,如果保存的对象比较多,复制的时间开销也比较大。

3.标记-整理

标记需要回收的垃圾,将标记对象删除,然后将存活对象移动到一起。解决了内存碎片化的问题和空间利用率的问题。但是整个算法时间开销比较大。

4.分代回收(重要)

上面3个方案只是铺垫,JVM中实际的方案是综合上面的方案,提出的更复杂的机制。这个机制是通过对象的年龄讨论的,GC会有一组线程进行周期性扫描,某个对象在经历一轮GC后还存在没有被标记为垃圾年龄就+1:

现在再来看堆区里面分为了很多区域:

Eden(伊甸区):新创建的对象都放在这个区,大部分对象的生命周期都是比较短的,第一轮GC到达的时候就会成为垃圾,只有少数对象能活过第一轮GC。

S0(生存区):活下来的对象通过复制算法,进入生存区。由于活下来的对象很少,生存区空间不需要很大。

s1(生存区):S0-S1,每经历一轮GC扫描,生存区就会淘汰一批对象,剩下的对象通过复制算法进入另一个生存区。

Old区(老年代):某些经历了很多了GC没有成为垃圾,就会进入老年代(复制算法)。

老年代的对象也是要进行GC扫描的,但老年代生命周期比较长,就可以降低GC扫描频率。

相关推荐
流星5211222 小时前
GC 如何判断对象该回收?从可达性分析到回收时机的关键逻辑
java·jvm·笔记·学习·算法
JanelSirry2 小时前
我的应用 Full GC 频繁,怎么优化?
jvm
JH30732 小时前
jvm,tomcat,spring的bean容器,三者的关系
jvm·spring·tomcat
DKPT6 小时前
JVM直接内存和堆内存比例如何设置?
java·jvm·笔记·学习·spring
siriuuus6 小时前
JVM 垃圾收集器相关知识总结
java·jvm
小满、9 小时前
什么是栈?深入理解 JVM 中的栈结构
java·jvm·1024程序员节
百花~1 天前
JVM(Java虚拟机)~
java·开发语言·jvm
每天进步一点点dlb1 天前
JVM中的垃圾回收算法和垃圾回收器
jvm·算法
漫漫不慢.1 天前
蓝桥杯-16955 岁月流转
java·jvm·蓝桥杯
boy快快长大2 天前
【JVM】线上JVM堆内存报警,占用超90%
jvm