【EE初阶】JVM

文章目录

  • JVM
    • [(一) JVM 内存区域划分](#(一) JVM 内存区域划分)
    • [(二) 类加载机制](#(二) 类加载机制)
    • [(三) 垃圾回收机制](#(三) 垃圾回收机制)

JVM

JVM的执行流程

程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器**执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口本地库接口(Native Interface)**来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。

(一) JVM 内存区域划分

  • 为啥要区域划分?

JVM 就是Java虚拟机 仿照真是的机器,真实的操作系统进行设计

真实的操作系统中,对于进程的地址空间是进行了 分区域设计的

JVM也就仿照操作系统的情况,进行了分区域设计~~

比如有一栋写字楼(操作系统)

有很多层,每一层相当于一个进程

我们的公司在一层上,这一层上租了下来,进行装修,分出若干个办公室

就相当于把这个楼层划分出若干个区域,有着不同的作用(人事,老板,开发人员...)

  • JVM具体是怎么划分的呢?

程序计数器的作用:用来记录当前线程执行的行号的。

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空。

程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!

四个核心区域:
1.程序计数器 => 是一块很小的区域,只是用来记录当前指令执行到哪个地址了
2.元数据区 => 保存当前类被加载好的数据~~
比如我们刚开始学Java的时候 .java => .class => 加载到内存中
要想运行这个代码,就需要把 .class 加载到内存里面(类加载)
3.
写代码的时候,肯定会有方法调用~
每次调用方法,就会进入方法内部执行,当方法执行完毕,返回到调用位置,继续往后走
注意了,这里的栈和数据结构里的栈并不是一回事
那和函数栈帧有关系吗?
JVM中的栈和栈帧,概念上是差不多的,但东西不是一个东西
JVM这个进程,是由C++代码实现的程序,这个进程本身就是存在一系列方法调用的~~

4.
保存 new 的对象的
比如 Test t = new Test();
如果 t 是一个局部变量 => t 在栈上
如果 t 是一个成员变量 => t 在堆上
如果 t 是一个静态成员变量 => t 在元数据区

堆就是JVM中最大的空间区域了
往集合类里面添加元素,如果堆上的对象,不再使用了的话,就需要被释放掉(就是后面要讨论的垃圾回收机制)
>元数据区 和 堆,整个Java进程 共用同一份>程序计数器 和 栈 ,一个进程中可能有多份(每个线程有一份)
局部变量: 栈帧结束,也就销毁了

成员变量:在堆上,被释放(垃圾回收)

静态成员变量:不会释放,只谈类加载,不谈类卸载

(二) 类加载机制

类加载本身是一个很复杂的事情

我们主要关心两个方面
1.类加载的步骤
Java的官方文档上,也有对应的描述,感兴趣可以仔细阅读
一共三个大阶段,其中第二个阶段,又分成了三个小步骤 => 一共是5个步骤
a) 加载 : 找到.class文件
根据类的 全限定类名(包名 + 类名,形如:java.lang.String这样的)
打开文件,读取文件内容到内存里
b)验证 :解析,校验 .class文件读到的内容,是否是合法的,并且把这里的内容转成 结构化数据 (.class 文件是二进制文件,格式都是有明确要求的~~)
c)准备 :给类对象 申请内存空间,此处申请的空间相当于"全0"的空间(未初始化的内存空间)
d)解析 :针对字符串常量,进行初始化
字符串常量,本身就包含在 .class文件中 =>就需要 .class文件里解析出来的字符串常量放到 内存空间里(元数据区,常量池中)
e)初始化 :针对刚才谈到的类对象进行最终的初始化
针对类对象的各种属性进行填充,包括 类中的静态成员
如果这个类还有父类,并且父类还没加载 =>触发父类的类加载
2.类加载的"双亲委派模型"
双亲委派模型 => 描述了类加载中,根据 全限定类名(包名 + 类名 ) 找到 .class文件的过程

  • 类加载触发的时机

Java程序一启动,就会加载用到的所有的类吗 -->不是的!!!

懒汉模式/懒加载

Java代码,用到哪个类,就会触发哪个类的加载

比如

1.构造了这个类的实例 ->加载

2.调用/使用了 类静态属性/静态方法

2.使用某个类的时候,他的父类若没有加载,也会触发父类的加载

  • 双亲委派模型(类加载这里的高频问题)
  • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此 ,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。


双亲委派模型的过程:

进行类加载,通过全限定类名,找 .class 的时候

就会从 ApplicationClassLoader 作为入口开始,

然后把 加载类 这样的任务,委托给 父亲 来进行, ExtensionClassLoader

ExtensionClassLoader 也不会立即进行查找, 而是也委托给 父亲 来进行,BootstrapClassLoader,BootstrapClassLoader 也想委托给父亲, 由于没有父亲, 只能自己进行类加载. 根据类名, 找标准库范围, 是否存在匹配的 .class 文件

BootstrapClassLoader 没有找到, 再把任务还给 孩子 ExtensionClassLoader , 接下来 ExtensionClassLoader 来负责进行找 .class 文件的过程. 找到就加载, 没找到, 也就把任务还给孩子 ApplicationClassLoader. 接下来 ApplicationClassLoader 负责找 .class. 找到就加载, 没找到就抛出异常

给出的这三个类加载器,是属于 JVM 自带的.

程序员是可以自定义类加载器的~~

当你自定义的时候,就可以把你的类加载器,也放到双亲委派模型中,也可以不放到里面.

Tomcat (Java 中的知名的 HTTP 服务器) 的内部就有自定义的类加载器

(由于现在用的 Spring MVC 内置了 tomcat, 用的时候感知不到了)

