JVM虚拟机(一)介绍、JVM组成、堆、栈、方法区/元空间、直接内存

目录

    • 一、JVM 介绍
      • 1.1 为什么要学 JVM?
      • 1.2 JVM 是什么?
    • 二、JVM 组成
      • 2.1 程序计数器
      • 2.2 Java堆
        • 1)JVM 内存结构
        • 2)Java 1.7 和 1.8 中堆的区别
      • 2.3 Java虚拟机栈
        • 1)虚拟机栈 和 栈帧
        • 2)常见面试题
      • 2.4 方法区/元空间
        • 1)方法区/元空间的介绍
        • 2)复现元空间不足的场景
        • 3)常量池
        • 4)运行时常量池
      • 2.5 直接内存
        • 1)常规IO 和 NIO 性能对比
        • 2)常规IO 和 NIO 分析

一、JVM 介绍

1.1 为什么要学 JVM?

在这里插入图片描述

  1. 应对面试: 如果说在面试的时候,我们连 JVM 的知识都不了解的话,面试官对我们的印象将会大打折扣。
  2. 中高级程序员必备技能: 如果说只满足于一个初级程序员,OK,根本不需要了解 JVM。它和我们平时开发没啥关系,但是如果你是 一个有追求的程序员,想在这个行业长期发展的话,也期望从一个小白升级为大牛的话,掌握 JVM 就至关重要了。
  3. 深入理解 Java: 什么意思呢?一旦掌握了 JVM,我们就知道了 Java 的运行机制,特别对于排查问题的能力将会有大幅度提升。像一些比较棘手的问题,就跟 JVM 有关系,比如:内存泄漏、CPU飙高等等。比如说我们也能够取解决这些问题,那就会不断地靠近大佬级别。

那掌握 JVM 能让你获得哪些技能呢?下面我们就来介绍一下JVM:

1.2 JVM 是什么?

JVM,全称 Java Virtual Machine,是 Java 程序的运行环境。

  • 比如说我们自己写的代码想要运行的话,都必须在 JVM 中才能运行。当然严格来说,是 Java 的二进制字节码的运行环境。
  • 我们都知道,Java 代码想要运行的话,就必须先经过编译之后,编译成 .class 文件才能运行。JVM 就是 .class 二进制字节码的运行环境。

JVM 的好处一:一次编写,到处运行。

我想你肯定是听说过这句话的,为什么我们的 Java 代码可以做到一次编写到处运行呢?大家看下面这张图:

  • 首先,最底层的是一个计算机的硬件,比如:CPU、内存;
  • 硬盘上面是操作系统,比如:Windows系统/Linux系统;
  • 然后在系统上面有 JVM 这个软件,也就是说 JVM 是运行在操作系统中的。

我们平时都说 Java 是一个跨平台的语言,它是怎么跨平台呢?就是因为 JVM 给我们屏蔽了操作系统的差异。别管是在 Windows 或者是 Linux,真正运行代码的并不是这些系统,而是我们的 JVM。所以说才能做到一次编写到处运行。

JVM 的好处二:自动内存管理,垃圾回收机制。

说到这里,一般会跟 C语言进行对比,C语言需要程序员自己去管理内存,如果程序员由于编码不当,很容易造成内存泄露的问题。而 Java 虚拟机的垃圾回收功能就大大减轻了程序员的负担,减少了程序员出错的机会。

直到了这两个 JVM 的好处之后,我们再来看一看 JVM 的组成,了解一下 JVM 是如何工作的。


二、JVM 组成

2.1 程序计数器

程序计数器属于 “运行数据区” 的一部分,这里面有一个组件叫做 PC Register,其实就是程序计数器,它到底什么意思呢?

  • 程序计数器:是线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。

首先,线程私有的有没有线程安全的问题呢?肯定是没有的。后面的话不太好理解,我举个例子,我们知道 Java 代码想要运行的话,先把 Java 的源码编译为 class 字节码文件,在字节码文件中详细说明了代码的执行过程。

