JVM理解
1、JVM的基本介绍
JVM,即 Java Virtual Machine ,是Java 程序的运行环境(Java 二进制字节码的运行环境)。
JVM的作用:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
1.1、JVM、JRE、JDK三者的比较
JVM、JRE、JDK 的关系如下图所示。
- JDK(Java Development Kit):java开发工具包,在JRE的基础上增加编译工具,如javac
- JRE(Java Runtime Environment):java的运行时环境,在JVM的基础上结合一些基础类库
- JVM:java虚拟机, 可以屏蔽java代码与底层虚拟机之间的关系
1.2、常见的JVM
1.3、JVM的整体架构
2、程序计数器
- 是线程私有的。每个线程都有自己的程序计数器,随着线程创建而创建,随线程销毁而销毁
- 不会存在内存溢出
3、虚拟机栈(线程内存)
3.1、虚拟机栈基本介绍
每个栈由多个栈帧(Frame)组成,对应着该线程内各个方法调用时所占用的内存,即线程内每个方法的调用都会创建一个新的栈帧(Stack Frame)。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
- 不会。栈帧内存 在每次方法调用结束后会自动弹出栈(自动回收),不需要回收(垃圾回收回收堆内存中无用对象,不会回收栈内存)
- 不是。因为服务器中物理内存是固定大小的,单个栈内存大了,可创建的线程数就少了。虽然栈内可进行更多次方法调用,但由于线程数减少,所以并不会提高效率。
- 可以通过 -Xss 参数来设置栈内存大小,JDK1.5+ 中默认是 1M,一般来说使用默认值即可
- 如果方法内的局部变量没有逃离方法的作用范围,那么它是线程安全的
- 如果是局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全
如下:
- package JVM;
- public class Demo01 {
- public static void main(String[] args) {
- StringBuilder sb = new StringBuilder();
- sb.append(4);
- sb.append(5);
- sb.append(6);
- }
- /**
- * 不会有线程安全问题。因为StringBuilder是线程内局部变量,属于线程私有,其他线程无法访问
- */
- public static void m1() {
- StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.append(1);
- stringBuilder.append(2);
- stringBuilder.append(3);
- System.out.println(stringBuilder.toString());
- }
- /**
- * 不是线程安全的。StringBuilder作为参数传入,StringBuilder可能被其他线程共享,不是线程安全
- */
- public static void m2(StringBuilder stringBuilder) {
- stringBuilder.append(1);
- stringBuilder.append(2);
- stringBuilder.append(3);
- System.out.println(stringBuilder.toString());
- }
- /**
- * 不是线程安全的。虽然StringBuilder是作为局部变量,但是返回结果为StringBuilder,可能被其他线程修改
- */
- public static StringBuilder m2() {
- StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.append(1);
- stringBuilder.append(2);
- stringBuilder.append(3);
- return stringBuilder;
- }
- }
3.1.1、栈帧代码演示
代码如下:
- /**
- * 演示栈帧
- */
- public class Demo1_1 {
- public static void main(String[] args) throws InterruptedException {
- method1();
- }
- private static void method1() {
- method2(1, 2);
- }
- private static int method2(int a, int b) {
- int c = a + b;
- return c;
- }
- }
开启 debug 模式,执行 main 主方法,当调试执行到 method2 方法时,可以看到创建了三个栈帧。当方法 main、method1、method2 执行结束后,栈帧依次被销毁。
3.2、栈内存溢出(StackOverflowError)
- 栈帧过多导致栈内存溢出。比如递归调用方法未正确结束递归
- 栈帧过大导致栈内存溢出。
如下分别为栈帧过多和栈帧多大的示例图:
代码示例,如下是演示栈帧过多导致栈内存溢出的情况:
- package cn.itcast.jvm.t1.stack;
- /**
- * 演示栈内存溢出 报错信息:java.lang.StackOverflowError
- * 可以通过设置 JVM 参数来设置栈内存,如:-Xss256k
- */
- public class Demo1_2 {
- private static int count;
- public static void main(String[] args) {
- try {
- method1();
- } catch (Throwable e) {
- e.printStackTrace();
- System.out.println(count);
- }
- }
- private static void method1() {
- count++;
- method1();
- }
- }
执行以上 main 方法,可以看到报错如下:
3.3、线程运行诊断
3.3.1、CPU占用过高
通过跑一段无限循环代码来使系统的 CPU 不断飙升,演示如何通过命令来诊断出导致 CPU 过高的线程。
代码示例:
- package cn.itcast.jvm.t1.stack;
- /**
- * 演示 cpu 占用过高
- */
- public class Demo1_16 {
- public static void main(String[] args) {
- new Thread(null, () -> {
- System.out.println("1...");
- while(true) {
- }
- }, "thread1").start();
- new Thread(null, () -> {
- System.out.println("2...");
- try {
- Thread.sleep(1000000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }, "thread2").start();
- new Thread(null, () -> {
- System.out.println("3...");
- try {
- Thread.sleep(1000000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }, "thread3").start();
- }
- }
代码编译后,传入 Linux 系统中,通过 java cn.itcast.jvm.t1.stack.Demo1_16 命令来运行该段程序。
然后通过 TOP 命令可以定位哪个进程对cpu的占用过高,如下:
通过 ps H -eo pid,tid,%cpu | grep 进程id 命令进一步定位是哪个线程引起的cpu占用过高,如下:
(注意,左边是进程id,右边是线程id)
如上找到 CPU 占用过高的线程,并且可以定位到具体的代码类名和行数。
3.3.2、程序阻塞运行很久没有结果
如下,通过一段代码演示程序发生线程死锁。
代码如下:
- package cn.itcast.jvm.t1.stack;
- /**
- * 演示线程死锁
- */
- class A{};
- class B{};
- public class Demo1_3 {
- 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("show a and b");
- }
- }
- }).start();
- Thread.sleep(1000);
- new Thread(()->{
- synchronized (b) {
- synchronized (a) {
- System.out.println("show a and b 222");
- }
- }
- }).start();
- }
- }
将该代码放置到 Linux 环境上执行,可以看到很久都没有输出结果。
当我们通过 jstack 命令来查看该进程的线程时,可以发现已经发生了死锁。
4、本地方法栈
在 java 虚拟机调用一些本地方法时需要给本地方法提供的内存空间。
- 本地方法:由于java有限制,不可以直接与操作系统底层交互,所以需要一些用c/c++编写的本地方法与操作系统底层的API交互,java可以间接的通过本地方法来调用底层功能。本地方法是由其它语言编写的,编译成和处理器相关的机器代码。本地方法保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的。
举例:Object的clone()、hashCode()、notify()、notifyAll()、wait()等,一个Native Method就是一个java调用非java代码的接口。
5、堆内存(Heap,线程共享)
5.1、堆内存的基本介绍(新生代、老年代、永久代)
特点:
- 它是线程共享的,堆中对象都需要考虑线程安全的问题。堆跟根程序计数器和虚拟机栈不同的是,后两者都是线程私有的,而堆是线程同享的
- 有垃圾回收机制。当一个对象不再被使用时,该对象就会被垃圾回收机制回收,即该对象内存会被垃圾回收掉。
堆内存区域介绍:
在jvm的堆内存中有三个区域:
- 年轻代:用于存放新产生的对象。
- 老年代:用于存放被长期引用的对象。
- 持久带(或元空间):用于存放Class,method元信息(1.8之后改为元空间)。
详细介绍如下:
年轻代:年轻代中包含两个区:Eden 和survivor,并且用于存储新产生的对象,其中有两个survivor区。
老年代:年轻代在垃圾回收多次都没有被GC回收的时候就会被放到老年代,以及一些大的对象(比如缓存,这里的缓存是弱引用),这些大对象可以不进入年轻代就直接进入老年代
持久代:持久代用来存储class,method元信息,大小配置和项目规模,类和方法的数量有关。
元空间:JDK1.8之后,取消perm永久代,转而用元空间代替。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于元空间并不在虚拟机中,而是使用本地内存,并且可以动态扩容。
为什么分代?
因为不同对象的生命周期是不一样的。80%-98%的对象都是“朝生夕死”,生命周期很短,大部分新对象都在年轻代,可以很高效地进行回收,不用遍历所有对象。而老年代对象生命周期一般很长,每次可能只回收一小部分内存,回收效率很低。
年轻代和老年代的内存回收算法完全不同,因为年轻代存活的对象很少,标记清楚再压缩的效率很低,所以采用复制算法将存活对象移到survivor区,更高效。而老年代则相反,存活对象的变动很少,所以采用标记清楚压缩算法更合适。
5.2、堆内存溢出
堆内存溢出模拟代码:
- package cn.itcast.jvm.t1.heap;
- import java.util.ArrayList;
- import java.util.List;
- /**
- * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
- * 可以通过配置JVM参数:-Xmx8m 来设置最大堆内存
- */
- public class Demo1_5 {
- public static void main(String[] args) {
- int i = 0;
- try {
- List<String> list = new ArrayList<>();
- String a = "hello";
- while (true) {
- list.add(a); // hello, hellohello, hellohellohellohello ...
- a = a + a; // hellohellohellohello
- i++;
- }
- } catch (Throwable e) {
- e.printStackTrace();
- System.out.println(i);
- }
- }
- }
6、方法区(Method Area)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。方法区存储类的结构的相关信息,如运行时常量池、成员变量、方法数据、成员方法和构造器的代码等。
方法区在虚拟机启动时创建,其逻辑上是堆的一个组成部分,但在实现时不同的JVM厂商可能会有不同的实现。方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
组成如下:以Oracle的HotSpot为例
- jdk1.6:永久代(PermGen space),占用JVM内存空间
- jdk1.8:元空间(Metaspace),移出JVM内存(除StringTable),放入操作系统内存
6.1、方法区内存溢出
通过不断创建类来演示产生方法区内存溢出,如下:
- package cn.itcast.jvm.t1.metaspace;
- import jdk.internal.org.objectweb.asm.ClassWriter;
- import jdk.internal.org.objectweb.asm.Opcodes;
- /**
- * 元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
- * 设置元空间大小:-XX:MaxMetaspaceSize=8m
- * 永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
- * 设置永久代内存大小:-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 < 10000; 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); // Class 对象
- }
- } finally {
- System.out.println(j);
- }
- }
- }
当使用 jdk1.8 及之后的版本时,内存溢出报错提示:java.lang.OutOfMemoryError: Metaspace。当使用 jdk1.8 之前的版本时,内存溢出报错提示:java.lang.OutOfMemoryError: PermGen space
(默认的元空间内存大小为操作系统的内存大小,可能没那么容易产生内存溢出,可以通过设置 jvm 参数限制元空间内存大小来演示内存溢出现象)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
2020-08-11 数据库的事务