【黑马JVM(1)】内存结构

news2024/11/18 17:50:15

JVM

  • JVM/JRE/JDK
    • 示例
  • JVM内存管理
    • JVM整体架构
    • 程序计数器
  • 虚拟机栈
    • 栈内存溢出
    • 线程诊断 top/ps -H/jstack
      • 案例一:CPU占用过多
      • 案例二: 程序运行很长时间没结果
  • 本地方法栈
    • 堆内存溢出
    • 堆内存诊断
      • 案例一:jps/jmap/jconsole工具使用
      • 案例二:垃圾回收后,内存占用仍然很高
  • 方法区
    • 方法区内存溢出
  • 常量池
    • StringTable
      • 示例一:字面量创建字符串
      • 示例二:字符串变量拼接
      • 示例三:字符串常量拼接
      • 示例三:intern方法
    • StringTable特性
      • StringTable优化
  • 直接内存
    • IO/DirectBuffer拷贝文件
      • 直接内存溢出
  • JVM参数

JVM/JRE/JDK

  • JVM: Java Virtual Machine的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算
    机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
    JVM 保正确执行 .class 文
    件,实现在 Linux、Windows、MacOS 等平台上运行。
  • JRE Java Runtime Environment的缩写,JVM 标准加上实现的一大堆基础类库就组成了Java的运行环境
    提供运行Java应用程序所必须的软件环境等。

JDK Java Development Kit的缩写,提供了一些非常好用的小工具,比如 javac、java、jar 等。
提供Java开发工具包

在这里插入图片描述
JVM好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

示例

简单看下Java程序的执行过程:
在这里插入图片描述

在这里插入图片描述

运行下面代码:遵循的就是 Java 语言规范。其中,调用了System.out 等模块,也就是 JRE 里提供的类库。

public class HelloTest {
	public static void main(String[] args) {
		System.out.println("Hello World");
		}
}

使用 JDK 的工具 javac 进行编译后,会产生 HelloWorld 的字节码。javap查看下字节码内容:
在这里插入图片描述

控制端输入:javap -v HelloTest

javap 将class反编译
v显示基本信息

查看main方法内:

0 getstatic #2 <java/lang/System.out> // getstatic 获取静态字段的值
3 ldc #3 <Hello World> // ldc 常量池中的常量值入栈
5 invokevirtual #4 <java/io/PrintStream.println> // invokevirtual 运行时方法绑定调用方法
8 return //void 函数返回

Java 虚拟机采用基于栈的架构,其指令由操作码和操作数组成。这些字节码指令,就叫作 opcode。其中,getstatic、ldc、invokevirtual、return 等,就是 opcode。
JVM 就是靠解析 opcode 和操作数来完成程序的执行的。当使用 Java 命令运行 .class 文件的时候,实际上就相当于启动了一个 JVM 进程。
然后 JVM 会翻译这些字节码,它有两种执行方式。

  • 常见的就是解释执行,将 opcode + 操作数翻译成机器代码;
  • 另外一种执行方式就是 JIT,即时编译,会在一定条件下将字节码编译成机器码之后再执行。

JVM内存管理

JVM整体架构

JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
在这里插入图片描述

程序计数器

Program Counter Register 程序计数器记住下一条jvm指令要执行的地址。(Java对硬件的抽象物理上通过寄存器实现(读取速度快))

当前指令所在的内存地址+当前指令所占用的字节数=下一条指令所在的内存地址

虚拟机指令的执行流程: 拿到一条指令,通过解释器翻译成机器码,才能交给CPU运行,程序计数器就是记住下一条jvm指令的执行地址。
在这里插入图片描述
在这里插入图片描述
观察这张图,不难发现每个指令前都有一个数字,这是指令的偏移地址,简称为行号,程序计数器的作用就是存储下一条指令的行号。
  以上图为例,当第一条指令执行时,JVM会将下一条指令的行号“3”放入程序计数器中。当第一条指令执行完时,解释器会根据程序计数器中的行号拿到下一条指令,继续运行。