从指定的 webapps 目录加载对应的类~~

(三) 垃圾回收机制

垃圾回收 => Java中释放内存的手段

比如C语言中,申请内存使用 malloc

申请之后,一定要手动调用 free 进行释放,否则就会出现 内存泄漏

要是 free 不小心忘记了 或者因为 提前return 了没执行到 free ,就直接出错了

Java引入垃圾回收,进行自动释放,JVM会自动识别出,某个内存,是不是后续不再使用了,自动释放

C++并没有引入GC,就是因为STW问题(卡了),运行效率会有所影响,但是现在Java17及以上,STW问题大部分情况下 < 1ms 时间了

  • GC回收 是回收 JVM中 堆 内存区域

程序计数器 =>线程销毁,自然释放

栈 => 方法执行结束,栈帧就结束,随之释放了

元数据区 => 类对象,一般不会释放

堆 => 创建很多新对象,也会有就对象消亡
说是"回收内存" 本质上 "回收对象"

  • GC工作过程

1.找到垃圾(不再使用的对象)
2.释放垃圾(对应的内存释放掉)

  • 找到垃圾(两个方法)

1.引用计数
2.可达性分析(Java采取这个方案)

1.找到垃圾(不再使用的对象) 1) 引用计数 => 每个对象 new 的时候,都搭配一个小的内存空间保存一个整数

但这个方法有两个缺点:

1)内存消耗更多

尤其是对象本身比较小,引用次数消耗得空间比例就更大了

假设引用计数是 4 个字节

对象本身是 8 个字节.

引用计数就相当于提高了 50% 的空间占用率.2) 可能出现 "循环引用" 这样的问题

class Test {

Test t = null;

}

Test a = new Test();

Test b = new Test();

a.t = b;

b.t = a;

a = null;

b = null;
可达性分析 => 用时间换空间
1.以代码中的一些特定对象,作为 遍历的"起点" =>GCRoots
1)栈上的局部变量(引用类型)
2)常量池引用指向的对象
3)静态成员
这三个对象,程序运行到任何一个时刻,JVM都是容易获取到的
2.尽可能的进行遍历
判定某个对象是否能访问到
3.每次访问到一个对象,都会把这个对象标记成"可达"
当完成所有的对象的遍历之后
未被标记成 "可达" 的对象就是 "不可达"

JVM 中一共有多少个对象, JVM 自身是知道了.

通过可达性分析, 知道了哪些是 "可达" 的, 剩下的就是 "不可达" =>接下来要回收的垃圾

  • 已经知道哪些对象 是垃圾了,如何进行释放呢?

1.标记 - 清除
2.复制算法
3.标记 - 整理
Java使用 分代回收 ~~ 把3个方法(主要是后2个) 结合起来,扬长避短

1.标记 - 清除
把垃圾对象的内存,直接进行释放.
这样做会产生内存碎片问题

2.复制算法

3.标记 - 整理

Java使用的是 分代回收,把3个方法(主要是后2个) 结合起来,扬长避短

"代" =>指的是 对象的年龄 也就是GC的轮次

某个对象, 经历一轮 GC 可达性分析之后, 不是垃圾;

此时对象的年龄就 + 1 ; 初始情况就是 0

经验规律:
如果一个对象是小年轻, 这个对象就很可能快速就挂掉
如果一个对象是老油条, 这个对象就可能继续存在

老年代, GC 频次就可以降低了
新生代, GC 频次就会比较高~~

  • 新创建的对象就放到 "伊甸区",绝大部分的伊甸区的对象, 活不过第一轮 GC 所以幸存区比伊甸区小~~

  • 伊甸区 => 幸存区: 复制算法, 复制的对象规模是很少~~ 复制的开销可控的~

  • 幸存区中的对象, 也要经历 GC 的扫描.每一轮 GC 都会消灭一大部分对象;剩余的对象再次通过 复制算法, 复制到另外一个幸存区

  • 如果这个对象在 幸存区 中经历了多次复制, 都存活下来了, 对象的年龄就大了,就会晋升到 老年代 中

一个对象: 伊甸区 => 幸存区 => 幸存区 ...=> 幸存区 => 老年代

蓝色过程为:复制算法
红色过程为:标记 - 整理

  • 新生代中的对象大部分会快速消亡,使得每次复制的开销都可控

  • 老年代的对象大部分会生命周期较长,使得整理的开销也都可控~~

  • 大概是经历15轮次GC机制,才能从幸存区 晋升到 老年代

好!那么到这里我们的EE初阶内容就到这里了,EE进阶的内容我们会结合spring结合初阶学过的知识,做出综合性的网站,做出更有实际价值的程序~~,大家可以期待后续的EE进阶内容!!!

相关推荐
不见长安在3 小时前
Jvm资料整理
jvm·1024程序员节
如果丶可以坑3 小时前
maven无法获取依赖问题
java·maven·1024程序员节
Arlene3 小时前
JVM 的垃圾回收机制
jvm
x70x803 小时前
git仓库基本使用
git·算法·编程
Reggie_L3 小时前
RabbitMQ -- 保障消息可靠性
开发语言·后端·ruby
blammmp3 小时前
RabbitMQ :概述,Web界面介绍,快速上手,工作模式
java·分布式·rabbitmq
何中应3 小时前
如何截取PDF内容为图片
java·开发语言·后端·pdf
仰泳的熊猫4 小时前
LeetCode:773. 滑动谜题
数据结构·c++·算法·leetcode
夏鹏今天学习了吗4 小时前
【LeetCode热题100(50/100)】岛屿数量
算法·leetcode·职场和发展