一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
JVM
JVM 是什么?
Java虚拟机,英文全称是Java Virtual Machine。JVM是可运行Java字节码文件的虚拟计算机。Oracle负责制定JVM规范,并会随着JDK的发布提供一个官方的JVM实现,但实际上不少商业公司也会提供商业级的JVM实现。常见的JVM实现是HotSpot。
JVM、JRE、JDK 三者之间的关系?
JVM是Java虚拟机,JRE是Java运行时环境,JDK是Java开发工具包JDK=JRE+Java开发工具(比如编译工具javac.exe和打包工具jar.exe等)JRE=JVM+Java常用类库JVM=Java虚拟机
┌─ ┌──────────────────────────────────┐
│ │ Compiler, debugger, etc. │
│ └──────────────────────────────────┘
JDK ┌─ ┌──────────────────────────────────┐
│ │ │ │
│ JRE │ JVM + Runtime Library │
│ │ │ │
└─ └─ └──────────────────────────────────┘
┌───────┐┌───────┐┌───────┐┌───────┐
│Windows││ Linux ││ macOS ││others │
└───────┘└───────┘└───────┘└───────┘
JVM 内存结构
程序计数器
程序计数器(寄存器)Program Counter Register
- 作用:用于记录下一条
JVM指令的执行地址 - 特点:
- 每个线程都有属于自己的程序计数器,它是线程私有的,生命周期与线程相同,随着线程创建而创建,随着线程销毁而销毁
- 占用较小的内存空间,不存在内存溢出
OutOfMemoryError的问题
虚拟机栈
虚拟机栈 JVM Stacks
- 每个线程运行时分配的内存空间,称为虚拟机栈
- 栈由多个栈帧
stack frame组成,对应每次方法调用时所占用的内存,用于保存参数、局部变量、返回地址等内容。 - 每个线程只能由一个活动栈帧,对应当前正在执行的那个方法
团子注:有点像
JavaScript中的执行上下文栈与执行上下文。
垃圾回收是否设计栈内存?
- 垃圾回收不会涉及栈内存
栈内存是否越大越好?
-Xss size是虚拟机参数,用于设置线程栈大小stack size,单位是字节,下面用不同形式设置了线程栈大小为1024KB。-Xss1m-Xss1024k-Xss1048576
- 栈内存的分配并非越大越好。线程栈大小越大,则支持更多层数的递归调用,但是由于物理内存的限制,会减少总线程数量。一般保持系统默认即可。
方法内的局部变量是否线程安全?
- 如果一个变量是线程私有的,则称之为线程安全的;如果一个变量是线程共享的,则称之为线程不安全的。
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
- 如果局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全问题。
public class ThreadSafeDemo {
// sb 线程安全
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
// sb 是方法参数,不是线程安全的
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
// sb 作为返回值,不是线程安全的
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
栈内存溢出
- 栈内存溢出对应的异常是
java.lang.StackOverflowError - 有两种可能会导致占内存溢出:
- 栈帧过多导致栈内存溢出,比如无限递归,或者调用第三方库对两个相互引用的对象进行
JSON序列化 - 栈帧过大导致栈内存溢出
- 栈帧过多导致栈内存溢出,比如无限递归,或者调用第三方库对两个相互引用的对象进行
线程运行诊断
- 问题:
CPU占用过多
- 使用
top命令定位进程编号,比如说是32655 - 使用
ps H -eo pid,tid,%cpu | grep 32655,第二列tid可以定位到线程编号 jstack 32655可以将进程中的所有Java线程列出来,需要把上一步查到的10进制线程编号换算成16进制的线程编号,进而可以定位到是哪个类的哪一行。
- 问题:程序运行很长时间没有结果
- 使用
jstack pid可以查看进程中的所有线程 - 查看最后是否有
Java-level deadlock,也就是死锁现象
原生方法栈
原生方法栈:Java 虚拟机在调用原生方法时提供的内存空间。
团子注:也有人称其为本地方法栈,但个人认为把
native翻译成“原生”更好一点。
原生方法不是由 Java 编写的,而是使用 C 或者 C++ 编写的方法,用于和操作系统底层 API 打交道。比如 Object 的 clone() 方法,方法声明为 protected native Object clone() throws CloneNotSupportedExceptoin,它没有方法实现,使用 native 关键字标识为一个原生方法。
堆
堆 Heap:使用 new 关键字创建的对象会使用堆内存。
特点:
- 它是线程共享的,堆中的对象都要考虑线程安全的问题。
- 有垃圾回收机制。
堆内存溢出
- 堆内存溢出对应的异常是
java.lang.OutOfMemoryError: Java heap space。 - 设置堆内存空间大小的
JVM参数是-Xmx,比如设置堆空间大小为8mb可以这样子-Xmx8m。 - 把堆内存设置的比较小,可以尽早地暴露出由堆内存溢出引起的问题。
堆内存诊断
-
jps工具:查看当前系统中有哪些Java进程 -
jmap工具:查看堆内存使用情况
jmap -heap <pid>
-
jconsole工具:图形界面、多功能监测工具,可以连续监测 -
jvisualvm工具:可视化工具,其中堆转储(堆Dump)可以截取堆内存快照,然后查找占用内存最大的20个对象,最后排查原始代码。
方法区
- 方法区类似传统语言中编译后的代码的存储区,用于存储每个类的结构。
JVM启动时会创建方法区,逻辑上讲方法区属于堆内存的一部分,但是不同JVM有不同的实现。HotSpot JVM在JDK 1.8之前,方法区使用永久代实现,占用堆内存;JDK 1.8及以后,方法区使用元空间实现,占用本地内存,也就是操作系统的内存,默认没有设置上限。- 如果方法区出现内存溢出,
JVM会抛出OutOfMemoryError。
内存溢出
- 永久代内存溢出:对应的异常是
java.lang.OutOfMemoryError: PermGen space,比如可以通过JVM选项-XX:MaxPermSize=8m来设置永久代大小为8mb。 - 元空间内存溢出:对应的异常是
java.lang.OutOfMemoryError:Metaspace,比如可以通过JVM选项-XX:MaxMetaspaceSize=8m来设置元空间大小为8mb。
下面的代码用来演示元空间内存溢出,如果想演示永久代内存溢出,只需要把 Opcodes.V1_8 改成 Opcodes.V1_6 即可。
/**
* 演示元空间内存溢出
* java.lang.OutOfMemoryError: Metaspace
* -XX:MaxPermSize=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 < 20000; 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);
}
} finally {
System.out.println(j);
}
}
}
在使用一些第三方库的时候,比如 Spring、MyBatis,它们会动态生成类的字节码,完成动态类加载。这些框架都用到了 ClassWriter,它继承自 ClassVisitor,属于 asm 项目。
运行时常量池
把下面的类编译成二进制字节码文件(.class 文件),其中包含如下信息:
- 类的基本信息
- 常量池
- 类方法定义,其中包含
JVM指令
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
我们很难直接阅读 .class 文件,可以借助 JDK 提供的工具反编译字节码:
javap -v HelloWorld.class-v表示显示详细信息
PS C:\Users\patrick\Desktop> javac HelloWorld.java
PS C:\Users\patrick\Desktop> javap -v HelloWorld.class
Classfile /C:/Users/patrick/Desktop/HelloWorld.class
Last modified 2022-4-9; size 425 bytes
MD5 checksum 92cb0edfd970bc2b3a2ec2be5a24945f
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 61
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // hello world
#14 = Utf8 hello world
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // HelloWorld
#22 = Utf8 HelloWorld
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 SourceFile
#28 = Utf8 HelloWorld.java
{
public HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
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 #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String hello world
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "HelloWorld.java"
常量池:常量池就是一张表,虚拟机指令根据该表查找要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池:常量池位于 .class 文件中,当该类被加载,它的常量池信息就会被放入运行时常量池,并把里面的符号地址(#数字)变成真实内存地址。