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(对象):回收直接内存