为什么要多此一举先把下一条指令的行号放入程序计数器中,直接取指令然后执行不是更简单吗?
  JVM是支持多个线程同时运行的,这就涉及到CPU的调度问题了。线程甲正执行的好好的, 大哥CPU告诉甲说,你累了,我陪会儿乙,甲只好乖乖休息。一段时间后,大哥回来了,这时甲就可以根据程序计数器中的行号取到下一条指令接着执行。
  这里有一点要注意,因为程序计数器记录的是行号,是会重复的,所以多个线程不能同时用一个,不然就乱了。所以程序计数器是线程私有的。
  
特点:

  • 是线程私有的(每个线程执行的指令地址不一样)
  • 不会存在内存溢,程序计数器中的行号永远只会有一个,当前指令执行时,会拿下一条指令的行号替换当前的行号。因此就不存在内存溢出问题。

查看二进制字节码:

1.安装插件:
在这里插入图片描述
2.查看

在这里插入图片描述

虚拟机栈

Java Virtual Machine Stacks 虚拟机栈,线程运行时所需要的内存空间。
一个线程对应一个虚拟机栈,多个线程对应多个虚拟机栈。
一个虚拟机栈,由多个栈帧(Frame)组成。一个栈帧对应着一次方法调用,方法调用时所需要的内存为栈帧,存储参数,局部变量,返回地址等。
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

垃圾回收是否涉及栈内存?
不涉及,栈内存是一次次方法调用时所产生的栈帧内存,栈帧内存在每次方法调用结束后会弹出栈,自动回收掉。
垃圾回收只会回收掉对内存中的内存占用,栈内存不需要。

栈内存分配越大越好吗?

-Xss 设置栈帧大小

操作系统默认栈大小:
在这里插入图片描述
每个线程在创建的时候都会创建一个虚拟机栈,而物理内存是固定的,栈内存划分的越大, 可分配的线程数就越少。

方法内的局部遍历是否线程安全?
如果方法内部局部变量没有逃离方法的作用范围它就是安全的,是线程私有的 (栈帧独有),不会产生在多个线程下产生线程干扰。
​ 如果局部变量引用了对象,并逃离方法的作用范围,能被其他线程访问到,就不是线程安全的。

在这里插入图片描述

栈内存溢出

1.栈帧过多导致栈内存溢出(递归调用)

2.栈帧过大导致栈内存溢出

在这里插入图片描述

栈帧设置为256k,只循环3237次
在这里插入图片描述

线程诊断 top/ps -H/jstack

案例一:CPU占用过多

nohup java cc.java &

nohup 是 no hang up 的缩写,意思是不挂断运行,一直运行下去
& 是在后台运行的意思

top 命令检测进程对CPU的使用,定位哪个进程对cpu占用过高

在这里插入图片描述

ps H -eo pid,tid,%cpu

ps 查看线程情况
H 打印进程数
eo 输出要查看的内容 pid(进程ID) tid (线程id) %cpu
在这里插入图片描述

ps H -eo pid,tid,%cpu | grep 32655 定位哪个线程对CPU占位过高

在这里插入图片描述

jstack 32655(进程id) 会输出相关堆栈信息

jstack输出的线程信息是16进制的,我们将tid 32665 用计算器转为10进制可以看到是 7f99
线程的状态是runnable,显示第8行有问题,可以定位到Java源代码
在这里插入图片描述

案例二: 程序运行很长时间没结果

运行一段程序,想输出一个结果,但是一直不输出,可以查看相关堆栈信息

jstack pid

jstack最后一段输出中,发现Found one Java-Level deadlock,找到死锁,定位到Java源码的Thread-1的29行和Thread-0的21行。
在这里插入图片描述

本地方法栈

Native Method Stacks:java虚拟机调用本地方法时,所提供的内存空间。
本地方法定义:不是由java代码写的方法,由C或C++所写(因为c/c++方便和操作系统打交道,Java间接的通过调用本地方法来进行工作)。

本地方法例如:Object类,native修饰符等,Java的基础类库或执行引擎里都会调用本地方法。

Heap:通过 new 关键字创建对象都会使用堆内存。

特点

  • 是线程共享的,所以堆中的对象都需要考虑线程安全的问题
    虚拟机栈中的局部变量都是线程私有的,只要它的局部变量不逃逸出方法的作用范围,就是线程安全的,但是堆中的对象,要考虑线程安全,因为它们是线程共享的。当然也有例外,后面讲解。
  • 堆里面有一个垃圾回收机制
    堆中不再被引用的对象,就会当成垃圾进行回收,以释放空闲的内存,不至于内存被创建的对象给撑爆。

