写点什么

JVM 学习之 内存结构

作者:JAVA活菩萨
  • 2022 年 8 月 06 日
  • 本文字数:3861 字

    阅读完需:约 13 分钟

JVM学习之 内存结构

目录


一、引言 1.什么是 JVM?定义:Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)好处:一次编写,到处运行自动内存管理, 垃圾回收功能数组下标越界检查多态比较 jvm、jre、jdk


2.学习 JVM 有什么用理解底层的实现原理中高级程序员的必备技能 3.常见的 JVM


4.学习路线


二、内存结构


  1. 程序计数器 1.1 定义 Program Counter Register 程序计数器(寄存器)


在物理上:位于寄存器


作用:是记住下一条 jvm 指令的执行地址


特点:


是线程私有的不会存在内存溢出 1.2 作用 0: getstatic #20 // PrintStream out = System.out;3: astore_1 // --4: aload_1 // out.println(1);5: iconst_1 // --6: invokevirtual #26 // --9: aload_1 // out.println(2);10: iconst_2 // --11: invokevirtual #26 // --14: aload_1 // out.println(3);15: iconst_3 // --16: invokevirtual #26 // --19: aload_1 // out.println(4);20: iconst_4 // --21: invokevirtual #26 // --24: aload_1 // out.println(5);25: iconst_5 // --26: invokevirtual #26 // --29: return 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。2. 虚拟机栈


2.1 定义 Java Virtual Machine Stacks (Java 虚拟机栈)


每个线程运行时所需要的内存,称为虚拟机栈每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法问题辨析垃圾回收是否涉及栈内存?


栈内存并不涉及垃圾回收,栈内存的产生就是方法一次一次调用产生的栈帧内存,而栈帧内存在每次方法被调用后都会被弹出栈,自动就被回收掉,不需要垃圾回收。来管理


栈内存分配越大越好吗?


不是,在线程不多的情况下,栈内存分配大在递归时能提高运行速度,但他会影响线程的数目,从而影响到整个系统的运行速度


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


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


如果是共享的需要考虑线程安全,如果是私有的不用考虑线程安全


2.2 栈内存溢出栈帧过多导致栈内存溢出 ---->一般递归的时候容易出现栈帧内存过大导致栈内存溢出 --->不易出现 StackOverflowError 栈内存溢出异常 @JsonIgnore2.3 线程运行诊断案例 1:cpu 占用过多


定位


用 top 定位哪个进程对 cpu 的占用过高 ps H -eo pid,tid,%cpu | grep 进程 id (用 ps 命令进一步定位是哪个线程引起的 cpu 占用过高)jstack 进程 id 可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号 3. 本地方法栈一些带有 native 关键字的方法就是需要 JAVA 去调用本地的 C 或者 C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。


为本地的方法提供一个运行的空间



4.1 定义 Heap 堆


通过 new 关键字,创建对象都会使用堆内存特点它是线程共享的,堆中对象都需要考虑线程安全的问题有垃圾回收机制 4.2 堆内存溢出 jps 工具查看当前系统中有哪些 java 进程 jmap 工具查看堆内存占用情况 jmap - heap 进程 idjconsole 工具图形界面的,多功能的监测工具,可以连续监测 jvisualvm 工具


  1. 方法区 5.1 方法区 Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区。方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用 的特殊方法。


方法区是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾收集或压缩它。本规范不要求方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小,也可以根据计算需要扩大,如果不需要更大的方法区域,可以缩小。方法区的内存不需要是连续的。


Java 虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,以及在方法区域大小可变的情况下,对最大和最小方法区域大小的控制。


以下异常情况与方法区相关:


如果方法区域中的内存无法满足分配请求,Java 虚拟机将抛出一个 OutOfMemoryError.


JVM 规范-方法区定义 5.2 组成


5.3 方法区内存溢出 1.8 以前会导致永久代内存溢出


演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space


-XX:MaxPermSize=8m


1.8 之后会导致元空间内存溢出


演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace


-XX:MaxMetaspaceSize=8m


场景:


springmybatis5.4 运行时常量池// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)public class Test {public static void main(String[] args) {System.out.println("hello world");}}然后使用 javap -v Test.class 命令反编译查看结果:


每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。


常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量 池,并把里面的符号地址变为真实地址 5.5 StringTable 面试题:


String s1 = "a";String s2 = "b";String s3 = "a" + "b"; // abString s4 = s1 + s2; // new String("ab")String s5 = "ab";String s6 = s4.intern();


// 问 System.out.println(s3 == s4); // falseSystem.out.println(s3 == s5); // trueSystem.out.println(s3 == s6); // true


    String x2 = new String("c") + new String("d"); // new String("cd")    x2.intern();    String x1 = "cd";
复制代码


// 问,如果调换了【最后两行代码】的位置呢,如果是 jdk1.6 呢 System.out.println(x1 == x2);// jdk1.6:// String x1 = "cd"; x2.intern();// x2.intern(); false String x1 = "cd"; ture


    // jdk1.8:    // String x1 = "cd";            x2.intern();    // x2.intern();  false          String x1 = "cd"; ture
复制代码


练习:


// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容 public class Demo1_22 {// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象// ldc #2 会把 a 符号变为 "a" 字符串对象// ldc #3 会把 b 符号变为 "b" 字符串对象// ldc #4 会把 ab 符号变为 "ab" 字符串对象


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";  // javac 在编译期间的优化,结果已经在编译期确定为ab    System.out.println(s3 == s4);//s3是在串池中的,而s4则是在堆中,所有不相等
System.out.println(s3 == s5);// true}
复制代码


}使用 javap -v Demo1_22.class 命令


5.6 StringTable 的特性常量池中的字符串仅是符号,第一次用到时才变为对象利用串池的机制,来避免重复创建字符串对象字符串变量拼接的原理是 StringBuilder (1.8)字符串常量拼接的原理是编译期优化可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串 池中的对象的引用返回 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回 5.7 StringTable 位置 jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。


5.8 StringTable 垃圾回收-Xmx10m 指定堆内存大小


-XX:+PrintStringTableStatistics 打印字符串常量池信息


-XX:+PrintGCDetails


-verbose:gc 打印 gc 的次数,耗费时间等信息


演示 StingTable 垃圾回收:


public static void main(String[] args) throws InterruptedException {int i = 0;try {for (int j = 0; j < 100000; j++) { // j=100, j=10000String.valueOf(j).intern();i++;}} catch (Throwable e) {e.printStackTrace();} finally {System.out.println(i);}


}
复制代码


5.9 StringTable 性能调优调整 -XX:StringTableSize=桶个数


考虑将字符串对象是否入池


  1. 直接内存 6.1 定义 Direct Memory


常见于 NIO 操作时,用于数据缓冲区分配回收成本较高,但读写性能高不受 JVM 内存回收管理文件读写过程(IO):


因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。


使用了 DirectBuffer 文件读取流程:


直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。


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

用户头像

JAVA活菩萨

关注

还未添加个人签名 2022.07.25 加入

还未添加个人简介

评论

发布
暂无评论
JVM学习之 内存结构_Java_JAVA活菩萨_InfoQ写作社区