JVM内存结构

JVM内存结构

内存结构基本示意图:

1. 程序计数器

1.1 定义

行号指示器,字节码解释器运行时通过改变计数器值来选取下一条需要执行的字节码指令。

1.2 特点

每条线程都需要有独立的程序计数器,线程私有

唯一一个不会出现OutOfMemoryError情况的内存区域

2. 栈

2.1 栈和栈帧

  • 栈:线程运行所需要的内存空间

    1. 线程生命周期和栈的生命周期相同
    2. 栈的内存空间和线程数呈负相关
  • 栈帧:每个方法运行时所需要的内存

    1. 存储局部变量表,操作数栈,动态连接,方法出口等
    2. 生命周期和方法相同,每个方法调用直到执行完毕,就对应着栈帧入栈到出栈的过程。

局部变量表:用于存储方法参数和局部变量等

2.2 栈的线程安全

!!!方法内的局部变量是否涉及线程安全:

线程安全:保证多个线程同时对某一对象或资源进行操作时不会出错

  1. 如果方法内局部变量没有逃离方法作用范围,那么他是**线程安全**的:

​ 没有逃离方法的作用范围,局部变量无逃逸,无法被其他线程访问。

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. 如果局部变量引用了对象,并逃离方法的作用范围,那么他是**不安全**的:

    作为参数传参进入,可以会有被别的对象调用的风险。

    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 栈的有关异常

  1. 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(); //递归调用自己,但是没有设置终止条件
}
}
  1. 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); // hello, hellohello, hellohellohellohello ...
a = a + a; // hellohellohellohello
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}

4.4 堆内存诊断

  • JPS: 查看系统中有哪些java进程

直接在Terminal窗口输入jps中可得到下图结果。

1
2
3
4
5
//进程编号         //进程名称
11936 Jps
13664 Demo1_4
436
9388 Launcher
  • Jmap:观测指定进程的内存情况

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);
}
}
  • 1.8 元空间内存溢出
1
2
// java.lang.OutOfMemoryError: Metaspace
// -XX:MaxMetaspaceSize=8m
  • 1.6 永久代内存溢出
1
2
// java.lang.OutOfMemoryError: PermGen space
// -XX:MaxPermSize=8m

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 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // hello world
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #22 // java/lang/Object
#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 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 hello world
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#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 // Method java/lang/Object."<init>":()V
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 // 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 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"; // ab
String s4 = s1 + s2; // new String("ab")
String s5 = "ab";
String s6 = s4.intern();

// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true

String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();
String x1 = "cd";

// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
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";
// new StringBuilder().append("a").append("b").toString()-> new String("ab")
// toString()方法生成了一个新的String对象

return s3 == s4; //false

7.3 编译期优化

1
2
3
4
5
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; //编译期的优化:两个常量之和是确定的
String s4 = "ab";
return s3 == s4 //true

7.4 intern()方法

  • 1.8

将字符串对象放入串池,有则不会放入,无则放入后返回

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");

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
String x = "ab";
System.out.println( s2 == x); //true
System.out.println( s == x ); //true
}
  • 1.6

地址复制后放入串池(和原来的地址不相同)

public static void main(String[] args) {

1
2
3
4
5
6
7
String s = new String("a") + new String("b");

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
String x = "ab";
System.out.println( s2 == x); //true
System.out.println( s == x ); //false 因为s2是复制的,和原来地址并不一样

}

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 特点

  1. 分配回收成本高,读写性能高
  2. 和系统内存相关,和java本身堆大小无关

8.3 内存释放

通过unsafe对象对于directMomery对象的调用