JVM内存结构

JVM内存结构

JVM(Java Virtual Machine):java 程序的运行环境(java 二进制字节码的运行环境)

优点:

  • 一次编写,到处运行:二进制字节码,屏蔽了Java代码和底层操作系统的差异
  • 自动内存管理:垃圾回收功能
  • 数组下标越界检查:越界抛出异常,防止占用其他数据的地址

JVM、JRE和JDK:

程序运行组成部分:

方法区:存放类

:存放类的实例

虚拟机栈程序计数器本地方法栈:对象调用方法时使用

一、程序计数器

程序计数器(Program Counter Register):存储下一条jvm指令的执行地址

特点:

  • 程序计数器是线程私有的
  • 不会存在内存溢出

程序运行:

  • Java源代码经过编译,成二进制字节码,即jvm指令
  • 程序计数器(寄存器):存储下一条要执行的jvm指令的执行地址
  • 解释器:从程序计数器中取出jvm指令,转换成机器码,交由CPU执行

程序计数器由寄存器实现,可以快速读取命令

二、 虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks):是每个线程运行时的内存空间

  • 栈帧每个栈由多个栈帧组成,栈帧是每个方法运行时需要的内存。存储了方法运行时的参数、局部变量、返回地址
  • 活动栈帧:每个线程只能有一个活动栈帧,对应着栈顶部正在执行的方法

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

​ 不会。栈内存是程序调用方法运时产生的栈帧内存,栈帧内存在每一次方法调用后都会弹出栈,即被自动回收,所以不需要垃圾回收栈内存。

栈内存分配越大越好?

​ 不是。物理内存有限,栈内存越大,会影响线程的数量,会是线程数量变少。栈内存增大不会使运行效率变高,而仅会使递归调用次数增加。

​ 默认Linux栈内存是1024kb,可通过-Xss size命令设置栈内存大小

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

​ 当多线程执行一个方法时:

  • 方法内不共享的局部变量:是线程安全的,因为每个线程都有独自的栈空间,栈为方法的调用分配栈帧,每个线程在各自栈帧内执行方法,局部变量没有逃离方法的作用范围,线程安全

  • 方法内共享的变量(static):该变量逃离了方法的作用范围,所以线程不安全

栈内存溢出:

  • 栈帧过多:如递归调用时,递归调用自己,不断产生栈帧,则造成栈内存溢出。

  • 栈帧过大

错误:java.lang.StackOverflowerError

设置栈内存:-Xss256k

对象转换为Json栈溢出:

两个互相包括的对象转换为Json时,会造成栈溢出

ObjectMapper.writeValueAsString(a)

解决方法: 对任意一个对象的成员变量使用@JsonIgnore,在转换为Json时忽略这个成员变量

线程诊断——Cpu占用过高:

  • 用top:定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id:(用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id: 可以根据线程****id 找到有问题的线程,进一步定位到问题代码的源码行号

线程诊断——很长时间没有运行结果:

​ 诊断是否存在死锁

三、 本地方法栈

本地方法栈:本地方法调用时使用的栈

本地方法:Java无法调用操作系统底层的接口,可以调用本地方法如C++实现的方法,调用操作系统底层api,实现底层方法的调用

四、 Heap堆

通过new创建的对象都会使用堆内存

特点:

  • 线程共享的。堆中的对象都要考虑线程安全问题

  • 有垃圾回收机制。当对象不被引用后回收其内存

堆溢出:

  • 异常:Java.lang.OutOfMemoryError
  • 修改堆空间大小:-Xmx8m/-XX:MaxPermSize=10m
  • 垃圾回收:System.gc();

堆内存诊断:命令行工具

  • jps:查看当前系统中有哪些 java 进程
  • jmap - heap 进程id:查看当前时刻堆内存占用情况
  • jconsole:可以连续监测堆内存情况
  • jvisualvm: jvm可视化工具, 使用堆dump功能查看堆占用空间大的对象

五、 方法区

JVM的方法区实现:

方法区内存溢出:加载的类过多

  • 1.8 以前会导致永久代内存溢出
演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m
  • 1.8 之后会导致元空间内存溢出
演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m

溢出场景:

​ Sping、Mybatis加载类时

运行时常量池:

  • 常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池:常量池存储在 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
  • .class文件主要包括:类的基本信息、常量池和类的方法定义,包含虚拟机指令

idea编译:ctrl+shift+F9

反编译:javac -p Hello.class

StringTable:串池

  • 编译时:常量池的字符串仅是符号,第一次用到时才变为对象
  • 运行时:利用串池机制,避免创建重复对象
  • 字符串变量拼接的原理:StringBuilder+new String
  • 字符串常量拼接的原理:编译器优化
  • 字符串的intern方法:主动将串池中还没有的字符串对象放入串池
  • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
String s=new String("a") + new String("b");  //String s=new String("ab");
String s1 = s.intern();

System.out.println(s1 == "ab");   //√
System.out.println(s =="ab");   //√        //×
  • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回

StableTable位置:

​ 1.7以后,StableTable是在堆中,方法区在内存中。

StableTable垃圾回收:

  • StableTable的数据结构是HashTable,数组(桶)+链表

  • 默认桶数:60013,当字符串常量值多于数组大小时,会进行垃圾回收-

  • 垃圾回收将没有引用的字符串清理

StableTabe性能调优:

HashTable桶个数越少,Hash值越密集,链表长度越大,查找并添加的速度越慢

  • 设置桶大小:-XX:StringTableSize=1009 -XX:+PringStringTableStatics

  • 将字符串对象入池:s.intern();

案例1

​ “字符串”:会延迟实例化,编译时先将字符串保存在常量池中,运行时用到再实例化

String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s3 == s5); //√

常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象

ldc #1 会把 a 符号变为 "a" 字符串对象
ldc #2 会把 b 符号变为 "b" 字符串对象
ldc #3 会把 ab 符号变为 "ab" 字符串对象
ldc #5 时串池StringTable中已存在ab,所以s3和s5是同一对象。

案例2

new关键字是在堆中创建的对象,地址不同,不相等

String s4 = s1 + s2; // StringBuilder().append("a").append("b").toString() new String("ab")
String s6=new String("ab");
System.out.println(s4 == s6); //×
System.out.println(s3 == s6); //×

六、直接内存

操作系统的内存

  • 常见的NIO操作时,用于数据缓冲区
  • 分配回收成本高,但读写性能高
  • 不接受JVM内存回收管理

数据缓冲读写:

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

不带数据缓冲的读写:

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

直接内存溢出:

​ Java.lang.OutOfMemoryError: Direct buffer memory

直接内存:释放

System.gc释放java内存,自动调用unsage.freeMemory(对象)释放直接内存

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

直接回收:禁用显示回收对直接内存的影响

  • -XX:+DisableExplicitGC 禁用显示回收(system.gc):防止显示回收对性能的影响
  • +unsage.freeMemory(对象):回收直接内存
posted @ 2023-08-23 17:25  言思宁  阅读(28)  评论(0编辑  收藏  举报