JVM内存结构
内存结构基本示意图:
1. 程序计数器
1.1 定义
行号指示器,字节码解释器运行时通过改变计数器值来选取下一条需要执行的字节码指令。
1.2 特点
每条线程都需要有独立的程序计数器,线程私有
唯一一个不会出现OutOfMemoryError情况的内存区域
2. 栈
2.1 栈和栈帧
栈:线程运行所需要的内存空间
- 线程生命周期和栈的生命周期相同
- 栈的内存空间和线程数呈负相关
栈帧:每个方法运行时所需要的内存
- 存储局部变量表,操作数栈,动态连接,方法出口等
- 生命周期和方法相同,每个方法调用直到执行完毕,就对应着栈帧入栈到出栈的过程。
局部变量表:用于存储方法参数和局部变量等
2.2 栈的线程安全
!!!方法内的局部变量是否涉及线程安全:
线程安全:保证多个线程同时对某一对象或资源进行操作时不会出错
- 如果方法内局部变量没有逃离方法作用范围,那么他是**线程安全**的:
没有逃离方法的作用范围,局部变量无逃逸,无法被其他线程访问。
1 2 3 4 5 6 7
| public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); }
|
如果局部变量引用了对象,并逃离方法的作用范围,那么他是**不安全**的:
作为参数传参进入,可以会有被别的对象调用的风险。
1 2 3 4 5 6
| public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); }
|
存在返回值,有可能被他人调用:
1 2 3 4 5 6 7
| public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; }
|
2.3 栈的有关异常
- StackOverFlowError: 线程请求的栈深度大于虚拟机所允许的深度
一般出现原因为递归调用无设置终止条件或者终止条件无法达到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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(); } }
|
- OutOfMemoryError: 栈无法申请到足够多的内存空间时
与内存空间有关。
3. 本地方法栈
为本地方法运行提供空间的内存空间
public native int hashCode();
1 2 3
| public boolean equals(Object obj) { return (this == obj); }
|
含有**native关键字**修饰的方法
object: motify, add, hashcode….
4. 堆
4.1 定义
是被所有线程共享的一块区域,在虚拟机启动的时候自动创建,目的是存放对象实例(new)。
4.2 特点
- 线程共享,需要考虑线程安全的问题
- 在虚拟机启动的时候自动创建
- 有垃圾回收机制
4.3 堆内存溢出
java.lang.OutOfMemoryError: Java heap space:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public static void main(String[] args) { int i = 0; try { List<String> list = new ArrayList<>(); String a = "hello"; while (true) { list.add(a); a = a + a; i++; } } catch (Throwable e) { e.printStackTrace(); System.out.println(i); } }
|
4.4 堆内存诊断
直接在Terminal
窗口输入jps中可得到下图结果。
1 2 3 4 5
| 11936 Jps 13664 Demo1_4 436 9388 Launcher
|
用 jmap -heap + 进程编号
查看当前进程的运行情况
Configuration```: 堆配置信息1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| ```MaxHeapSize```: 最大堆空间
<img src="/source/images/jmap2.png" style="zoom:67%;" align="left"/>
```Heap Usage```: 堆使用情况
```used```: 已经使用
```free```: 未使用
- **Jconsole:图形化页面监测工具,可以实现动态的检测**
<img src="/source/images/jconsole.png" style="zoom:50%;" align="left" />
## 5. 方法区
### 5.1 作用
存储被虚拟机加载的类型信息,常量,静态变量,代码缓存等数据
### 5.2 JVM1.8和1.6不同的组成
![](/source/images/MethodArea.png)
主要变化:实现方式由永久代转变为了元空间,常量池中```StringTable```放到了堆内存中。
原因:永久代垃圾回收机制为```Full GC```,回收效率低,易导致临时变量占据空间过多,堆内存中垃圾回收机制较永久代灵活
### 5.3 方法区内存溢出
```java 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); } }
|
6. 常量池
作用:用于存储信息,本身时二进制字节码对象,常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
通过javap -v HelloWorld.class
可以获得class二进制字节码文件的基本信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| Last modified 2022-11-24; size 442 bytes MD5 checksum 103606e24ec918e862312533fda15bbc Compiled from "HelloWorld.java" public class cn.itcast.jvm.t5.HelloWorld minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#15 #2 = Fieldref #16.#17 #3 = String #18 #4 = Methodref #19.#20 #5 = Class #21 #6 = Class #22 #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 HelloWorld.java #15 = NameAndType #7:#8 #16 = Class #23 #17 = NameAndType #24:#25 #18 = Utf8 hello world #19 = Class #26 #20 = NameAndType #27:#28 #21 = Utf8 cn/itcast/jvm/t5/HelloWorld #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V { public cn.itcast.jvm.t5.HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 4: return LineNumberTable: line 4: 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 #2 3: ldc #3 5: invokevirtual #4 8: return LineNumberTable: line 6: 0 line 7: 8 }
|
主要操作流程: 根据指令后面编号去查找相应内容
7. StringTable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public static void main(String[] args) { String s1 = "a"; String s2 = "b"; String s3 = "a" + "b"; String s4 = s1 + s2; String s5 = "ab"; String s6 = s4.intern();
System.out.println(s3 == s4); System.out.println(s3 == s5); System.out.println(s3 == s6);
String x2 = new String("c") + new String("d"); x2.intern(); String x1 = "cd";
System.out.println(x1 == x2); }
|
7.1 常量池与串池之间的关系
常量池中存储的信息只是作为一个符号,通过方法调用(String a = "a"
)或引用的时候才能真正的成为一个对象,而这时候如果串池中没有该对象的时候,将对象放入串池
懒惰性:用到了才会在串池中创建,没有用到不会创建(延迟加载)
HashCode结构
7.2 字符串拼接
1 2 3 4 5 6 7 8
| String s1 = "a"; String s2 = "b"; String s3 = s1 + s2; String s4 = "ab";
return s3 == s4;
|
7.3 编译期优化
1 2 3 4 5
| String s1 = "a"; String s2 = "b"; String s3 = "a" + "b"; String s4 = "ab"; return s3 == s4
|
7.4 intern()方法
将字符串对象放入串池,有则不会放入,无则放入后返回
1 2 3 4 5 6 7 8 9 10 11
| public static void main(String[] args) {
String s = new String("a") + new String("b");
String s2 = s.intern(); String x = "ab"; System.out.println( s2 == x); System.out.println( s == x ); }
|
地址复制后放入串池(和原来的地址不相同)
public static void main(String[] args) {
1 2 3 4 5 6 7
| String s = new String("a") + new String("b");
String s2 = s.intern(); String x = "ab"; System.out.println( s2 == x); System.out.println( s == x );
|
}
7.5 性能调优
- 调整-XX:StringTableSize=桶个数:桶个数多有利于提高效率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static void main(String[] args) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) { String line = null; long start = System.nanoTime(); while (true) { line = reader.readLine(); if (line == null) { break; } line.intern(); } System.out.println("cost:" + (System.nanoTime() - start) / 1000000); }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public static void main(String[] args) throws IOException {
List<String> address = new ArrayList<>(); System.in.read(); for (int i = 0; i < 10; i++) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) { String line = null; long start = System.nanoTime(); while (true) { line = reader.readLine(); if(line == null) { break; } address.add(line.intern()); } System.out.println("cost:" +(System.nanoTime()-start)/1000000); } } System.in.read();
|
8. 直接内存
8.1 作用
是一种操作系统的内存,可以通过NIO中的directBuffer对象来实现内存调用的效率提高
直接用buffer调用需要通过系统缓冲区和Java缓冲区,而directMomery可以直接访问
8.2 特点
- 分配回收成本高,读写性能高
- 和系统内存相关,和java本身堆大小无关
8.3 内存释放
通过unsafe对象对于directMomery对象的调用