JVM(java虚拟机)你了解多少?

JVM(java虚拟机)

Hello!各位优秀的程序员小伙伴们,欢迎来到这篇博客来了解JVM,本篇文章主要对于java虚拟机中的内存模型,OOM,类加载的机制,以及一些常见的垃圾回收算法和垃圾回收器做相关介绍,点击左侧的目录可实现对你感兴趣的地方进行快速访问哦!

首先什么是JVM?

举个例子:创建一个java类,在idea上写好后进行main方法编译运行到出来这个编译的结果。在这个过程中存在jvm,那么jvm到底是什么呢?且看下面这个图来解释:
在这里插入图片描述

创建一个java类编译后启动的一个java进程,在这时系统为进程分配了一块内存空间,执行进程的代码指令:其中就会创建java虚拟机(在java虚拟机中启动了一个 线程,来执行main方法,执行的方式是java虚拟机把class字节码的内容翻译为所在系统的机器码)
所以JVM就是:把Class字节码翻译成机器码(计算机能够识别)的一款Java底层工具
我们经常提及的JDK和Jre和Jvm都是什么关系呢?且看下图
这是文件中存储的关系图:
在这里插入图片描述
所以jdk中包含了jre,jre中包含了jvm
在这里插入图片描述

java虚拟机运行时数据区

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

  1. 方法区
    方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的
    运行时常量池是方法区的一部分,存放字面量与符号引用。
    字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
    符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。

  2. 堆区
    程序中创建所有的对象都存放在堆中。堆中分为两个区域:新生代和老生代区。新生代就是新建的对象,当新生代经过多次GC(垃圾回收)就会放入老生代区。

  3. 虚拟机栈
    Java 虚拟机栈的生命周期和线程相同(一个线程对应一个栈帧),Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。
    其中存储的相关数据的描述如下:

局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
操作数栈:每个方法会生成一个先进后出的操作栈。
动态链接:指向运行时常量池的方法引用。
方法返回地址:PC 寄存器的地址

查看IDEA中栈帧的情况,创一个如图方法:
在这里插入图片描述
打断点运行查看数据信息,
在这里插入图片描述
运行到打断点的位置,
在这里插入图片描述
运行完之后发现输出数据并没有改变,是因为swap中的方法中的m和n是局部变量,从main方法中传入了m和n的值,在swap方法中是已经改变后的结果,但是在mian方法中没有去接收该结果,所以main方法中的数值和swap方法中的数值不一样。

  • 本地方法栈
    本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的。
  • 程序计数器
    程序计数器的作用:用来记录当前线程执行的行号的。程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

在内存中的异常问题:

堆溢出

堆溢出简称为OOM,OOM全称是OutOfMemoryError(内存不足异常),那么为什么会出现内存不足的情况呢?主要原因有两点,内存泄漏内存溢出。对于内存溢出和内存泄漏介绍如下所示:
内存溢出:内存对象确实还应该存活,某个运行时数据区域,需要创建数据,但是空间不足,就会出现内存溢出。
内存泄漏:在那个内存区域中,保存了一些对象等数据,但是以后不会用到,也没有办法被GC,就会出现内存泄漏。举个例子,用户登录页面之后,不做退出操作,数据仍被保留,非常多的用户不做退出操作就会导致出现内存泄漏。

虚拟机栈溢出:

常见的虚拟机栈的异常为StackOverFlow异常和OOM异常。
对于该两类的异常解释为:
StackOverFlow:栈帧调用太深导致。创建一个方法不断的递归调用,就会在线程中不断的创建栈帧,最后出现该异常。
OOM:如果虚拟机在拓展栈时无法申请足够的内存空间,就会抛出OOM异常。

类加载

1.类加载的时机

1.在java类中执行mian方法时,需要先执行类加载,;
2.运行时,执行静态方法调用,静态变量操作时进行加载
3.new一个对象的时候,进行类加载
4.通过反射创建一个类的对象,就可以再通过反射生成实例对象,或者调用静态方法类加载只执行一次(已经执行类加载,方法区就已经有了类的信息,堆也有的类对象)如果在多线程中,有需要执行类加载的代码(上面几种时机)jvm执行类加载的时候,会进行synchronized加锁来保证线程安全

2.类加载过程