堆内存溢出

当一个对象不被使用时,就可以成为所谓垃圾被回收掉,也就是它占用的内存会被释放掉。怎么还可能出现堆内存耗尽呢?

可以被回收的一个对象的条件是没人在使用它,但是如果不断的产生对象,而产生的这些新对象,仍然被使用,就意味着这些对象不能作为垃圾,这样的对象达到一定的数量,就会导致堆内存被耗尽,即堆内存溢出。

代码示例:

public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

初始的时候,a变量引用"hello",第一次变量,会把hello放入List集合中,接下来做一个字符串的拼接,变成了两个hello,组成了一个新的字符串,再循环的时候,会把两个hello的字符串对象。再追加到list集合,再做一次的拼接。以此类推,不断地把拼接的Hello对象加入list集合中,list的作用范围是从声明开始,一直都是有效范围,所以不能被垃圾回收掉,随着这个字符串就越来越多,它就会把堆空间占满。
在这里插入图片描述

Xmx 设置堆内存大小

控制堆空间的一个最大值,改小之后只循环17次。
在这里插入图片描述
在这里插入图片描述

堆内存诊断

案例一:jps/jmap/jconsole工具使用

代码示例:

public class Demo1_4 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000); //停留时间敲命令
        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
        System.out.println("2...");
        Thread.sleep(20000);
        array = null; //变量数组不再引用
        System.gc();//进行垃圾回收
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

jps 查看当前系统中有哪些java进程,并展示进程号

在这里插入图片描述

jmap 查看进程在堆内存占用情况,只能查询某个时刻

在这里插入图片描述
新创建的对象,都会使用eden Space区,used代表使用内存,capacity代表总容量
执行到1,查看堆内存使用情况:
在这里插入图片描述
执行到2,查看堆内存使用情况:增加了10M,就是创建的byte数组
在这里插入图片描述

执行到3,查看堆内存使用情况:包含一些不用的信息都回收掉了
在这里插入图片描述

jconsole工具

图形界面,多功能检测工具,可以连续监测,还可以查看内存占用,线程情况,类加载数量等信息。

启动代码,控制台输入jconsole
在这里插入图片描述

选择进程,直接连接
在这里插入图片描述
在这里插入图片描述

可以看到堆内存的使用情况:
在这里插入图片描述
在这里插入图片描述

还可以检测线程死锁
在这里插入图片描述

案例二:垃圾回收后,内存占用仍然很高

程序运行起来之后,并不知道程序代码怎么编写的,可以借助一些工具来查看一下。

jps 查看进程id

在这里插入图片描述

jmap - heap 13556 查看堆内存使用情况

在这里插入图片描述

jconsole 执行jc,但是堆内存还是200MB空间

是不是有一些由于我们的一些编程失误,导致了很一些对象始终被引用而无法释放他们的内存呢
在这里插入图片描述

使用 jvisualvm 工具

在这里插入图片描述

与jconsole类似,也可以监测一个内存的占用,执行垃圾回收,检测线程,查看堆各个组成成分。
在这里插入图片描述
堆dump工具,抓取堆的当前快照,可以进一步对里面的一些详细内容进行分析。这是jmap,jconsole 工具所不具备的。
在这里插入图片描述

查找保留前20个内存最大的对象,点进去查询细节:
在这里插入图片描述
可以看到student对象中big属性,是一个byte数组占用空间大约是一兆左右。每个student 对象占用一兆的内存,两百个对象占内存两百多兆。
排查出问题:student对象以及list导致内存占用比较高。而且这个对象是长时间使用的,导致垃圾回收没办法回收他们的内存。
在这里插入图片描述
查看Java源码:
student对象的属性的大小是1兆。循环了两百次,都加到了一个list 中,而这个list 由于一直在main方法里,而main方法一直没结束,所以都是在它的生存范围内,所以一直没能被回收,就导致内存占用居高不下。

/**
 * 演示查看对象个数 堆转储 dump
 */