在这里插入图片描述

我们举一个具体的例子,现在我们想要去查看 class 字节码的信息,我们可以通过 javap 命令来查看字节码的反汇编信息,它就详细记录了字节码的执行过程。

# 打印堆栈大小,局部变量的数量和方法的参数
java -v xx.class

我们新建一个 Java 文件,内容如下:

public class Application {
	public static void main(String[] args) {
		System.out.println("hello world");
	}
}

这里面有一个 main() 方法,打印了一个 “Hello world”,但是这些在字节码中是怎么执行的呢?

我们在文件所在目录的命令行中,先使用 javac 命令进行编译:

javac Application.java

编译结果:

在这里插入图片描述

我们再使用 javap 命令进行分析:

javap -v Application.class

输出内容如下:

Classfile /D:/test/Application.class
  Last modified 2024-4-6; size 440 bytes
  MD5 checksum d8a7300dcbdbc1a7c3b962f3f6420821
  Compiled from "Application.java"
public class com.demo.jvm.Application
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // hello world
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // com/demo/jvm/Application
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Application.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               hello world
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               com/demo/jvm/Application
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public com.demo.jvm.Application();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}
SourceFile: "Application.java"

文件内容比较多,我们主要看一下 main() 方法的部分:

在这里插入图片描述

我们可以看到,虽然在源码中,main() 方法只有一行代码,打印了一下 “Hello world”,但是在 class 字节码中拆成了多行去执行。注意看 Code 中,这里面会有代码的指令地址,也可以理解为代码的执行行号,是 0、3、5、8,我们可以针对这每一行做一个简单的分析:

  1. getstatic:获取一个静态的变量,这里主要指 System.out 中的 out 是静态变量。后面的注释中也说明了,out 是一个 Print Stream。
  2. ldc:意思是加载一个常量,这里的常量指的是 String 字符串类型的 “Hello world”。
  3. invokevirtual:表示要调用一个方法,这里指 println() 方法。
  4. return:最后一行,意思就是结束了这个方法。

虽然在 Java 代码中只有一行代码,但是在 二进制字节码 中就变成了多行 ,它的执行顺序就是代码的执行行号。

假如说,有多个线程来执行这段代码,我们的程序计数器就是用来给每一个线程去记录这个行号的。如下图所示:

在这里插入图片描述

  • 线程1从位置0开始执行,当执行到位置10的时候,CPU的时间片被线程2夺走了,目前线程1没有执行权了,所以为了再次获取执行权的时候可以继续执行,它要记录一下位置,目前执行到了第10行。
  • 线程2也是从位置0开始执行,它一直执行到了第9行,然后线程2的时间片被线程1夺走了。

在这里插入图片描述

  • 线程1记录了刚才执行到了第10行,重新获取到时间片之后,继续从第10行开始执行就行了。

这样我们就感受到了 程序计数器 的作用,每个线程都有这么一个程序计数器,主要记录当前每个线程执行的代码行号。

2.2 Java堆

是一块 线程共享的区域,主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出 OutOfMemoryError 异常,也就是内存溢出。

注意:堆作为线程共享的区域,肯定会存在线程安全问题的。

1)JVM 内存结构

下面我们来看一下 Java堆 中的结构是怎样的,大家来看这个图:

这个就是运行数据区了,这里面就包含了:

  • 虚拟机栈、本地方法栈、程序记录器,这些我们介绍过了。
  • 这里面还有一个 本地内存,这里就包含了 直接内存元空间,元空间就是之前的方法区。
  • 最右边的就是 了。

