JVM(一)内存结构
1. 程序计数器
- 作用:记住下一条JVM指令的执行地址。
- 特点: 线程私有;不会存在内存溢出
- 存在于CPU寄存器中Program Counter Register
2. Java虚拟机栈
局部变量表,操作数栈,方法返回地址、动态连接(从运行时常量池中连接)
2.1 Java虚拟机定义
- 每个线程运行时所需要的内存,称为虚拟机栈;
- 每个栈由多个栈帧,对应着每次方法调用时候占用的内存;
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法。
问题辨析:
-
垃圾回收是否涉及栈内存?
不涉及,方法调用完会自动弹出,回收内存。存储的局部变量、方法参数会自动释放。
-
栈内存分配越大越好吗?
不是,栈内存越大,线程数会受到限制,因为内存有限,栈内存是线程独享的。 -
方法的局部变量是否线程安全?
变量是否是线程安全的,取决于这个变量被多线程共享时,每次运行结果和单线程运行的结果是否是一样的。
a. 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
b. 如果局部变量引用了对象,且逃离了方法的作用范围,那么就需要考虑线程安全(基本数据类型不会有这个问题)
2.2 栈内存溢出
- 栈帧过多会导致栈内存溢出;(递归时候)
- 栈帧过大会导致栈内存溢出;
2.3 线程运行诊断
案例:cpu占用过多定位
Linux下用top定位哪个进程对cpu的占用过高
ps H -eo pid,tid,%cpu | grep 进程id
(用ps命令进一步定位是哪个线程引起的cpu占用过高)
jstack 进程id
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号。
案例:程序执行很长时间没有结果
- 下面程序发生了死锁。
两个或多个并发进程中,每个进程占有某种资源,同时又在等待其他进程释放它或它持有的资源,才能向前推进状态,这一组进程产生了死锁。
public class Demo {
static A a = new A();
static B b = new B();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (a) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
Thread.sleep(1000);
new Thread(()->{
synchronized (b) {
synchronized (a) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
}
}
3. 本地方法栈
本地方法栈发挥的作用和虚拟机栈类似,用于native修饰的方法。(一般由C语言编写native方法)
4. 堆
4.1堆的定义
通过 new 关键字,创建对象都会使用堆内存。
特点:
• 它是线程共享的,堆中对象都需要考虑线程安全问题
• 有垃圾回收机制
4.2 堆内存溢出
List集合中一直添加数据,会出现堆内存溢出
4.3 堆内存诊断#
- jps工具
查看当前系统中有哪些java进程 - jmap工具
查看堆内存占用情况 jmap - jconsole工具
图形界面,多功能的监测工具,可以连续监测 - jvisualvm(推荐使用)
案例 垃圾回收后,内存占用仍然很高
使用 jvisualvm 命令进行诊断
使用 堆Dunp 进行对当前线程内存进行转储快照,进而分析为什么垃圾回收后内存还很高
5. 方法区
方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
5.1 定义
5.2 组成
jdk 1.6 对方法区的实现称为永久代
jdk 1.8 对方法区的实现称为元空间
5.3 运行时常量池
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.4 方法区溢出
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 < 20000; i++, j++) {
//ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//版本号,pubblic,类名,包名,父类
cw.visit(Opcodes.V1_6, 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);
}
}
}
演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m
演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m
场景:框架会产生很多运行时的类,容易导致内存溢出
• spring
• mybatis
都用到cglib
5.5 StringTable(串池)
StringTable 是运行时常量池中的一个东西
// 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 == s5);
}
}
//结果
s4 不等于 s3 //s3是串池中的,s4是通过new对象生成的,其值存在堆中
s3 等于 s5 //
对于 单独的赋值,是对数据的直接到StringTable中取找,如果没有,则创建,有则直接取
对于 s4 = s1+s2 则是使用StringBuilder(),方法进行拼接,结果是一个新的对象,该对象存在堆上面
5.6 StringTable特性
• 常量池中的字符串仅是符号,第一次用到时才变为对象
• hashtable 结构,不能扩容
• 利用串池的机制,来避免重复创建字符串对象
• 字符串变量拼接的原理是 StringBuilder (1.8)
• 字符串常量拼接的原理是编译期优化
• 可以使用itern方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6 将这个字符对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
5.7 StringTable位置
1.7 的时候StringTable 是在堆空间
1.6 时StringTable是在永久代中 ,永久代是Full GC (老年代空间不足才会触发)才会触发,导致StringTable的回收效率不高,所以在1.7 以后把StringTable 转移到堆中(只需要miner GC 就能触发垃圾回收)
5.8 StringTable 垃圾回收
/**
* 演示 StringTable 垃圾回收
* -XX:MaxPermSize=10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo1_7 {
// 1754
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 500000; j++) { // j=10, j=1000000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
5.8 StringTable 性能调优
• 调整 -XX:StringTableSize=桶个数
• 考虑将字符串对象是否入池
如果字符常量比较多时,可以把桶的个数(StringTableSize)调大,让有更多的hash,减少hash冲突
6. 直接内存
6.1 定义
• 常见于NIO操作时,用于数据缓冲区
• 分配回收成本较高,但读写性能高
• 不受JVM内存回收管理
传统IO
ByteBuffer:
/**
* 演示 ByteBuffer 作用
*/
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
public class ByteBufferTest {
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 用时: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);
}
}
6.2 分配和回收原理
• 使用了Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
• ByteBuffer 的实现类内部,使用了 Cleaner(虚引用) 来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMenory 来释放直接内存