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.classLast modified 2024-4-6; size 440 bytesMD5 checksum d8a7300dcbdbc1a7c3b962f3f6420821Compiled from "Application.java"
public class com.demo.jvm.Applicationminor version: 0major version: 52flags: 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: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #3                  // String hello world5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 5: 0line 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.classLast modified 2024-4-6; size 440 bytesMD5 checksum d8a7300dcbdbc1a7c3b962f3f6420821Compiled from "Application.java"
public class com.demo.jvm.Applicationminor version: 0major version: 52flags: 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: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #3                  // String hello world5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 5: 0line 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 msdirectBuffer(); // 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

搜索二维矩阵2 合并两个有序链表

240. 搜索二维矩阵 II - 力扣&#xff08;LeetCode&#xff09; class Solution { public:bool searchMatrix(vector<vector<int>>& matrix, int target) {int i matrix.size() - 1, j 0;while(i > 0 && j < matrix[0].size()){if(matrix[i][j…

基于wsl的Ubuntu20.04上安装桌面环境

在子系统Ubuntu20.04上安装桌面环境 1. 更换软件源 由于Ubuntu默认的软件源在国外&#xff0c;有时候后可能会造成下载软件卡顿&#xff0c;这里我们更换为国内的阿里云源&#xff0c;其他国内源亦可。 双击打开Ubuntu20.04 LTS图标&#xff0c;在命令行中输入 # 备份原来的软…

Java(二)面向对象进阶