我们重点来看一下堆里面的内容,可以看到它是分了两部分:

  • 年轻代:被划分为三部分:Eden区和两个大小严格相同的 Survivor区,也叫幸存者区(S0、S1)。

    根据 JVM 的策略,一个对象实例化后会先到 Eden区,假如对象在垃圾回收之后还存活,他就会被复制移动到 S0 或者 S1。假如在经过几次垃圾回收之后,对象依然存活于 Survivor区,它就会被放到老年代

  • 老年代:主要指的是生命周期比较长的对象,一般是一些老的对象。

关于对象的具体挪动规则,我们后面介绍垃圾回收的时候会再详细说明。

这里还有一个内容,我们要再介绍一下,就是元空间:

  • 元空间 的主要作用是 用来保存类的信息,静态变量、常量,还有编译后的代码

其实,在 Java8 之前,堆中有一个叫做 永久代 的东西,它跟元空间的作用是一样的。这时候面试官可能会再问这么个问题,说 Java 的 1.7 和 1.8,它们的堆的区别是什么?

2)Java 1.7 和 1.8 中堆的区别

大家来看图:

在这里插入图片描述

左边的是 Java7 的内存结构,右边是 Java 8 的内存结构,我们会发现 Java7 中的堆有一个叫部分做 “方法区/永久代”,但是在 Java8 中并没有。是这样的,到了 Java8 版本后,JVM 把 当前的 “方法去/永久代” 放到了本地内存,也就是元空间中。

为什么要放到本地内存呢?是这样的,因为元空间或者说方法区中主要存储的是一些类或者常量,那么项目随着动态类加载的情况会越来越多,那么这块内存就会变得不可控:

  • 如果内存分配小了,系统运行的过程中就会容易出现内存溢出。
  • 如果内存分配大了,又会导致浪费内存。

所以说 Java8 之后就做了优化,现在都放到了本地内存,就是为了能够让堆去节省空间,防止内存溢出。其实我们最终的目的都是为了避免 OOM,防止内存溢出。

Java 1.7 和 Java 1.8 中堆区别-总结:

  • 1.7 中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码。
  • 1.8 移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出。

2.3 Java虚拟机栈

1)虚拟机栈 和 栈帧

Java虚拟机栈:英文是 Java Virtual machine Stacks,每个线程运行时所需要的内存,称为虚拟机栈,它的特点就是先进后出

  • 每个线程运行的时候都会创建虚拟机栈,所以栈内存也是线程安全的。
  • 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所需要的数据,或者说占用的内存。

如上图所示,栈帧里面就包含了方法的参数、局部变量、返回地址。如果当前方法调用了其他方法,就会对应有其他的栈帧。但是:

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

假如栈帧1调用了栈帧2,栈帧2又调用了栈帧3,就会逐个进行压栈操作,最终如下所示:

当方法执行完毕之后,先是栈帧3弹栈,就会释放栈帧3的内存,其次是栈帧2,最后才是栈帧1,最终操作结果如下所示:

现在我们熟悉了虚拟机栈以后,我们来回答几个面试题:

2)常见面试题

1.垃圾回收是否涉及栈内存?

垃圾回收主要指的是堆内存,当栈帧弹栈以后,内存就会释放,这里并不需要垃圾回收器去回收。

2.栈帧内存分配的越大越好吗?

未必,默认的栈帧内存通常为 1024KB,即 1MB。栈帧过大会导致线程数变少。

例如:机器总内存为 512MB,目前能活动的线程数则为 512 个,如果栈内存改为 2048KB,那么能活动的栈帧就会减半。

3.方法内的局部变量是否线程安全?

  • 如果方法内局部变量没有逃离方法的作用范围,那么它是线程安全的。
  • 如果是局部变量引用了对象,并逃离方法的作用范围,则需要考虑线程安全。

(如果局部变量逃逸了,线程就不安全。)