类加载的过程主要分为三部分:加载、连接、初始化
加载
加载class字节码到方法区,在堆中,生成一个class类对象
(连接操作)验证
验证class字节码数据,是否安全并且符合java虚拟机规范
(连接操作)准备
静态变量设置为初始值(对象初始值就是null,基础数据类型,就是对应的初始值)常量(final修饰的)会设置为真实的值
(连接操作)解析
将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
符号引用:编译的class文件中 需要有 变量/引用 到 值 对应的关系 此时还没有加载到到内存中就使用“符号引用”来表示这种关系
直接引用:执行类加载,把calss字节码加载到内存中,内存中体现的 变量到值的关系,称为直接引用
初始化
静态变量真正的初始化赋值,静态代买块初始化

3.类加载机制:

双亲委派模型(jdk默认的加载机制):不直接执行当前类加载器的类加载代码,而是要查找当前类加载器的父类加载器,父类也是找父类,直到最顶级的再进行加载,找不到就交给下一类。
总的来说:从下到上查找类加载器,从上到下执行类加载
优点:使用双亲委派机制,保证加载的先是jdk提供的类,避免了重复加载类。在创建同jdk类中同名的类时,会避免使用本地创建的类来执行类加载,保证了编译的安全。
缺点:扩展性相对不好,如JDBC操作,不通过数据库的驱动包就不一样,而jdk对于不同的驱动包不能做的自主识别,所有不能对于所有的类加载完全实现。
解决方法:采用SPI机制,将需要加载的类的全限定名 放在一个 jdk 能够找到的位置,然后告诉jdk,让jdk在执行类加载的时候找到该位置执行加载。

垃圾回收

死亡对象的判断算法:

引用计数算法:给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死“。
可达性分析算法:
使用“引 用”来判断死亡对象:将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。对于每种引用的介绍如下:

  1. 强引用 : 强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类
    的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。
  2. 软引用 : 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在
    系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回
    收还是没有足够的内存,才会抛出内存溢出异常。
  3. 弱引用 : 弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的
    对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是
    否够用,都会回收掉只被弱引用关联的对象。
  4. 虚引用 : 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否
    有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实
    例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通
    知。

垃圾回收算法

1.标记清除算法

分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
但是存在两个缺点:
1.效率较低
2.清除元素之后会产生大量的不连续的内存碎片

2. 复制算法

"复制"算法是为了解决"标记-清理"的效率问题。把所用的内存划分为两个一样大小空间,每次都只使用其中一块。把存活的对象复制到另一个空间,就把之前的空间清理了。
好处:清空半区的时候效率高,不会出现内存碎片
缺点:利用率较低(只有50%)

3.标记整理算法

类似标记清除算法,采取的方案,是将存活的对象,移动到连续的空间,再清理剩余空间
优点:不会出现内存碎片问题

4.分代算法

在堆中,根据对象创建回收的特点,分为了两块区域:
(1)新生代
新生代又分为:Eden(E区),2个Survivor(S区)对象朝生夕死:很快的创建,又很快的变为不可用的垃圾
其中默认的划分E:S:S=8:1:1,空间利用率就是90%。默认就是每次使用E区和一块S区来保存对象,另一个S区留空。在进行Gc的时候,把存货对方复制到另一个留空的S区。
采取的算法:复制算法
(2)老年代
对象可能长期存活,
采取的算法:标记清除算法,标记整理算法

常见的垃圾回收器

1.Serial收集器

Serial 收集器是最基本、发展历史最悠久的收集器。
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World,译为停止整个程序,简称 STW)。

2. ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本。
除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。

3.Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集
器。

4.Parallel Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

5.G1收集器(唯一一款全区域的垃圾回收器)

G1(Garbage First)垃圾回收器是用在堆的空间很大的情况下,把堆划分为很多相等的区域,每个区域根据需要设置E(Eden)、S(Survivor)、T(Tenured 老年区)。然后并行的对其进行垃圾回收。G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。
新生代:
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域。
老年代:
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:
初始标记(Initial Mark)阶段 - 同CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是跟minor gc一同发生的。也就是说,在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minor gc的时候一并将年老代上的Initial Mark给做了。
并发标记(Concurrent Mark)阶段 - 在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,就是如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。
最终标记(CMS中的Remark阶段) - 在这个阶段G1做的事情跟CMS一样, 但是采用的算法不同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象。
筛选回收(Clean up/Copy)阶段 - 在G1中,没有CMS中对应的Sweep阶段。相反 它有一个Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的

以上就是今天带来的JVM相关的知识了,感谢观看呦!!!

猜你喜欢

转载自blog.csdn.net/qq_53699052/article/details/126789345