1. 什么是 JVM?
1.1 定义
JVM,即Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)
1.2 益处
- 一次编写,到处运行的基石
- 主动内存治理,垃圾回收性能
- 数据下标越界查看
- 多态
1.3 JVM、JRE、JDK 比拟
1.4 常见的 JVM
JVM 只是套标准,基于这套标准的实现常见如下几种:
2. 内存构造
2.1 整体架构
JVM 内存构造次要蕴含:程序计数器、虚拟机栈、本地办法栈、堆、办法区,其中前三者是线程公有的,后两者是线程共享的。整体架构如下图:
2.2 程序计数器
2.2.1 作用
用于保留 JVM 中下一条要执行的指令的地址。
2.2.2 特点
线程公有
- CPU会为每个线程调配工夫片,当以后线程的工夫片应用完当前,CPU就会去执行另一个线程的代码。程序计数器是每个线程所公有的,当另一个线程的工夫片用完,又返回来执行以后线程的代码时,通过程序计数器能够晓得应该执行哪一条指令
不会存在内存溢出。
- 因为永远只存储一个指令地址,JVM标准中惟一一个不存在OutOfMemoryError的区域
2.3 虚拟机栈
2.3.1 定义
每个线程运行须要的内存空间,称为虚拟机栈。每个栈由多个栈帧组成,对应着每次调用办法时所占用的内存。每个线程只能有一个流动栈帧,对应着以后正在执行的办法。
应用 IDE 调试演示,从控制台能够看到办法进入虚拟机栈后的样子,如下:
2.3.2 特点
- 线程公有的
- 存在栈内存溢出
2.3.3 问题辨析
垃圾回收是否波及栈内存?
- 不须要。因为虚拟机栈中是由一个个栈帧组成的,在办法执行结束后,对应的栈帧就会被弹出栈,内存会主动开释。所以无需通过垃圾回收机制去回收栈内存
栈内存的调配越大越好吗?
- 不是。因为物理内存是肯定的,栈内存越大,能够反对更多的递归调用,然而可执行的线程数就会越少
办法内的局部变量是否是线程平安的?
- 如果办法内局部变量没有逃离办法的作用范畴,则是线程平安的
- 如果局部变量援用了对象,并逃离了办法的作用范畴,则须要思考线程平安问题
- 代码示例:
//是线程平安的,因为局部变量sb只在m1()办法内应用 public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } //不是线程平安的,因为局部变量sb是作为参数传过来的,m2()执行的同时可能会被其余线程批改 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; }
2.3.4 栈内存溢出
Java.lang.stackOverflowError 栈内存溢出
产生起因
- 虚拟机栈中,栈帧过多(比方有限递归)
- 每个栈帧所占用内存过大
2.3.5 线程运行诊断
CPU 占用过高。Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时须要定位占用CPU过高的线程
- top 命令,查看哪个过程占用CPU过高
- 拿到占用CPU过高的过程ID后,再通过 ps 查看哪个线程占用 CPU 过高。"
ps H -eo pid, tid, %cpu | grep 查到的占用CPU过高的过程ID
" - 通过 "
jstack 过程id
" 命令查看过程中线程ID(nid),与上一步查出的线程ID(tid)来比照定位有问题的线程以及源码行号。留神jstack查找出的线程ID(nid)是16进制的须要转换。
程序运行很长时间没有后果 。个别可能是产生了线程死锁,这时须要找到死锁的线程
- 找到以后运行的java程序的过程ID(也能够应用
jps
命令) - 通过 "
jstack 过程id
" 命令查看过程中是否有死锁线程 演示
class A{}class B{}public class Demo004 { 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(); }}
- 找到以后运行的java程序的过程ID(也能够应用
2.4 本地办法栈
一些带有native关键字的办法就是须要JAVA去调用本地的C或者C++办法,因为JAVA有时候没法间接和操作系统底层交互,所以须要用到本地办法。
本地办法栈也是 线程公有的
2.5 堆
2.5.1 定义
通过new关键字创立的对象都会被放在堆内存
2.5.2 特点
- 所有线程共享。堆内存中的对象都须要思考线程平安问题
- 有垃圾回收机制
- 存在堆内存溢出
2.5.3 堆内存溢出
java.lang.OutofMemoryError :java heap space 堆内存溢出
2.5.4 堆内存诊断工具
- jps工具:查看以后零碎中有哪些java过程
- jmap工具:java内存映像工具。如查看堆内存占用状况 ,
jmap -heap pid
- jconsole工具:图形界面的,Java监督与治理控制台
- jvisualvm工具:图形界面的,多合一故障解决工具
2.6 办法区
2.6.1 定义
办法区用于存储每个类的构造,比方运行时常量池、字段和办法数据,以及办法和构造函数的代码,包含在类和实例初始化以及接口初始化中应用的非凡办法。办法区域能够是固定大小,也能够依据计算的须要进行扩大,如果不须要更大的办法区域,则能够膨胀。
2.6.2 组成
办法区是一个抽象概念,JDK1.8以前办法区由永恒代实现,JDK1.8当前办法区由元空间实现
2.6.3 办法区内存溢出
JDK1.8之前会导致永恒代内存溢出
- 设置永恒代最大内存参数:-XX:MaxPerSize=<size>
- 异样信息:java.lang.OutOfMemoryError: PerGen space
JDK1.8之后会导致元空间内存溢出
- 设置元空间最大内存参数:-XX:MaxMetaspaceSize=<size>
- 异样信息:java.lang.OutOfMemoryError: Metaspace
演示
/** * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
*/public class Demo006 extends ClassLoader { // 继承ClassLoader,能够用来加载类的二进制字节码 public static void main(String[] args) { int j = 0; try { Demo006 test = new Demo006(); 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); } }}```
尽管咱们本人编写的程序没有大量应用动静加载类,但如果咱们在应用内部一些框架时,可能大量动静加载类,就可能会导致元空间内存溢出。如 spring、mybatis 的动静代理
2.6.4 常量池
- 常量池:Class文件中除了类的版本、字段、办法、接口等形容信息,还有常量池,用于寄存编译期生成的各种字面量和符号援用。就是一张表,虚拟机指令依据这张常量表找到要执行的类名、办法名、参数类型、字面量等信息。
- 运行时常量池:常量池是class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把外面的符号地址翻译为实在地址
较于Class文件常量池,运行时常量池具备动态性,在运行期间也能够将新的常量放入常量池中,而不是肯定要在编译时确定的常量能力放入。最次要的使用便是String类的intern()办法
常量池与JVM字节码演示
public class Demo007 { public static void main(String[] args) { System.out.println("hello world"); }}
通过 "javap -v <class文件>
" 反编译,查看二进制字节码,其中蕴含了类根本信息,常量池,类办法定义(蕴含虚拟机指令)。
进入Demo007.class 所在目录执行,javap -v Demo007.class
,输入后果如下,
Classfile /E:/codes/java_demos/out/production/jvm/indi/taicw/jvm/structure/methodarea/constantpool/Demo007.class Last modified 2020-10-7; size 622 bytes MD5 checksum 1356d96bb349e8a95e8c5df1cb36040c Compiled from "Demo007.java"public class indi.taicw.jvm.structure.methodarea.constantpool.Demo007 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #23 // hello world #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #26 // indi/taicw/jvm/structure/methodarea/constantpool/Demo007 #6 = Class #27 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lindi/taicw/jvm/structure/methodarea/constantpool/Demo007; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 Demo007.java #20 = NameAndType #7:#8 // "<init>":()V #21 = Class #28 // java/lang/System #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #23 = Utf8 hello world #24 = Class #31 // java/io/PrintStream #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #26 = Utf8 indi/taicw/jvm/structure/methodarea/constantpool/Demo007 #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V{ public indi.taicw.jvm.structure.methodarea.constantpool.Demo007(); 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 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lindi/taicw/jvm/structure/methodarea/constantpool/Demo007; 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 #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello world 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 8: 0 line 9: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;}SourceFile: "Demo007.java"
2.6.5 字符串常量池 StringTable
字符串常量池 是常量池中专门用来寄存常量字符串的一块区域,应用 StringTable 进行存储,底层数据结构是一个哈希表。
StringTable 地位
- JDK1.6版本中,字符串常量池是在永恒代
- JDK1.7版本及当前版本,JVM 曾经将字符串常量池从办法区中移了进去,在 Java 堆(Heap)中开拓了一块区域寄存字符串常量池
StringTable 个性
- 常量池中的字符串仅是符号,第一次用到时才变为对象并放入字符串常量池
- 利用字符串常量池的机制,来防止反复创立字符串对象
- 字符串变量拼接的原理是 StringBuilder
- 字符串常量拼接的原理是编译期优化
能够应用
String#intern()
办法,被动将以后字符串退出到字符串常量池中,并返回常量池中该字符串对象的援用- JDK1.7及当前版本中调用
intern()
办法将这个字符串对象尝试放入串池,如果串池有则不会放入,如果没有则放入串池,并把串池中的对象援用返回 - JDK1.6 版本中调用
intern()
办法将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份放入串池,并把串池中的对象援用返回,而之前那个字符串对象还是存在的
- JDK1.7及当前版本中调用
字符串拼接与字符串常量池相干问题
public class Demo008 { public static void main(String[] args) { String s1 = "a"; //字符串 "a" 放入串池,StringTable ["a"] String s2 = "b"; //字符串 "b" 放入串池,StringTable ["a", "b"] String s3 = "ab"; //字符串 "ab" 放入串池,StringTable ["a", "b", "ab"] //因为s1、s2是变量,只有运行期间能力确定具体值,所以底层实现是 new StringBuilder().append("a").append("b").toString(),返回一个新的"ab"字符串对象 String s4 = s1 + s2; // javac 在编译期间的优化,后果曾经在编译期确定为 "ab",所以等价于 s5 = "ab",此时常量池已存在不必再创立新的"ab"对象 String s5 = "a" + "b"; // s4调用intern()办法尝试把"ab"放入串池中,此时串池已存在"ab", 并返回串池中的"ab"对象援用赋值给s6 String s6 = s4.intern(); //false,起因:s3 是串池中"ab"字符串对象的援用,s4是堆中"ab"对象的援用 System.out.println(s3 == s4); //true,起因:s3、s5 都是串池中"ab"字符串对象的援用 System.out.println(s3 == s5); //true,起因:s3、s6 都是串池中"ab"字符串对象的援用 System.out.println(s3 == s6); String x2 = new String("c") + new String("d"); x2.intern(); //把字符串"cd"放入串池,StringTable ["a", "b", "ab", "cd"] String x1 = "cd"; //间接把串池中的"cd"对象援用赋值给 x1 //JDK1.7及之后版本,后果为true, 起因:x2调用intern()办法尝试把"cd"放入串池,此时串池不存在"cd"放入胜利,此时串池中的"cd"对象与堆中的是同一个 //JDK1.6版本,后果为false, 起因:x2调用intern()办法尝试把"cd"放入串池,此时串池不存在"cd"字符串,会复制一个新的"cd"对象放入串池中,此时串池中的"cd"对象与堆中的不是同一个 System.out.println(x1 == x2); String x4 = new String("e") + new String("f"); String x3 = "ef"; //把字符串"cd"放入串池,StringTable ["a", "b", "ab", "cd", "ef"] x4.intern(); //后果为false, 起因:x4调用intern()办法尝试把"ef"放入串池,此时串池已存在"cd"放入失败,此时x3是串池中"ef"对象的援用,而x4是堆中的"ef"对象援用 System.out.println(x3 == x4); }}
2.6.6 StringTable 垃圾回收
因为在jdk1.7版本当前,字符串常量池是放在堆中,如果堆空间有余,字符串常量池也会进行垃圾回收。如下演示代码,设置最大堆内存为10m 并打印 gc 信息
/** * 演示 StringTable 垃圾回收 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc */public class Demo009 { public static void main(String[] args) { int i = 0; try { for (int j = 0; j < 100000; j++) { // j=100, j=10000 String.valueOf(j).intern(); i++; } } catch (Throwable e) { e.printStackTrace(); } finally { System.out.println(i); } }}
2.6.7 StringTable 性能调优
StringTable 的数据结构是哈希表,所以能够适当减少哈希表桶的大小,来缩小字符长放入StringTable所需的工夫
- 设置StringTable桶大小参数:
-XX:StringTableSize=<桶个数>
- 设置StringTable桶大小参数:
- 对于有些反复的字符串能够思考放入常量池,能够缩小内存占用
2.7 间接内存
2.7.1 定义
间接内存并不是JVM内存区域的一部分,不受JVM内存回收治理,常见于NIO操作用于数据缓存区,调配回收老本高但读写性能高。
java程序资源文件读取过程中,未应用间接内存和应用间接内存区别如下:
未应用间接内存
- 须要从用户态向内核态申请资源,内核态会创立零碎缓冲区,用户态会创立一个java 缓冲区byte[],而后数据从零碎缓存区复制到java缓存区
应用间接内存
- 须要从用户态向内核态申请资源,即内核态会创立一块间接内存direct memory,这块direct memory内存能够在用户态、内核态应用。间接内存是操作系统和Java代码都能够拜访的一块区域,无需将数据从零碎内存复制到Java堆内存,从而进步了效率
应用一般内存形式与应用间接内存形式读取大文件效率比拟,演示代码如下:
import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;/** * 演示 一般内存与间接内存读取文件效率 */public class Demo011 { static final String FROM = "C:\\Users\\taichangwei\\Downloads\\CentOS-6.10-x86_64-bin-DVD2.iso"; //一个2.03G的文件 static final String TO = "E:\\a.iso"; static final int _1Mb = 1024 * 1024; public static void main(String[] args) { io(); directBuffer(); } 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); }}/*执行三次别离用时io 用时:4944.8368directBuffer 用时:1927.7912io 用时:4989.7547directBuffer 用时:2287.7028io 用时:5022.2618directBuffer 用时:2453.5307 */
2.7.2 间接内存溢出
间接内存尽管不受JVM内存治理,然而物理内存是无限的,所以间接内存还是会存在内存溢出,java.lang.OutOfMemoryError: Direct buffer memory
演示间接内存溢出,代码如下:
import java.nio.ByteBuffer;import java.util.ArrayList;import java.util.List;/** * 演示 间接内存溢出 */public class Demo012 { static int _100Mb = 1024 * 1024 * 100; public static void main(String[] args) { List<ByteBuffer> list = new ArrayList<>(); int i = 0; try { while (true) { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb); list.add(byteBuffer); i++; } } finally { System.out.println(i); } }}
循环了 36 次,应用了3.6G的内存导致内存溢出了
2.7.3 间接内存的调配及回收原理
底层应用了UnSafe对象实现间接内存的调配及回收,并且回收须要被动调用freeMemory()办法
间接内存调配及回收的底层原理演示,代码如下:
import sun.misc.Unsafe;import java.io.IOException;import java.lang.reflect.Field;public class Demo014 { static int _1Gb = 1024 * 1024 * 1024; public static void main(String[] args) throws IOException { Unsafe unsafe = getUnsafe(); // 分配内存 long base = unsafe.allocateMemory(_1Gb); unsafe.setMemory(base, _1Gb, (byte) 0); //暂定期待。此时查看工作管理器(windows零碎),会发现有一个占用1G内存的java程序 System.in.read(); // 开释内存 unsafe.freeMemory(base); //暂定期待。此时查看工作管理器(windows零碎),会发现方才占用1G内存的java程序内存开释了 System.in.read(); } public static Unsafe getUnsafe() { try { //Unsafe 构造方法是私有化的,只能通过反射取得 Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null); return unsafe; } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } }}
通常状况是咱们是应用 "
ByteBuffer.allocateDirect(int capacity)
" 来申请间接内存就能够了。ByteBuffer的实现类外部应用了Cleaner(虚援用)来检测ByteBuffer,一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean()办法调用freeMemory()来开释内存allocateDirect(int capacity) 办法外部实现:
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
DirectByteBuffer(int cap) 办法外部实现
DirectByteBuffer(int cap) { super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
这里调用了一个Cleaner的create()办法,且后盾线程还会对虚援用的对象监测,如果虚援用的理论对象(这里指的是DirectByteBuffer对象)被回收当前,就会调用Cleaner的clean()办法,来革除间接内存中占用的内存
public void clean() { if (remove(this)) { try { this.thunk.run(); } catch (final Throwable var2) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { if (System.err != null) { (new Error("Cleaner terminated abnormally", var2)).printStackTrace(); } System.exit(1); return null; } }); } } }
其中 "
this.thunk
" 是上一步传入的Deallocator
对象,它的run办法外部实现如下:public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity); }