举个例子:如下图所示,分别观察 m1()、m2()、m3() 中的 sb 变量是否存在线程安全问题?

  • m1()方法: m1() 方法中的 sb 就是一个局部变量,然后在这里面添加了两个数据,1和2,最终打印了一下当前的数据就结束了。这种情况下,局部变量 sb 就是线程安全的。因为在 m1() 中,对于局部变量 sb 来说,每个线程来了以后,都会创建这么一个栈帧,那每个栈帧都会有这样一个局部变量 sb。即使我们操作了成千上万次,它也会去创建成千上万次,也就是说对于每个线程来说,局部变量 sb 都是独有的,所以说并没有线程安全问题
  • m2()方法: m2() 方法中有一个行参 StringBuilder,然后往里面添加了1和2两个数据,最终也是打印了一下。它是线程安全的吗?并不是,虽然形参 sb 也是一个局部变量,但是在这个参数传递的过程当中,有可能会被其他线程调用,比如 main() 方法中就开启了一个新的线程来去调用 m2() 方法,同时 main() 方法中也有一个局部变量 sb,也就是说 main() 方法也在操作当前的局部变量,那么 main() 方法所对应的线程和 m2() 方法所对应的线程,多个线程在同时操作局部变量 sb 进行添加数据,两个线程共用了同一个局部变量,所以说不是线程安全的
  • m3()方法: m3() 跟 m2() 的情况是一样的,它也不是线程安全的,它虽然没有记录形参,但是它会把局部变量进行返回,那么这个局部变量也有可能被其他线程公用,比如我们可以在 main() 方法中去调用 m3() 方法,得到局部变量 sb,然后在 main() 方法中开启多个线程同时去操作这个变量,那它也就成了多个线程共用的变量,那也就线程不安全了

4.栈内存溢出有哪些情况?

  • 栈帧过多导致栈内存溢出。

    典型问题:递归调用,如下所示:

  • 栈帧过大导致栈内存溢出。

    一个栈帧默认有 1MB 的内存,一个虚拟机栈一般不会出现超过 1MB 的内存,所以这个情况出现少一些。

5.堆和栈的区别是什么?

  • 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储 Java 对象和数组的。堆会GC垃圾回收,而栈不会。

  • 栈内存是线程私有的,而堆内存是线程共享的,要考虑线程安全的问题。

  • 两者异常错误不通,但如果栈内存或者堆内存不足都会抛出异常:

    栈空间不足:java.lang.StackOverFlowError。

    堆空间不足:java.lang.OutOfMemoryError。

2.4 方法区/元空间

1)方法区/元空间的介绍

首先我们来看一下方法区所在的位置,在图中可以看到,方法区属于运行数据区的一部分。

下面是关于方法区的介绍:

  • 方法区(Method Area) 是各个线程共享的内存区域(跟我们之前讲过的堆空间是一样的)。

  • 主要存储类的信息、运行时常量池。

  • 方法区是在虚拟机启动的时候创建,关闭虚拟机时释放元空间的内存。

  • 如果方法区域中的内存无法满足分配请求,则会抛出 OutOfMemoryError: Metaspace。

    (有时也会明确提示元空间太小了)

大家看下面的图:

方法区逻辑上是属于堆的一部分,但是不同的厂商存储的位置不太一样,我们目前都是用的 Oracle 提供的 HotSpot 编译器。在 JDK8 之前,方法区是存储在堆中一个叫永久代的存储区域中,但是在 JDK8 之后把永久代给移除了,换了一种实现,这种实现就叫元空间(Metaspace)

  • 元空间就不在堆内存中了,它用的是本地内存,也就是操作系统的内存。为什么要挪动到这里呢,就是为了避免OOM。

下面我们来看元空间存储了哪些内容:

  • Class: 这个就是类的信息,包含了:类的结构、方法、字段等。
  • Classloader: 这个是加载类的。
  • 运行常量池: 后面我们会专门介绍。
2)复现元空间不足的场景

首先,我们先准备这样一个类,实现生成 1w 个类信息,看看它会不会出问题:

