JVM的基本认识
【JavaEE】了解JVM
JVM是Java虚拟机(Java Virtual Machine)的缩写
- 它是Java编程语言的关键组成部分。
- JVM是一种用于在计算机上执行Java字节码的虚拟机。
- 它是Java平台的核心技术,提供了跨平台的特性和Java的主要优点。
JVM的来历可以追溯到20世纪90年代初,当时Sun Microsystems(现在是Oracle Corporation的一部分)开发了Java语言。
- Java的目标是在不同的硬件和操作系统平台上实现 “一次编写,到处运行” 的理念。
- 为了实现这一目标,Sun Microsystems开发了JVM,它是Java语言的运行时环境。
接下来就是介绍JVM中重要的几个 芝士点
1. JVM中的内存区域划分
JVM其实就是一个java进程,java进程会从操作系统这里申请一大块内存区域,再进行一系列划分,给java代码使用~
- 所以java程序不鼓励多进程,而鼓励多线程也是这个原因
而我们重点要学的是内存的进一步划分,不同的区域有不同的用途
1.1 JVM的核心区域
- 堆,java中实例化出来的对象,即成员变量
- 栈,维护方法之间的调用关系,即局部变量
- 方法区(旧说法)/元数据区(新说法),类加载之后的类对象,包括静态变量、方法…
- 类对象就是.class文件的特殊数据结构构成的对象,类名.class去获取
- 方法不是变量,其实在内存中是字节码(二进制指令)形式存在!
经典的面试题:
- 给一段代码,问你某个变量处在内存中的哪个区域
- 根据上面的变量形态与内存区域的对应即可~
注意:
- 变量的类型与它在内存中的区域无关
Apple a = new Apple();
- 我们常常把a叫做引用,或者对象,其实它指向的内容,才是对象,才是属于堆的东西
记住java对象的这个核心关系即可:
1.2 JVM内存城防图
灰色:两个栈
- 虚拟机栈,给java代码使用的
- 本地方法栈,是给JVM内存的native本地方法使用的(JVM的本地方法内部是通过C++实现的)
之前的String常量池…也在元数据区里~
- 不必多说~
Program Counter Register(程序计数器)
用途是记录当前程序执行到哪个指令~
- 简单地用long类型的变量去存储 “一个内存地址”
- 这个内存地址就是下一个要执行的字节码所在的地址
CPU也是有这么一个专门的寄存器,JVM参考了CPU
堆的细节安排,在垃圾回收策略里就体现出来了,随后讲解
堆和元数据区,在一个JVM进程中,只有一份;栈和程序计数器,则存在多份(每个线程只有一份)
2. JVM的类加载机制
类加载:把.class文件,加载到内存,得到类对象,这样的过程~
- 程序要运行的必要条件:指令和数据
类加载的步骤,其实非常的复杂,而我们只需要理解一些基本流程
- 可以去看看官方的规范:Java SE Specifications (oracle.com)
- The Java® Virtual Machine Specification (oracle.com)
提到了 五个词
- 加载 - loading
- 验证 - verification
- 准备 - preparation
- 解析 - resolution
- 初始化 - initialization
2.1 loading
找到.class文件,并且读文件内容,获取到字节码
- 涉及到一个经典的考点:双亲委派模型,在后面单独解释
2.2 verification
.class文件的数据格式:
- 二进制
- 类似c语言的结构体(JVM本质就是C++写的)
- u4 => 无符号整型(四个字节)
- u2 => 无符号整型(两个字节)
- 其他就是JVM中定义的其他结构体
这一步就是验证这个.class文章的内容是否符合标准,感兴趣的可以去研究一下每个成员的含义~
2.3 preparation
给类对象分配内存空间
- 这个是未初始化的空间,内存空间的数据是全是0的
2.4 resolution
解析,则是针对字符串常量,进行初始化
最主要是将“常量池的符号引用替换为直接引用”:
2.5 initialization
针对类对象进行初始化(初始化静态成员变量,静态方法,执行静态代码块,加载父类、内部类…)
第四第五步我觉得界限不明显,可能是本就是一步,比较这五个步骤是人为划分出来的
2.6 类加载触发的时机
并不是jvm一启动,就把所有的.class都加载了!整体是一个“懒汉模式”,非必要不加载
- 要创建这个类的实例
- 使用这个类的静态方法/静态属性
- 使用子类,会触发父类的加载
- 反射(使用类对象的场合)
2.7 双亲委派模型
因为这个好名字,成为了一个热门面试题,其实这个加载步骤在类加载中并不是什么关键的步骤~
接下来就来好好了解一下吧!
主要工作,在第一个步骤中,找.class文件的一个过程~
JVM中,内置了三个类加载器(加载类,需要用到的一组特殊模块)
- BootStrap ClassLoader,负责加载Java标准库中的类
- Extension ClassLoader,负责加载一些非标准的类,(Sun/Oracle扩展库的类)
- Application ClassLoader,负责加载项目中自己写的类,以及第三方库中的类
也可以自己去定义类加载器~
三个类加载器负责三组不同的目录~
当我们具体加载一个类的时候,需要先给定一个类的全限定类名“一系列包名.类名”,例如:“java.lang.String”
双亲委派模型被称为“双亲”,是因为它建立了一个父子关系的类加载器层级结构,通过委派加载的方式提供了一种高效、安全和一致的类加载机制。就体现在两个委派方向咯~其实是翻译的问题,双亲就是family~
3. JVM中的垃圾回收策略
JVM中帮助程序员自动释放内存的~
在C中,malloc的内存必须手动free,否则就容易出现内存泄露(只申请不释放,出现逐渐崩溃)
- C++中的内存泄露不易发现,并且现象就像温水煮青蛙,很久才会被发现,并且发现的时候就一发不可收拾
java等后续的编程语言,引入了GC来解决这个问题~
GC的全称是垃圾回收(Garbage Collection)。在计算机科学中,垃圾回收是一种自动化的内存管理技术,用于在程序运行时自动回收不再使用的内存资源,以便重新分配给其他需要的对象。
当程序运行时,会动态地创建和销毁对象。由于对象的动态性,手动管理内存资源变得复杂和容易出错。垃圾回收器(GC)的作用就是在程序运行时监测和识别不再使用的对象,然后释放其占用的内存资源。
能够有效的减少内存泄露的出现概率!作死的一样会出现~
申请内存的时机是明确的,使用到了必须要申请
释放内存的时机是模糊的,完全不使用了才能释放
而C/C++将这个释放的时机,全权交给程序员,但是 java的JVM则通过一系列策略自动判断是否释放
- 这些策略的准确性是比较高的,但是是需要代价的,那就是时间/空间
3.1 JVM释放的空间
-
是堆,GC的主要目标
- 不仅仅是java,C/C++中自主申请和释放的也是堆
-
因为栈是局部变量,创建与销毁本就系统自动的行为(随着线程的销毁而销毁,汇编操作中(栈帧创建与销毁,即入栈和出栈)方法结束的出栈操作而被销毁)
-
程序计数器,就一个long变量,随着线程销毁而销毁
-
元数据区/方法区,存的类对象,很少会“卸载”,进程结束销毁~
而GC就是以对象为单位进行释放的,即释放内存=释放对象
- 总不能释放半拉对象吧,🤣
3.2 GC的两个阶段
- 找,谁是垃圾(涉及垃圾回收算法)
- 释放,将垃圾对象的内存整体释放掉
接下来我们要了解一下垃圾回收算法,我们学习思想,不代表JVM的真实实现方法,JVM的实现方法是在此基础上的优化~
3.3 垃圾回收算法
一个对象,后续不再使用,就可以认为是垃圾~
java中使用一个对象,只能通过“引用”~
- 如果一个对象,没有引用指向它,此时这个对象一定是无法被使用的(妥妥的垃圾)
- 如果一个对象,已经不想用了,但是这个引用可能还被指向着(这个携带程序员主观意愿,JVM无法判断)
3.3.1 引用计数
不针对JVM的判断方法,python和PHP采取了一个算法:引用计数
就是给对象安排一个额外的空间,保存一个整数,表示该对象有几个引用指向它~
- 随着引用的销毁,计数就会减少,为0的时候,立即释放~
缺陷就是:
-
每个对象都需要怎么一个可见来存放这个计数
-
还有个漏洞,就是一些“循环引用”引起的问题,最典型的就是循环链表
- 这个时候,c1 = null; c2 = null;
- 计数并非为0,不能释放~
3.3.2 可达性分析
而这个算法,才是JVM采取的方案,并没有采取引用计数~
- 我们可以把对象之间的引用关系,理解成一个“有向图结构”,从一些特殊的起点出发,进行遍历,只要能便利访问到的对象,就是“可达”,否则就是“不可达”
- 不可达的就是垃圾咯~
例如下图,则就是一种复杂的引用关系~
-
边的箭头我省略了
-
然后我们就寻找其中特殊的起点(蓝),开始遍历,每个蓝色的都便利一边,访问的到的标记为“可达”,所有蓝点都便利完后,未被标记的就是“不可达”
-
即顺藤摸瓜
- 而这个蓝色起点,可以是引用中的“局部变量”,因为我们是在方法内部可以先通过局部引用变量去访问堆区空间的,进而延申出后面的引用关系!
- 其次,还可以是常量池上引用的对象,方法区中静态成员引用的对象
- 这些蓝点有一个名称:GCroots
缺点:
- 耗时及其大
- 这个过程中,必须保证原代码的引用关系不要发生变化,所以要加锁,即STW问题
- 加锁 -> 其他业务暂停工作,Stop the world!
随着java的发展,JVM的垃圾回收不断的更新优化,STW问题也被很好的应对,不能完全消除,但是STW的时间可以忽略不及了~
3.4 释放“垃圾”对象
接下来介绍三种典型的策略
3.4.1 标记释放
通过找的过程后,我们已经知道哪些是需要释放的了~
而“标记释放”则是直接将被标记为“不可达”的内存空间直接释放,虽然速率快,但显然,释放出来的空间,七零八落,这导致这些空间释放了之后,完整性不高,甚至可能导致之后无法再被申请!
- 因为申请空间是需要一段完整的空间!
- 即 “产生内存碎片”
就相当于,你有2G的内存,但是不连续,都是内存碎片,这样就导致100M的空间都申请不出来~
3.4.2 复制算法
这种算法是将原本的堆去分为两个部分,一次只用其中一半:
而删除的时候,将“可达”的内存搬运到另一侧
然后这一侧,全部统一释放~
解决了“内存碎片”的问题,但是也很明显
- 若大部分要保留则搬运成本大
- 空间利用率低
3.4.3 标记整理
这种算法则是类似顺序表删除元素操作的方式:
-
就是一个搬运的过程~
-
“不可达”的数据被覆盖
-
“搬运完成之后”,再对后面“不可达”的数据进行释放
解决了“内存碎片”的问题,但是缺点很明显:
-
搬运开销还是比较大
- 因素:
- 需保留的内存
- “不可达”的数据在内存中排的比较前
3.4.4 分代回收
对于前面三种策略其实各有千秋,但是都有缺点,而我们现在要做的就是,在他们适合存在的场合使用他们,将利益最大化,就衍生出算法“分代回收”,这也联系到“堆”的布局:
这就有一个概念:“年龄”
- 对象的年龄:一个对象每经过一轮扫描(可达性分析),就涨一岁
- 一个对象刚出生,我们认为是0岁
还有一个普遍的经验规律,或者说是一个代码习惯:
- 一个对象年龄越长,这个对象就更可能存活更久时间;
- 一个对象年龄曰小,这个对象就更可能被销毁
即,"要死的话早就死了"
- 伊甸是西方的说法,伊甸园是上帝创造的第一个人出生的地方~
分代回收过程如下:
- 新创建的对象,放到伊甸区,在伊甸区的对象,进行"标记释放"
- 在第一轮”GC“存活下来的对象,移动至幸存区,S0/S1
- 有绝大多数的对象在第一轮”GC“中就别释放了,采取“标记删除”的方式直接释放即可,因为不会出现内存碎片的问题,因为原可达数据已经被移走了
- 在幸存区的对象,进行"复杂算法"
- 因为在这个区域内的对象需要保留的比较少
- 并且这个区域本来就小,不用担心内存利用率低的问题
- 经过若干轮”GC“存活下来的对象,移动到老年代
- 在老年代的对象,进行"标记整理"
- 因为在这个区域内能存活更长时间的内存都排列在前面了
- 前面的能活得更久,后面的则更容易被释放,所以挪动次数被尽可能降低了
- 不仅如此,在老年代,“GC”的频率降低
- 特殊情况:在第一轮“GC”后,内存很大的存活对象,直接放进老年代
- 内存很大,复制算法成本太高,放在老年代被后面的数据覆盖会更好
- 并且大内存的对象,也不会很多
贯彻这个过程的一句话就是:"要死的对象早就死了,活下来的就是有两把刷子的"
感兴趣的同学可以去学习“垃圾收集器”,这就是具体的实现方法了,有改进和优化…
- CMS
- G1
- ZGC
认识越新的越好~
4. JMM
JMM的全称是Java内存模型(Java Memory Model)。
- Java内存模型定义了Java程序中多线程并发访问共享内存的行为规范,确保多线程程序的正确性和可预测性。
Java内存模型主要关注的是多线程环境下的共享内存访问问题。
- 在多线程编程中,多个线程同时访问和修改共享的变量和对象可能会导致不可预料的结果,如数据竞争、内存可见性问题等。
- Java内存模型提供了一套规范,定义了一系列规则和原则,来约束线程如何协作和访问共享内存。
Java内存模型包含了对于线程之间的操作顺序、变量的可见性、原子性操作、内存屏障等方面的规范。
- 它确保在满足规定的条件下,程序员可以正确地编写并发程序,而无需担心数据不一致或未定义行为。
通过定义内存访问规则和操作顺序,Java内存模型提供了happens-before关系的概念。
- happens-before关系指定了对于不同线程之间操作的顺序性,确保线程之间的操作按一定的顺序发生,从而保证了程序的正确性。
Java内存模型的规范不仅仅适用于Java语言本身,也适用于运行在Java虚拟机上的其他语言。
- 它为多线程编程提供了标准化的原则和规则,使得程序员能够更加方便地控制线程的行为,编写并发安全的程序。
总结而言,JMM的全称是Java内存模型,它定义了Java程序中多线程并发访问共享内存的行为规范,确保多线程程序的正确性和可预测性,提供了一套规则和原则来约束线程的协作和访问共享内存。
- JVM和JMM是紧密联系的,JVM作为Java程序的运行环境,依赖于JMM的规范来保证多线程程序的正确性和可预测性。
- JVM执行的字节码遵循JMM的规则,通过指定的线程安全机制和内存可见性保证线程间的正确通信和协作。
之前在讲多线程线程安全(内存可见性)的时候讲过了,传送门:【JavaEE】线程安全问题_s:103的博客-CSDN博客
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆!总的来说,JVM不需要了解的太深,如果你能理解本篇文章,就足够了~文章来源:https://www.toymoban.com/news/detail-560934.html
JavaEE的初阶内容已经结束,接下来将学习JavaEE的进阶内容,比如一些框架,敬请期待!文章来源地址https://www.toymoban.com/news/detail-560934.html
到了这里,关于【JavaEE】了解JVM的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!