超硬核!!!一篇文章搞定整个JVM运行时数据区


注释:JVM就是Java虚拟机,Java虚拟机就是JVM

解释非常详细:让你面试不在害怕被问到运行时数据区
在这里插入图片描述

1 JVM运行时数据区

什么是运行时数据区(就是我们java运行时的东西是放在那里的)
在这里插入图片描述

2 解析JVM运行时数据区

2.1 方法区(Method Area)

  1. 方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  2. 它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

2.2 Java堆(Java Heap)

  1. java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
  2. 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
  3. java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
  4. 从内存回收角度来看java堆可分为:新生代和老生代。
  5. 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
  6. 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
  7. 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

2.3 程序计数器(Program Counter Register)

  1. 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号)
  2. 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

总结:也可以把它叫做线程计数器

例子:在java中最小的执行单位是线程,线程是要执行指令的,执行的指令最终操作的就是我们的电脑,就是 CPU。在CPU上面去运行,有个非常不稳定的因素,叫做调度策略,这个调度策略是时基于时间片的,也就是当前的这一纳秒是分配给那个指令的。

假如:线程A在看直播
在这里插入图片描述
突然,线程B来了一个视频电话,就会抢夺线程A的时间片,就会打断了线程A,线程A就会挂起
在这里插入图片描述
然后,视频电话结束,这时线程A究竟该干什么?
(线程是最小的执行单位,他不具备记忆功能,他只负责去干,那这个记忆就由:程序计数器来记录

在这里插入图片描述

2.4 Java虚拟机栈(Java Virtual Machine Stacks)

  1. java虚拟机是线程私有的,它的生命周期和线程相同。
  2. 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

解释:每虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。

在这里插入图片描述
解析栈帧:

  1. 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
  2. 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
  3. 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
  4. 出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落

思考:.
一个方法调用另一个方法,会创建很多栈帧吗?
答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面

栈指向堆是什么意思?
栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址,堆中的数据等下讲

递归的调用自己会创建很多栈帧吗?
递归的话也会创建多个栈帧,就是一直排下去

2.5 本地方法栈(Native Method Stack)

  1. 本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字
  2. 它是虚拟机栈为虚拟机执行Java方法(也就是字节码)的服务
  3. native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。
  4. 同理可得,本地方法栈中就是C和C++的代码

3 Java内存结构

在这里插入图片描述
上面已经讲了运行时数据区,这里就差几个小组件了

3.1 JVM字节码执行引擎

虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。

“虚拟机”是一个相对于“物理机”的概念,虚拟机的字节码是不能直接在物理机上运行的,需要JVM字节码执行引擎编译成机器码后才可在物理机上执行。

3.2 垃圾收集系统

程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。

垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理

3.3 直接内存(Direct Memory)

  1. 直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是既然是内存,肯定还是受本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。
  2. 在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native(本地)堆中来回复制数据。

直接内存与堆内存的区别:
直接内存申请空间耗费很高的性能,堆内存申请空间耗费比较低
直接内存的IO读写的性能要优于堆内存,在多次读写操作的情况相差非常明显

代码示例:(报错修改time 值)

package com.lijie;

import java.nio.ByteBuffer;

/**
 * 直接内存 与 堆内存的比较
 */
public class ByteBufferCompare {

    public static void main(String[] args) {
        allocateCompare();   //分配比较
        operateCompare();    //读写比较
    }

    /**
     * 直接内存 和 堆内存的 分配空间比较
     */
    public static void allocateCompare() {
        int time = 10000000;    //操作次数
        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {

            ByteBuffer buffer = ByteBuffer.allocate(2);      //非直接内存分配申请
        }
        long et = System.currentTimeMillis();
        System.out.println("在进行" + time + "次分配操作时,堆内存:分配耗时:" + (et - st) + "ms");
        long st_heap = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接内存分配申请
        }
        long et_direct = System.currentTimeMillis();
        System.out.println("在进行" + time + "次分配操作时,直接内存:分配耗时:" + (et_direct - st_heap) + "ms");
    }

    /**
     * 直接内存 和 堆内存的 读写性能比较
     */
    public static void operateCompare() {
        //如果报错修改这里,把数字改小一点
        int time = 1000000000;
        ByteBuffer buffer = ByteBuffer.allocate(2 * time);
        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            buffer.putChar('a');
        }
        buffer.flip();
        for (int i = 0; i < time; i++) {
            buffer.getChar();
        }
        long et = System.currentTimeMillis();
        System.out.println("在进行" + time + "次读写操作时,堆内存:读写耗时:" + (et - st) + "ms");
        ByteBuffer buffer_d = ByteBuffer.allocateDirect(2 * time);
        long st_direct = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            buffer_d.putChar('a');
        }
        buffer_d.flip();
        for (int i = 0; i < time; i++) {
            buffer_d.getChar();
        }
        long et_direct = System.currentTimeMillis();
        System.out.println("在进行" + time + "次读写操作时,直接内存:读写耗时:" + (et_direct - st_direct) + "ms");
    }
}

测试结果:

在进行10000000次分配操作时,堆内存:分配耗时:98ms
在进行10000000次分配操作时,直接内存:分配耗时:8895ms
在进行1000000000次读写操作时,堆内存:读写耗时:5666ms
在进行1000000000次读写操作时,直接内存:读写耗时:884ms

代码来源:「猕猴桃0303」
链接为:https://blog.csdn.net/leaf_0303/article/details/78961936

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章