public class Demo1_13 {

    public static void main(String[] args) throws InterruptedException {
        List<Student> students = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            students.add(new Student());
//            Student student = new Student();
        }
        Thread.sleep(1000000000L);
    }
}
class Student {
    private byte[] big = new byte[1024*1024];
}

在实际的生产环境下分析的手段和排查的方式也是类似。通过堆转储功能(堆dump)把内存快照抓下来。再分析对内存占用最大的那些对象,就可以得出一些有用的结论,再去分析和排查你的原始代码就可以了。

方法区

jvm中对方法区的定义:Chapter 2. The Structure of the Java Virtual Machine

在这里插入图片描述
在这里插入图片描述

方法区是JVM所有线程共享的区域,它存储了与类结构相关的信息:运行时常量池,成员变量,方法数据,成员方法、构造方法的代码。它的大小就决定了系统可以保存多少种类。
  方法区在虚拟机启动时创建,它在逻辑上是堆的组成部分(具体实现上是否为堆的一部分视厂商而定,如永久代,元空间都是其实现)。JVM规范并不强制方法区的位置。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类会出现OOM错误。

在JDK 7以前,习惯上把方法区称为永久代(习惯上),而到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替。这两个最大的区别就是:元空间不在虚拟机设置的内存中,而是使用本地内存。

在这里插入图片描述
在这里插入图片描述

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
在这里插入图片描述

方法区内存溢出

代码示例:

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // 类加载器可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

运行之后并没有出现溢出,因为Java8之后使用元空间,使用的是系统内存,所以设置元空间初始大小为1.8
在这里插入图片描述

-XX:MaxMetaspaceSize=8m 设置原空间初始大小 1.8

在这里插入图片描述
1.8环境,Metaspace代表元空间导致的OOM

-XX:MaxMetaspaceSize=8m 设置元空间初始大小

在这里插入图片描述
1.6环境,PermGen代表是永久代导致的OOM。

XX:MaxPermSize=8m 设置永久代初始大小

在这里插入图片描述

常量池

JVM指令参考:https://blog.csdn.net/A598853607/article/details/125026953

通过一段代码认识常量池:

示例代码:

public class HelloWorld {
    public HelloWorld() {
    }

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

hello world要运行,需要先编译成二进制字节码。
字节码由三部分组成:类的基本信息,类的常量池,类中的一些方法定义(包含虚拟机指令)。

javap -v helloWord 查看二进制反编译后内容

PS D:\java\idea\IdeaProject2\jvm\out\production\jvm\cn\itcast\jvm\t5> javap -v HelloWorld
警告: 二进制文件HelloWorld包含cn.itcast.jvm.t5.HelloWorld
Classfile /D:/java/idea/IdeaProject2/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
  Last modified 2023-2-15; size 567 bytes
  MD5 checksum 8efebdac91aa496515fa1c161184e354
  Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // cn/itcast/jvm/t5/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t5/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               cn/itcast/jvm/t5/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public cn.itcast.jvm.t5.HelloWorld();
    descriptor: ()V
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t5/HelloWorld;

  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 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

类的基本信息:
在这里插入图片描述
常量池:
在这里插入图片描述
方法定义:
在这里插入图片描述
查看虚拟机指令:虚拟机指令后的注释是java程序帮忙加上的。解释器在翻译虚拟机指令的时候,只看到没加注释的这几行指令。
在这里插入图片描述
在这里插入图片描述
常量池就是一张常量表,jvm虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量(字符串和基本数据类型)等信息。
运行时常量池就是存在 *.class 文件中的,当该类被加载到虚拟机以后,它的常量池中的信息会被加载到运行时常量池中(在内存中的位置)。并把符号地址变为内存中的真实地址。

StringTable

在运行时常量池中,有一块逻辑区域是StringTable(串池),用于存放字面量,串池实际上是一个hash表,不能扩容。当jvm指令在执行时,当执行到字面量信息时,会去串池中查找是否有该字面量,如果没有该字面量将其放入到串池中。

示例一:字面量创建字符串

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
    }
}

在这里插入图片描述

