Java虚拟机一:深入理解JVM内存模型

- 日理万妓 2023-05-28 12:50 185阅读 0赞

什么是JVM

JVM是Java Virtual Machine(Java 虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是平台无关性。而使用Java虚拟机是实现这一特点的关键。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息Java虚拟机与操作系统进行交互,操作系统与硬件进行交互。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

JVM基本构成

ä¸æå¸¦ä½ æ·±å¥çè§£JVM

JVM总体上是由类装载器(ClassLoader)运行时数据区执行引擎垃圾收集**这四个部分组成。其中我们最为关注的运行时数据区**,也就是JVM的内存部分则是由方法区(Method Area)、JAVA堆(Java Heap)、虚拟机栈(JVM Stack)、程序计数器、本地方法栈(Native Method Stack)这几部分组成。

1.类装载器:在jvm启动时或者类运行时,将需要的class文件(字节码文件)加载到JVM中。

Java编译器(Java Compiler)负责将Java Source(.java File)编译成字节码文件(.class File),类加载器将class文件加载到JVM中。

2.运行时数据区(内存区):是JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域

å¨è¿éæå¥å¾çæè¿°

方法区(Method Area)

方法区是各线程共享的内存区域,用于存储类结构信息的地方包括所有的class和Static变量,即常量、静态变量、构造函数等以及编译后的方法实现的二进制形式的机器指令集等数据。其中方法区还包含运行时常量池。可以选择不实现垃圾收集。被装载的class的信息存储在Methodarea的内存中。

②Java堆(Heap)

Java堆是各线程共享的内存区域,用于存贮Java对象和数组的地方,是GC主要的回收区,堆内存分为三部分:新生代、老年代、永久代。Jdk1.8及之后:无永久代,改用元空间代替(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。

Java栈(JVM Stack)

Java栈是线程私有的,java栈总和是线程关联在一起,线程结束栈内存也就释放。每当创建一个线程时,jvm会为这个线程创建一个对应的java栈,在这个java栈中又会包含过个栈帧,每运行一个方法就创建一个栈帧,用于存贮局部变量表、操作栈等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。

java栈中存放局部变量的基本数据类型和对象的引用。

本地方法栈(Native Method Stack)

和java栈的作用差不多,只不过是为jvm使用到的本地方法服务的。

程序计数器(Program Counter Register)

程序计数器是一块非常小的内存空间,几乎可以忽略不计,用于保存当前线程执行的内存地址,由于jvm程序是多线程执行的(线程轮休切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要每个线程有一个独立的计数器,记录之前终端的地方。

3.执行引擎:负责执行class文件中包含的字节码指令,执行引擎以指令为单位读取Java字节码。它就像一个CPU一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。

不过Java字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被JVM执行的语言。字节码可以通过以下两种方式转换成合适的语言:

  • 解释器: 一条一条地读取,解释并执行字节码执行,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行语言的一个缺点。
  • 即时编译器:用来弥补解释器的缺点,执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多,编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

4.垃圾收集(Garbage Collection, GC)

垃圾收集即垃圾回收,简单的说垃圾回收就是回收内存中不再使用的对象。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用对象),则没有被任何指针给指向,因此占用的内存也可以被回收掉。

垃圾收集器一般必须完成两件事:检测出垃圾和回收垃圾

检测垃圾方法:

①引用计数法:给一个对象添加引用计数器,每当用地方引用他,计数器就+1,引用失效就-1.但是有个问题,如果有两个对象A和B,互相引用,除此之外没有其他任何对象引用他们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是相互引用,计数不为0,导致无法回收。

②可达性分析法:以根集对象为起点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根据一般包括 Java栈中引用的对象,方法区常量池中引用的对象、本地方法中引用的对象等。总之,jvm在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用,不能够被引用的对象就会被垃圾收集器收集。

可达性:在java中对象是通过引用使用的,如果没有引用指向该对象,那么该对象将无从处理或使用,这样的对象为不可达。

垃圾回收算法:

①标记清除算法

标记-清楚算法采用从根集合(GC Roots)进行扫描,首先标记出所有需要回收的对象(根搜索算法),标记完成后统一回收掉所有被标记的对象。

ä¸æå¸¦ä½ æ·±å¥çè§£JVM

存在问题:

  • 效率问题:标记和清除过程的效率都不高;
  • 空间问题:标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。

复制算法

复制算法是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象复制到另外一块上面, 然后把已使用过的内存空间一次清理掉。

标记-整理算法(Mark-Compact)

标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

ä¸æå¸¦ä½ æ·±å¥çè§£JVM

分代收集算法(Generational Collection)

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

ä¸æå¸¦ä½ æ·±å¥çè§£JVM

垃圾收集器

(1)Serial收集器【串行收集器】:新生代单线程收集器
(2)ParNew收集器【串行收集器的多线程版本】:新生代多线程收集器,其实就是Serial收集器的多线程版本
(3)Parallel Scavenge收集器【PS收集器】

新生代并行的多线程收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC 线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制 指定,用-XX:ParallelGCThreads=4来指定线程数。
(4)CMS收集器【老年代收集器】:CMS收集器是一种以获取最短回收停顿时间为目标的收集器
(5)G1收集器(Garbage First) G1是一款面向服务端应用的垃圾收集器

ä¸æå¸¦ä½ æ·±å¥çè§£JVM

以上JVM基本内存结构就讲解完了,针对线程栈进行重点说明。

Java虚拟机栈(Java Virtual Machine Stack)

栈运行原理:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进……F3栈帧,再弹出F2栈帧,再弹出F1栈帧。

70

Java虚拟机对运行时虚拟机栈(JVM Stack)的组织

Java虚拟机在运行时会为每一个线程在内存中分配了一个虚拟机栈,来表示线程的运行状态和信息,虚拟机栈中的元素称之为栈帧(JVM stack frame),每一个栈帧表示这对一个方法的调用信息。如下所示:

类加载器将class文件加载到JVM,将类结构信息和编译后的方法实现的二进制形式的机器指令集等数据放进方法区,动态链接通俗讲运行时动态的从方法区获取字节码机器指令集数据。栈帧中保存了一个引用,指向该方法在运行时常量池中的位置。

操作数栈:临时的内存空间,对相关变量指令进行计算使用,将结果存放在局部变量表。

20151227134135995

定义一个public class Bootstrap类,方法实现如下:

20151227151012844

为main方法创建栈帧:为main方法创建一个栈帧(VM Stack),并将其加入虚拟机栈中

20151227160915577

举例说明:

  1. public class Demo {
  2. public static void foo() {
  3. int a = 1;
  4. int b = 2;
  5. int c = (a + b) * 5;
  6. }
  7. }

简单解释下执行过程,注意:偏移量的数字只是简单代表第几个指令哦,首先常数1入栈,栈顶元素就是1,然后栈顶元素移入局部变量区存储,常数2入栈,栈顶元素变为2,然后栈顶元素移入局部变量区存储;接着1,2依次再次入栈,弹出栈顶两个元素相加后结果入栈,将5入栈,栈顶两个元素弹出并相乘后结果入栈,然后栈顶变为15,最后移入局部变量。执行return命令如果当前线程对应的栈中没有了栈帧,这个Java栈也将会被JVM撤销。示意图如下:
aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vY25ibG9nc19jb20vcm95aTEyMy81NDgwOTYvb182MDk5NTYyNzQ1MzMwMDczNzguZ2lm

文章参考:

https://blog.csdn.net/wangtaomtk/article/details/52267634

https://blog.csdn.net/csdnliuxin123524/article/details/81303711

https://www.toutiao.com/a6717906477710836236/

发表评论

表情:
评论列表 (有 0 条评论,185人围观)

还没有评论,来说两句吧...

相关阅读