package com.demo.test;

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class MetaspaceDemo extends ClassLoader {

    public static void main(String[] args) {
        MetaspaceDemo demo = new MetaspaceDemo();
        for (int i = 0; i < 10000; i++) {
            // ClassWriter 作用是生成类的二进制字节码
            ClassWriter cw = new ClassWriter(0);
            // 入参:版本号,public,类名,包名,父类,接口
            cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
            // 返回 byte[]
            byte[] bytes = cw.toByteArray();
            // 执行了类的加载
            demo.defineClass("Class" + i, bytes, 0, bytes.length); // Class 对象
        }
    }
}

如果是正常执行,不会有任何报错:

在这里插入图片描述

我们需要在启动命令中限制一下元空间的大小,在IDEA中编辑启动配置,选择 “Add VM options”。

-XX:MaxMetaspaceSize=8m 拷贝到输入框中,点击 “Apply”。

再次启动,我们就可以看到如下报错:

  • Exception in thread “main” java.lang.OutOfMemoryError: Metaspace

在这里插入图片描述

3)常量池

常量池 可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。

我们还是可以通过 javap 命令来查看字节码的结构,包含了三部分内容:类的基本信息、常量池、方法定义。

我们还是找到之前用过的一个 Application 类进行演示:

package com.demo.jvm;

public class Application {
	public static void main(String[] args) {
		System.out.println("hello world");
	}
}

先使用 javac 命令进行编译:

javac Application.java

然后使用 javap 命令来查看常量池:

javap -v Application.class

完整的执行结果如下所示:

Classfile /D:/test/Application.class
  Last modified 2024-4-6; size 440 bytes
  MD5 checksum d8a7300dcbdbc1a7c3b962f3f6420821
  Compiled from "Application.java"
public class com.demo.jvm.Application
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // hello world
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // com/demo/jvm/Application
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Application.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               hello world
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               com/demo/jvm/Application
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public com.demo.jvm.Application();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}
SourceFile: "Application.java"

首先,第一部分是类的基本信息介绍。

在这里插入图片描述

下面这部分就是常量池了:

在这里插入图片描述

再往下就有一些方法的定义,首先第一个是默认的无参构造函数,其次是 main() 方法。

在这里插入图片描述

对应关系如下所示:

在这里插入图片描述

4)运行时常量池
  • 常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会被放入 运行时常量池,并把里面的 符号地址变为真实地址。我们前面提到的 #1、#2、#3 就是符号地址。

在这里插入图片描述

2.5 直接内存

直接内存:并不属于 JVM 中的内存结构,不由 JVM 进行管理,属于虚拟机所在操作系统的内存。常见于 NIO 操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高

  • 补充:我们平时的 IO 叫做 BIO,NIO 要比 BIO 的吞吐量高很多。
1)常规IO 和 NIO 性能对比

举个例子:比如说我们要用 Java 代码完成一次文件的拷贝,将文件从 E:/bak1/ 复制到 E:/bak2/ 中,如下所示:

在这里插入图片描述

这样我们有两种实现方式:常规IO,或者 NIO。下面我们我们就用这两种方式来实现一下,对比一下它们的不同。

实现代码如下:

package com.demo.test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 演示 ByteBuffer 作用
 */
public class DirectMemoryDemo {
    public static final String FROM = "E:\\bak1\\01-java成神之路.mp4";
    public static final String TO = "E:\\bak2\\abc.mp4";
    public static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // 659.7956 ms
        directBuffer(); // 370.8198 ms
    }

    /**
     * 常规IO
     */
    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0 + " ms");
    }

    /**
     * NIO
     */
    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(byteBuffer);
                if (len == -1) {
                    break;
                }
                byteBuffer.flip(); // 切换到读模式
                to.write(byteBuffer);
                byteBuffer.clear(); // 切换到写模式
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0 + " ms");
    }
}

文件大小为:325MB

执行结果如下所示:

在这里插入图片描述