常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
当运行到此方法,执行到对应的代码时(jvm执行 ldc #2) 才会把 a 符号变为 “a” 字符串对象,并将对象放入串池(StringTable[]) 中,StringTable是一个哈希表,长度固定,“a”就是哈希表的key。一开始的时候,会根据“a”到串池中找其对象,一开始是没有的,所以就会创建一个并放入串池中。串池为 [“a”]。执行到指令ldc #3时,会和上面一样,生成一个“b”对象并放入串池中,串池变为[“a”, “b”]。同样地,后面会生成“ab”对象并放入串池中。串池变为[“a”, “b”, “ab”]。

小结一下:字面量创建字符串对象是懒惰的,即只有执行到相应代码才会创建相应对象(和一般的类不同)并放入串池中。如果串池中已经有了,就直接使用串池中的对象(让引用变量指向已有的对象)。串池中的对象只会存在一份,也就是只会有一个“a”对象。

普通的java对象在类加载的时候就会生成并放入堆中,而这种方式生成的String不同,只有当执行到新建String的代码时才会生成字符串对象。

astore_0 将引用类型或returnAddress类型值存入局部变量0

示例二:字符串变量拼接

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1+s2;
		System.out.println(s3 == s4);  // false
    }
}

通过虚拟机指令明白:
行号为9的指令是个new,就说明s4的创建方式和s1、s2、s3不同,它是在堆里新建了一个对象,前面根据字面量创建的则是在串池中生成了字符串对象。通过后面的注释明白new了一个StringBuilder对象。接着看17,21,可以发现“s1 + s2”的方式是通过StringBuilder对象调用append方法实现的。最后看24,调用了toString方法生成了新的字符串对象。

所以:s1+s2的执行流程:new StringBuilder().append(“a”).append(“b”).toString()
在这里插入图片描述
StringBuilder.toString(),创建一个新的String对象,存入ab
在这里插入图片描述
说明:当两个字符串变量拼接时,jvm会创建一个StringBuilder对象,利用其append方法实现变量的拼接。最后再通过其toString方法生成一个新的String对象。

最后输出结果,发现s3不等于s4,这说明s3指向串池中的“ab”对象,s4指向堆中的“ab”对象。这是两个不同的对象。

示例三:字符串常量拼接

public class Demo1_22 {

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";
        System.out.println(s3 == s5);//true

    }
}

在这里插入图片描述
行号为6时,串池中没有ab对象,就创建ab对象放入串池,执行到29行时要去常量池中找4号位置的ab符号,发现串池中已存在,就不会创建新的对象,直接引用即可。
为什么会以这种方式创建s5,其实是编译期的优化。y因为编译器发现这是两个常量a,b相加,结果在编译期间就可以确定为ab,不会再变。s4 = s1+s2,因为s1和s2是变量,在运行时被引用的值可能被修改,结果不能确定,所以要在运行时使用StringBuilder动态拼接。

示例三:intern方法

 public static void main(String[] args) {

        String x = "ab";
        String s = new String("a") + new String("b");

        // 堆  new String("a")   new String("b") new String("ab")
        String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

        System.out.println( s2 == x);//true
        System.out.println( s == x );//false
    }

在这里插入图片描述
通过虚拟机指令明白:
第一行代码看行号为0的指令,往串池中添加ab字符,
第二行代码从行号为3的指令new StringBuilder对象,调用他的空参构造器,行号10new String对象,行号14将a字符添加到串池中,实例化String对象,值为a,调用StringBilder的append方法,再new一个String对象,实例化值为b,StringBuilder拼接ab,调用toString方法。

intern方法的作用就是在尝试把堆中对象放入串池中。如果串池中已有,会返回串池中的对象。并且s调用intern方法后依旧指向堆中的对象。如果串池中没有,会在串池中创建一个“ab”对象并返回,并且会让s指向串池中的“ab”对象。
  
在jdk1.6,当一个String调用intern方法时,如果串池中没有,会将堆中的字符串对象复制一份放到串池中,最后返回串池中刚加入的对象。

StringTable特性

常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,可以避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder(jdk1.8)
字符串常量拼接的原理是编译器优化
可以使用intern方法,主动将串池中还没有的字符串对象放入串池

