ZB-055-04JVM运行时内存区域

JVM运行时内存区域

方法区

  • 被整个虚拟机所共享的Class信息等
  • 运行时常量池
  • Java7之前:永久代(PermGen)
  • Java8之后:元空间(Metaspace)

我们知道每当你 new Object() 或者 new Cat()的时候,都会在堆内开辟空间,而在你分配空间之前

  • 你首先得知道 Object 长什么样子,也就是 Class,Class就是那个模子,那个说明书,它指明了这个对象如何被创建
  • 因此 Object.class 和 Cat.class 就是你创建 Object / Cat 对象时候参照的说明书
  • 这个说明书放在哪里呢? 我们称之为 方法区

方法区中存的一般都是这些类型信息,被JVM所有人所共享,一般来说是只读的

  • 意外情况就是 运行时常量池

看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("abc");
String s3 = s1.intern();
String s4 = s2.intern();

System.out.println( s1 == s2); // false
System.out.println( s3 == s4); // true
System.out.println( s3.equals(s4)); // 进入 equals if 分支
System.out.println( s1.equals(s2)); // 进入 equals else 分支
}
}
/*
s1 == s2 返回 false可以理解 因为它比较的是引用 ——-》 地址
s3 == s4 为什么 true ? 点击 intern() 进入发现 它是一个 native 方法 意思是它没有java代码的由 java虚拟机帮你实现
intern() 意思是 在一个你看不到的地方 一个池子 Pool 存了很多字符串 ,如 "abc" ,保证你每次调用的时候返回的都是同一个字符串实例
所以 intern() 你可以理解为 内联化,内部表示
*/

intern()的注释非常好

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
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java&trade; Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();

/**
*返回字符串对象的规范表示形式。
*<p>
*一个字符串池,最初是空的,由
*类{@code字符串}。
*<p>
*如果池中已经包含了
* string等于这个{@code string}对象
* {@link #equals(Object)}方法,则池中的字符串为
*返回。否则,将此{@code字符串}对象添加到
*池和这个{@code String}对象的引用被返回。
*<p>
*对于任意两个字符串{@code s}和{@code t},
* {@code s.intern () = = t.intern()}是{@code真的}
*当且仅当{@code s.equals(t)}为{@code true}时。
*<p>
*所有字符串和字符串值常量表达式都是
*实习。的第3.10.5节定义了字符串字面量
* <引用> Java™语言规范< /引用>。
*
* @返回一个与该字符串内容相同的字符串,但是
*保证来自唯一字符串池。
* /
public native String intern();

为什么要 intern() 答案就在 String.equals()

equals 方法实现

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
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

// 如果你要比较两个字符串 它就会跑一个循环,比较每个字符。假如你这个字符串有 几百个几千个字符,意味着你运行 equals 就要比较那么多的次数,效率非常低

假如碰巧这两个对象是同一个对象的话,它的地址是一样的话,只需要一次就可以完成
所以Java的 Collection 体系非常依赖 equals
如你把 String 声明的东西 当作 map的 key 。做成 Set / List 它们比较的时候都会使用 equals
有这样字符串常量池的意义就在于,如果有个常量池,可以对于每个字符串都返回一摸一样对象的时候,用来比较它们相等性的时候就可以通过一个 指令 this == anObject 就完成,效率高很多

子类对象创建的时候同时创建父类对象吗? 是不是有n个Object对象在 jvm中?

是也不是

假如你有以下类,请分别创建 Base.java 和 Sub.java

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
public class Base{
int i;
}

// Base对应字节码
// class version 49.0 (49)
// access flags 0x21
public class Base {

// compiled from: Base.java

// access flags 0x0
I i

// access flags 0x1
public <init>()V
L0
LINENUMBER 1 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this LBase; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}

------------------分割线------------------------
public class Sub extends Base{
int j;
}

// Sub对应字节码
// class version 49.0 (49)
// access flags 0x21
public class Sub extends Base {

// compiled from: Sub.java

// access flags 0x0
I j

// access flags 0x1
public <init>()V
L0
LINENUMBER 1 L0
ALOAD 0
INVOKESPECIAL Base.<init> ()V
RETURN
L1
LOCALVARIABLE this LSub; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}

每当你 new Sub()的时候实际上它会先偷偷帮你调用父类的构造器 也就是偷偷帮你调用new Base(),而你去看 Base 发现它会偷偷调用 new Object()

  • 这样对象就是一层一层的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    _____________________
    object| |
    ______| |
    |
    Base 的数据 i |
    --------------------|
    Sub 的数据 j |
    --------------------|

    同时也解释了 为什么面向对象里你总是可以把一个 子类转换成 父类

基本数据类型的值是放在堆常量池还是栈的常量池?

  • 基本数据类型不放常量池
  • 放在堆还是栈取决于你这个东西是什么东西

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Main {
    public static void main(String[] args) {
    int i = 0;
    // 这个 i 放在 main 的栈帧里

    Base base = new Base();
    // 这个 base会在 堆上开辟一块内存
    // base 里有个 i 初始化是 0
    // base 指向这块空间 base = 一个地址(指向堆上开辟的base对象)
    }
    static class Base{
    int i;
    }
    }
    • 如果是局部变量放在 栈里
    • 如果是一个类的成员它就放在堆里面

永久代(PermGen):Java7之前

  • OutOfMemory
    • 有的时候看到的是 heap space
    • 有的时候是 perm gen (java8之后把它移出来了,移到元空间里了)

元空间(Metaspace):Java8之后

  • 元空间和Native是共享内存

为什么 Java8之后把 perm gen 放在元空间里

  • 永久代在之前的过程中,是和堆放在一起的,会和堆抢内存。很容易碰到 OutOfMemory