可以看到差距还是很明显的,NIO 几乎比常规IO 快了一倍,NIO 的读写效率更高一些。为什么 NIO 的读写效率更高呢?下面我们就一起来分析一下。其实主要还是跟 “直接内存” 有很大关系。

2)常规IO 和 NIO 分析

我们先来看一下常规IO 是怎么操作的,我们先来看个图:

在这里插入图片描述

这个就是常规IO 数据的操作流程。

  • 首先我们要知道,Java 本身并不支持磁盘读写的能力,它要调用磁盘读写的话必须调用我们操作系统提供的函数,这里就是调用本地的(native)方法。我们之前说过 native 修饰的方法都是操作系统提供的方法,Java 就是使用 native 修饰的方法来去操作磁盘文件。
  • 这里就涉及到了 CPU 的运行状态:用户态、内核态。
    • 我们首先会从 Java 的用户态切换到内核态,这个就是一个 CPU 状态的改变。但其实内存这一块儿也会有一些相关的操作:
      1. 当切换到内核态之后,这时候就由本地的函数去读取磁盘中的文件,读取到之后,会在操作系统中划出一块儿缓冲区,我们称之为 “系统缓冲区”。磁盘内容就会先读入到系统缓冲区中,它不可能把一个 300MB 的文件一次性读取到内存中,那样的话内存太紧张了,所以说它会利用缓冲区分批次地去读取。
      2. 这里要注意,这个 “系统缓冲区” 我们 Java 代码是不能够运行的。所以说 Java 会在堆中分配一块儿内存,Java 的缓冲区对应我们代码中的 new byte[]我们 Java 代码要想访问刚才读到的文件流数据,必须要从系统的缓冲区间接地读取到 Java 的缓冲区中。
      3. 读入到 Java 缓冲区之后,进程就会进入到了下一个状态,我们再去调用输出流的写入操作,这样反复进行读取,我们的文件就能复制到目标的位置。

这里我们也发现了问题所在了:由于我们有两块缓冲区,也就是两块内存:一个是系统提供的系统缓冲区,第二个是 Java 中有一个 Java 的缓冲区。读取数据的时候就必然会涉及到数据要去存两份的问题,第一次先读取到系统中去,第二次才能读取到 Java 的缓冲区。因为我们 Java 代码本身是访问不到系统缓冲区的,我们必须要把它读取到 Java 缓冲区之后,才能对它进行操作。这里就造成了一次不必要的数据复制,因此效率就不是很高。

以上就是 常规IO 的操作,下面我们介绍一下 NIO 是怎么做的:

在这里插入图片描述

这里面就用到了直接内存,也就是说在操作系统中划出了一块儿缓冲区,这块缓冲区和 常规IO 不一样的地方在于操作系统划分的内存,Java代码是可以访问的!换句话说,这块儿内存,系统可以访问它,Java代码也能够访问它。它是两端代码都可以共享的内存区域,这就是 直接内存

加入了直接内存之后,大家可以很明显地看出来,磁盘文件在读取的时候,Java代码操作起来就非常方便了。其实就是比我们刚才的代码少了一次缓冲区的复制操作,所以这个速度就得到了成倍的提升。这就是直接内存给我们带来的好处,它确实比较适合这种文件的IO操作。

总结一下直接内存:

  • 直接内存并不属于 JVM 的内存结构,不由 JVM 进行管理,是虚拟机所在的操作系统内存。
  • 直接内存常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理。

整理完毕,完结撒花~🌻





参考地址:

1.新版Java面试专题视频教程,java八股文面试全套真题+深度详解(含大厂高频面试真题),https://www.bilibili.com/video/BV1yT411H7YK

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/524724.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Transformer模型-Feed Forward前馈网络和Relu激活函数的简明介绍

今天介绍transformer模型的Feed Forward network前馈网络和Relu激活函数 背景 位置感知Position-Wise前馈网络&#xff08;FFN&#xff09;由两个全连接层&#xff08;fully connected dense layers&#xff0c;就是线性层&#xff08;Linear Layer&#xff09;&#xff0c;或密…