StringTable优化

  • -XX:StringTableSize=1009 调整StringTable桶个数,最小为1009,如果系统内字符串较多,可以适当增大该值
  • 如果有大量字符串且有重复。使用intern方法,将字符串入池减少字符串个数,节约堆内存的使用
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());
                    //address.add(line);
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

未入池之前内存对比:
在这里插入图片描述
在这里插入图片描述
入池之后内存对比:

在这里插入图片描述
在这里插入图片描述

直接内存

直接内存:Direct Memory,不属于Java虚拟机的内存管理,属于操作系统的内存。

  • 常见于 NIO 操作时,用于数据读取时缓冲区内存,
  • 分配回收成本较高,但读写性能高(Java使用系统内存,分配成本高些)
  • 不受 JVM 内存回收管理()

IO/DirectBuffer拷贝文件

/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = "E:\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io使用传统阻塞IO文件拷贝 用时:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 使用缓冲区 用时:479.295165 702.291454 562.56592
    }

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

    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);
    }
}

Java并不具备磁盘读写的能力,要调用操作系统的提供的方法。涉及到CPU要从用户态,切换到内核态。
在这里插入图片描述
切换到内存态后,就可以由CPU的函数读取磁盘文件的内容,读取数据后内核态会在操作系统内存中画出一块缓冲区,称为系统缓存区,磁盘内容会先读入到系统缓冲区中,系统缓冲区Java代码是不能运行的,Java会在堆内存中分配一块Java的缓冲区,对应Java中new byte[_1Mb],Java代码要能读取到流中的数据,必须要从系统缓存区中的数据间接的读入到Java缓冲区,再到用户态,Java调用输出流的写入操作,重复读写操作,将文件赋值到目标位置。
在这里插入图片描述
因为Java堆内存中有块缓冲区,系统内存中由块系统缓存区,数据必然要存两份,造成不必要的数据复制,效率不是很高。

使用ByteBuffer之后,allocateDirect方法会分配一块直接内存,会在操作系统中画出一块缓冲区,Java代码和系统都可以直接访问,磁盘文件读取会读取到直接内存,Java代码也可以直接访问直接内存,减少了一次复制操作。
在这里插入图片描述

直接内存溢出

public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}

在这里插入图片描述

显式的垃圾回收gc对内存占用的影响

public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * -XX:+DisableExplicitGC 禁用掉显式的垃圾回收之后,直接内存不能通过显示代码回收掉的话,只能等到真正的垃圾回收发生时才会被清理,对应的直接内存才会被清理。
     */
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,不仅要回收新生代,还会回收老年代,导致程序暂定的时间较长 Full GC
        System.in.read();
    }
}

在这里插入图片描述
分配完毕。。。
在这里插入图片描述
gc垃圾回收后。。。
在这里插入图片描述

直接内存分配的底层原理:Unsafe

ByteBuffer底层分配和释放直接内存的类型为Unsafe类,jdk内部使用Unsafe类,不建议程序员使用。

public class Demo1_27 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存,返回base代表刚分配的内存地址
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 释放内存,通过内存地址释放
        unsafe.freeMemory(base);
        System.in.read();
    }
    

	//通过反射获取Unsafe对象
    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

分配和回收原理
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
ByteBuffer 的实现类内部,使用了 Cleaner (虚引用类型)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存。

垃圾回收对Java中无用的对象是自动释放的,不需手动调用方法,直接内存必须主动调用freeMemory()方法,才能完成对直接内存的释放。

JVM参数

-XX:+PrintStringTableStatistics 打印字符串表的统计信息,可以精准看到串池中实例个数和占用大小信息
-XX:+PrintGCDetails -verbose:gc 打印垃圾回收信息,次数已经花费时间
 -XX:StringTableSize=1009 调整StringTable桶个数,最小为1009,如果系统内字符串较多,可以适当增大该值
  -XX:+DisableExplicitGC 禁用掉显式的垃圾回收

 在jdk8下设置-XX:MaxMetaspaceSize=8m 设置元空间初始大小
 在jdk6下设置 -XX:MaxPermSize=10m 设置永久代初始大小

-Xmn10M 设置年轻代大小。整个堆大小=年轻代大小 + 年老代大小 + 常量池。
 -Xsx500m 