目录 面向对象 多态性 向下转型 Object equals() toString() clone() finalize() Static 单例模式 代码块 final 抽象类与抽象方法(或abstract关键字&#xff09; 接口 接口的多态性 接口的默认方法 内部类 成员内部类 局部内部类 枚举类 实现接口的枚举类 …

网络安全流量平台_优缺点分析

FlowShadow&#xff08;流影&#xff09;&#xff0c;Ntm&#xff08;派网&#xff09;&#xff0c;Elastiflow。 Arkimesuricata&#xff0c;QNSMsuricata&#xff0c;Malcolm套件。 Malcolm套件优点&#xff1a;支持文件还原反病毒引擎&#xff08;clamav/yara&#xff09;…

IntelliJ IDEA 2024.1 更新亮点汇总:全面提升开发体验

IntelliJ IDEA 2024.1 更新亮点汇总&#xff1a;全面提升开发体验 文章目录 IntelliJ IDEA 2024.1 更新亮点汇总&#xff1a;全面提升开发体验摘要引言 IntelliJ IDEA 2024.1 的新增功能主要亮点全行代码完成 最终的支持 Java 22 功能新航站楼 贝塔编辑器中的粘滞线 人工智能助…

【SpringBoot3】SpringBoot入门

需求&#xff1a;使用 SpringBoot 开发一个web应用&#xff0c;浏览器发起请求 /hello后&#xff0c;给浏览器返回字符串 “hello world "。 步骤 ①. 创建Maven工程 ②. 导入spring-boot-stater-web起步依赖 <dependency> <groupId>org.springframework…

React18从入门到实战

文章目录 一、React环境的搭建二、项目文件的介绍&#xff08;1&#xff09;package.json&#xff0c;他是项目存放依赖包的地方&#xff0c;里面包括了一些项目核心包及下载的其他插件包&#xff08;2&#xff09;src文件夹是项目源码目录&#xff0c;平时开发页面就在其中&am…

Leetcode 581. 最短无序连续子数组

心路历程&#xff1a; 本以为这道题要用动态规划求解&#xff0c;因为题目中这几个关键字与动态规划太匹配了&#xff0c;结果想了半天也没发现dp(i)和dp(i-1)的递推关系。 这道题本意考察双指针的做法&#xff0c;也可以用排序后做比较的方式来做。 注意的点&#xff1a; 1…

修电机所需要的基本工具

等距式 模具 同心式模具 电机划线刀 压脚 千分尺 -----测量线径 钳形电流表------- 测量 空载 满载下的电流值 摇表&#xff0c; 测量线圈是否碰到外壳 指针式万用表 胶锤 整理线圈 绝缘纸和青稞纸&#xf…

服务器主机安全受到危害的严重性

为了让小伙伴们了解到服务器主机安全受到危害的严重性&#xff0c;以下详细说明一下&#xff1a;1. 数据泄露&#xff1a;如果服务器主机遭受攻击&#xff0c;攻击者可能会窃取敏感数据&#xff0c;如用户数据、商业秘密、机密文件等&#xff0c;导致数据泄露和商业机密的泄漏。…

设计模式深度解析:AI大模型下的策略模式与模板方法模式对比解析

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》《MYSQL应用》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 策略模式与模板方法模式对比解析 文章目录 &#x1f31f;引言&#x1f31f;Part 1:…

【单源最短路 图论】882. 细分图中的可到达节点

作者推荐 视频算法专题 本文涉及知识点 单源最短路 图论 LeetCode 882. 细分图中的可到达节点 给你一个无向图&#xff08;原始图&#xff09;&#xff0c;图中有 n 个节点&#xff0c;编号从 0 到 n - 1 。你决定将图中的每条边 细分 为一条节点链&#xff0c;每条边之间…

Spring Security——11,自定义权限校验方法

自定义权限校验方法 一键三连有没有捏~~ 我们也可以定义自己的权限校验方法&#xff0c;在PreAuthorize注解中使用我们的方法。 自定义一个权限检验方法&#xff1a; 在SPEL表达式中使用 ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的 hasAuthority方法&am…

hadoop分布式计算组件

什么是计算、分布式计算&#xff1f; 计算&#xff1a;对数据进行处理&#xff0c;使用统计分析等手段得到需要的结果 分布式计算&#xff1a;多台服务器协同工作&#xff0c;共同完成一个计算任务 分布式计算常见的2种工作模式 分散->汇总(MapReduce就是这种模式)中心调…

【Linux系列】如何确定当前运行的是 RHEL 9 还是 RHEL 8?

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

物联网可视化平台

随着数字化转型的深入&#xff0c;物联网技术正在成为企业实现智能化、高效化运营的重要工具。物联网可视化平台&#xff0c;作为连接物理世界与数字世界的桥梁&#xff0c;为企业提供了直观、实时的数据展示和监控能力&#xff0c;从而在数字化转型中扮演着关键角色。 一、物…

抖音-引流私域转化模式1.0现场视频,从抖音源源不断把人加到私域,

抖音-引流私域转化模式1.0现场视频&#xff0c;从抖音源源不断把人加到私域&#xff0c;让加到私域的粉丝买单 抖音-引流私域转化模式1.0现场视频&#xff0c;从抖音源源不断把人加到私域 - 百创网-源码交易平台_网站源码_商城源码_小程序源码 课程内容&#xff1a; 01.第一…

后端nginx使用set_real_ip_from获取用户真实IP

随着nginx的迅速崛起&#xff0c;越来越多公司将apache更换成nginx. 同时也越来越多人使用nginx作为负载均衡, 并且代理前面可能还加上了CDN加速&#xff0c;但是随之也遇到一个问题&#xff1a;nginx如何获取用户的真实IP地址. 前言&#xff1a;Nginx ngx_http_realip_module…

深入理解计算机系统 家庭作业 2.96

题目出的很不好,感觉没有标准. #include <stdio.h>typedef unsigned float_bits;int float_f2i(float_bits f) {unsigned sign f >> (31);unsigned exp (f >> 23) & 0xff;unsigned frac f & 0x7fffff;unsigned add (frac & 0x3) 0x3;unsig…

Pytorch张量的数学运算:向量基础运算

文章目录 一、简单运算二、广播运算1.广播的基本规则2.广播操作的例子 三、运算函数 参考&#xff1a;与凤行 张量的数学运算是深度学习和科学计算中的基础。张量可以被视为一个多维数组&#xff0c;其在数学和物理学中有广泛的应用。这些运算包括但不限于加法、减法、乘法、除…