LLM-base版本和chat版本的比较

突然想到了这个问题&#xff0c;网上搜集了一些资料&#xff0c;自己也总结一下 首先放一张llama2论文当中的图&#xff0c;可以很直观的看到区别 面试回答版 问题&#xff1a; 大语言模型base版和chat版的区别是什么&#xff1f; 回答&#xff1a; base版本更适合文本补全…

医疗器械FDA | 常见的网络安全材料发补问题都有哪些?

FDA网络安全资料发补咨询点此​​获取https://work.weixin.qq.com/ca/cawcde5ee29d239046 ————————--- 01 安全文档编写问题 FDA网络安全文档编写格式、内容、可读性等未满足官方要求&#xff0c;则将可能被要求发补整改编写后的文档。 02 安全管理问题 a. 网络安…

Apache-Pulsar安装操作说明

说明 Pulsar 是一种用于服务器到服务器消息传递的多租户高性能解决方案。 Pulsar 的主要特性如下&#xff1a; 对 Pulsar 实例中的多个集群的本机支持&#xff0c;并跨集群无缝地复制消息。 极低的发布和端到端延迟。 无缝可扩展至超过一百万个主题。 一个简单的客户端 API&…

C语言--#运算符和##运算符

#运算符和##运算符 这里提前补充一点printf C语言里面会天然的吧printf里面俩字符串合并为一个 #的使用 在C语言中&#xff0c;#符号主要用于预处理器指令。预处理器是在代码编译之前执行的&#xff0c;它处理所有以#开始的指令。以下是一些常见的使用情况&#xff1a;1. **…

自定义校验器

1.前端校验 <template><el-dialog:title"!dataForm.brandId ? 新增 : 修改":close-on-click-modal"false":visible.sync"visible"><el-form:model"dataForm":rules"dataRule"ref"dataForm"keyu…

MySQL-主从复制:概述、原理、同步数据一致性问题、搭建流程

主从复制 1. 主从复制概述 1.1 如何提升数据库并发能力 一般应用对数据库而言都是“读多写少”&#xff0c;也就说对数据库读取数据的压力比较大&#xff0c;有一个思路就是采用数据库集群的方案&#xff0c;做主从架构、进行读写分离&#xff0c;这样同样可以提升数据库的并…

基于R语言、MaxEnt模型融合技术的物种分布模拟、参数优化方法、结果分析制图与论文写作

第一章、理论篇&#xff1a;以问题导入的方式&#xff0c;深入掌握原理基础 什么是MaxEnt模型&#xff1f; MaxEnt模型的原理是什么&#xff1f;有哪些用途&#xff1f; MaxEnt运行需要哪些输入文件&#xff1f;注意那些事项&#xff1f; 融合R语言的MaxEnt模型的优势&…

从0到1搭建文档库——sphinx + git + read the docs

sphinx git read the docs 目录 一、sphinx 1 sphinx的安装 2 本地构建文件框架 1&#xff09;创建基本框架&#xff08;生成index.rst &#xff1b;conf.py&#xff09; conf.py默认内容 index.rst默认内容 2&#xff09;生成页面&#xff08;Windows系统下&#xf…

大厂高频面试题复习JAVA学习笔记-JUC多线程及高并发(上)

目录 0 JUC基础概念 wait/sleep的区别 并发与并行的区别 线程的六个状态 JUC结构 ​编辑 1 请谈谈你对volatile的理解 JMM&#xff08;java内存模型&#xff09; 可见性 不保证原子性 有序性​编辑 指令重排 哪些地方用到volatile&#xff1a; 双端检查机制DLC …

PaddleVideo:PP-TSM视频分类

本文记录&#xff1a;使用Paddle框架训练TSM&#xff08;Temporal Shift Module&#xff09; 前提条件&#xff1a;已经安装Paddle和PadleVideo&#xff0c;具体可参考前一篇文章。 1-数据准备&#xff1a; 以UCF101为例&#xff1a;内含13320 个短视频&#xff0c;视频类别&…