-Xms500m 初始堆内存大小,设定程序启动时占用内存大小
-Xmx500m  最大堆内存,设定程序运行期间最大可占用的内存大小
-Xss256k  设置单个线程栈大小,一般默认512~1024kb,设置栈帧大小

 -XX:+UseSerialGC

JVM参数参考:https://blog.csdn.net/dsydly/article/details/106303058

在这里插入图片描述

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

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

相关文章

Java---打家劫舍ⅠⅡ

目录 打家劫舍Ⅰ 题目分析 代码一 代码二 打家劫舍Ⅱ 打家劫舍Ⅰ 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被…

设备树实践操作

目录一、使用设备树给DM9000网卡_触摸屏指定中断1、修改方法2、实验方法二、在设备树中时钟的简单使用1、参考文档2、知识讲解三、在设备树中pinctrl的简单使用1、几个概念2、设备树中pinctrl节点3、platform_device, platform_driver匹配4、驱动中想选择、设置某个状态的引脚四…

ESP32设备驱动-BMI160惯性测量传感器驱动

BMI160惯性测量传感器驱动 BMI160 是一种高度集成的低功耗惯性测量单元 (IMU),可提供精确的加速度和角速率(陀螺仪)测量。 BMI160 集成: 16位数字,三轴加速度计16位数字,三轴陀螺仪BMI160特性: 高性能加速度计和陀螺仪(硬件同步) 极低功耗:typ.925A(加速度计和陀螺…

BIM小技巧丨关于如何在Revit明细表中显示门窗面积

在明细表中显示门窗面积(以门明细表为例)在新建一个门明细表后&#xff0c;可以发现在Revit中不能直接使用明细表统计门窗面积。 这时&#xff0c;可以通过使用添加“计算值”的方式来处理&#xff0c;得到如下图所示&#xff0c;两种不同的面积统计结果&#xff1a; 除此之外&…

Android 12 快速适配

Android 12 需要更新适配点并不多&#xff0c;本篇主要介绍最常见的两个需要适配的点&#xff1a;android:exported[1] 和 SplashScreen[2] 。一、android:exported它主要是设置 Activity 是否可由其他应用的组件启动&#xff0c; “true” 则表示可以&#xff0c;而“false”表…

满汉楼练习 马踏棋盘

1. 满汉楼 1.结构图 2. 数据库 pwd CHAR(32) NOT NULL DEFAULT ‘’,# 密码&#xff0c;32位 INSERT INTO employee VALUES(NULL, ‘666’, MD5(‘123456’), ‘老韩’, ‘经理’); MD5(‘123456’)是经过MD5加密过后的32位的字符串&#xff0c;用来保存密码 select * fro…

辉光管时钟学习制作及开源软硬件工程

文章目录前言开源地址辉光管项目介绍辉光管的工作条件硬件部分部分介绍充电电路驱动电路不足之处软件部分总结前言 作为一个电子人&#xff0c;一直想做一个辉光管时钟&#xff0c;算是大学的一个心愿&#xff0c;终于在快要毕业前做了一个&#xff0c;下面把软件和硬件的部分…

Windows事件日志监控

大多数数据泄露属内部人员而为&#xff0c;但各企业在监控内部网络活动方面仍存在不足。无论是大型还是小型企业&#xff0c;监控内部网络活动已成为其主要要求。要保护网络安全以防范泄露和威胁&#xff0c;各企业需要采取积极的措施来保证其网络和数据的安全性。监控事件日志…

NCRE计算机等级考试Python真题(四)

第四套试题1、以下选项中&#xff0c;不属于需求分析阶段的任务是&#xff1a;A.需求规格说明书评审B.确定软件系统的性能需求C.确定软件系统的功能需求D.制定软件集成测试计划正确答案&#xff1a; D2、关于数据流图&#xff08;DFD&#xff09;的描述&#xff0c;以下选项中正…

跨境人都在用的指纹浏览器到底有什么魔力?三分钟带你了解透彻

什么是指纹浏览器&#xff1f;这是东哥近期收到最多的粉丝私信咨询&#xff0c;指纹两个字大家都很熟悉&#xff0c;指纹浏览器就变得陌生起来。之前东哥也跟大家分享过很多次指纹浏览器的用法&#xff0c;鉴于还是很多人不认识这个好用的工具&#xff0c;东哥今天就来详细给大…

【ICCV2022】 CAPAO:一种高效的单阶段人体姿态估计模型

CAPAO&#xff1a;一种高效的单阶段人体姿态估计模型 重新思考关键点表示&#xff1a;将关键点和姿态建模作为多人姿态估计的对象&#xff08;Rethinking Keypoint Representations: Modeling Keypoints and Poses as Objects for Multi-Person Human Pose Estimation&#xf…

k8s学习之路 | Day16 k8s 中的容器初探

文章目录容器镜像镜像名称镜像拉取策略私有仓库的拉取策略容器的环境变量和启动命令容器的环境变量容器的启动命令容器的生命周期钩子postStartpreStop容器的探针startupProbelivenessProbereadinessProbek8s 集群中最小的管理单元就是一个Pod&#xff0c;而Pod里面才是容器&am…

linux下devmem访问物理寄存器MT7621 mdio控制

在同专栏的mdio访问phy的三种方式篇&#xff0c;我们着重介绍了通过ioctrl的方式将mdio总线与网卡绑定进行访问&#xff0c;但是实时上数据接口和管理接口可以独立去控制&#xff0c;很不幸&#xff0c;作者现在必须把mdio与网卡解除绑定并独立操控&#xff0c;因此接下来将详细…

【elementUI】基于elementUI自定义封装分页内容

文章目录前端分页的封装后端进行分页的封装&#xff0c;利用el-pagination网页获取数据进行分页主要有前端分页和后端分页&#xff0c;对于数据量较小的数据&#xff0c;可以使用前端分页进行实现。但是一般的分页都是后端获取分页实现前端分页的封装 思路&#xff1a; 1.假设一…

Mybatis源码学习笔记(五)之Mybatis框架缓存机制原理解析

1 Mybatis框架的缓存模块 MyBatis 内置了一个强大的事务性查询缓存机制&#xff0c;它可以非常方便地配置和定制。Mybatis框架中的缓存分为一级缓存和二级缓存&#xff0c;三级缓存基本都要借助自定义缓存或第三方服务来进行实现。但本质上是一样的&#xff0c;都是借助Cache接…

只会手工测试,裸辞后怎么才能找到工作

我们可以从以下几个方面来具体分析下&#xff0c;想通了&#xff0c;理解透了&#xff0c;才能更好的利用资源提升自己。 一、我会什么&#xff1f; 先说第一个我会什么&#xff1f;第一反应&#xff1a;我只会功能测试&#xff0c;在之前的4年的中我只做了功能测试。内心存在…

如何改变照片的大小kb?照片怎么改到100kb?

在平时的日常工作生活当中&#xff0c;我们都会遇到需要上传照片的情况&#xff0c;但是随着拍摄的照片越来越清晰照片体积也越来越大&#xff0c;很容易遇到图片太大上传不成功的情况&#xff0c;那么这时候应该怎么办呢&#xff1f;今天来给大家分享一款照片压缩器&#xff0…

TCP/IP协议,网络工程部分

这个博客参考了许多up主的视频和网上其他的博主的文章&#xff0c;还有我老师的ppt 这里是目录一、osi七层模型&#xff08;参考模型&#xff09;1.物理层2.数据链路层&#xff08;数据一跳一跳进行传递&#xff09;3.网络层&#xff08;端到端传输&#xff09;4.传输层&#x…

synchronized底层如何实现?什么是锁的升级、降级?

第16讲 | synchronized底层如何实现&#xff1f;什么是锁的升级、降级&#xff1f; 我在上一讲对比和分析了 synchronized 和 ReentrantLock&#xff0c;算是专栏进入并发编程阶段的热身&#xff0c;相信你已经对线程安全&#xff0c;以及如何使用基本的同步机制有了基础&#…

Web Spider案例 网洛者 第一题 JS混淆加密 - 反hook操作 练习(五)

文章目录一、资源推荐二、第一题 JS混淆加密 - 反hook操作2.1 过控制台反调试(debugger)2.2 开始逆向分析三、python具体实现代码四、记录一下&#xff0c;execjs调用混淆JS报错的问题总结提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、资源推荐 …