数据库相关知识总结

一、数据库三级模式 三个抽象层次&#xff1a; 1. 视图层&#xff1a;最高层次的抽象&#xff0c;描述整个数据库的某个部分的数据 2. 逻辑层&#xff1a;描述数据库中存储的数据以及这些数据存在的关联 3. 物理层&#xff1a;最低层次的抽象&#xff0c;描述数据在存储器中时如…

Prometheus-Grafana基础篇安装绘图

首先Prometheus安装 1、下载 https://prometheus.io/download/ 官网路径可以去这儿下载 2、如图&#xff1a; 3.解压&#xff1a; tar -xf prometheus-2.6.1.linux-amd64 cd prometheus-2.6.1.linux-amd64 4.配置文件说明&#xff1a; vim prometheus.yml 5.启动Promethe…

基于 Vue3 + Webpack5 + Element Plus Table 二次构建表格组件

基于 Vue3 Webpack5 Element Plus Table 二次构建表格组件 文章目录 基于 Vue3 Webpack5 Element Plus Table 二次构建表格组件一、组件特点二、安装三、快速启动四、单元格渲染配置说明五、源码下载地址 基于 Vue3 Webpack5 Element Plus Table 二次构建表格组件&#x…

c++的学习之路:15、list(2)

本章主要是讲模拟实现list&#xff0c;文章末附上代码。 目录 一、创建思路 二、构造函数 三、迭代器 四、增删 五、代码 一、创建思路 如下方代码&#xff0c;链表是由一块一块不连续的空间组成的&#xff0c;所以这里写了三个模板&#xff0c;一个是节点&#xff0c;一…

书生浦语训练营二期第三次作业

文章目录 基础作业1. 在茴香豆 Web 版中创建自己领域的知识问答助手第一轮对话第二轮对话第三轮对话第四轮对话第五轮对话 2.在 InternLM Studio 上部署茴香豆技术助手修改配置文件创建知识库运行茴香豆知识助手 基础作业 1. 在茴香豆 Web 版中创建自己领域的知识问答助手 我…

2-django、http、web框架、django及django请求生命周期、路由控制、视图层

1 http 2 web框架 3 django 3.1 django请求生命周期 4 路由控制 5 视图层 1 http #1 http 是什么 #2 http特点 #3 请求协议详情-请求首行---》请求方式&#xff0c;请求地址&#xff0c;请求协议版本-请求头---》key:value形式-referer&#xff1a;上一次访问的地址-user-agen…

Vue3与TypeScript中动态加载图片资源的解决之道

在前端开发中&#xff0c;Vue.js已成为一个备受欢迎的框架&#xff0c;尤其是在构建单页面应用时。Vue3的发布更是带来了许多性能优化和新特性&#xff0c;而TypeScript的加入则进一步提升了代码的可维护性和健壮性。然而&#xff0c;在实际的项目开发中&#xff0c;我们有时会…

校园圈子小程序,大学校园圈子,三段交付,源码交付,支持二开

介绍 在当今的数字化时代&#xff0c;校园社交媒体和在线论坛成为了学生交流思想、讨论问题以及分享信息的常用平台。特别是微信小程序&#xff0c;因其便捷性、用户基数庞大等特点&#xff0c;已逐渐成为构建校园社区不可或缺的一部分。以下是基于现有资料的校园小程序帖子发…

Redis中的Sentinel(六)

Sentinel 选举领头Sentinel. 当一个主服务器被判断为客观下线时&#xff0c;监视这个下线主服务器的各个Sentinel会进行协商&#xff0c;选举出一个领头Sentinel,并由领头 Sentinel对下线主服务器执行故障转移操作。以下是Redis选举领头Sentinel的规则和方法: 1.所有在线的S…