Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域称作运行时数据区域,分为5个部分:程序计数器、虚拟机栈、本地方法栈、堆、方法区。
这是在网上找的一张图,今天就来把各个区的用途和创建销毁时机来说一下。
1、程序计数器:它是当前程序执行的字节码的行号指示器,jvm执行引擎具体要执行哪一行指令,是由程序计数器来指示的。
javap是jkd自带的反编译命令,在命令行执行以下命令,得到MyStack的字节码文件,程序计数器指示的就是#1,#2,#3之类的行号:
javap -verbose MyStack.class > MyStack.txt
Constant pool:
#1 = Class #2 // com/bill99/MyStack
#2 = Utf8 com/bill99/MyStack
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/bill99/MyStack;
#14 = Utf8 print
#15 = Fieldref #16.#18 // java/lang/System.out:Ljava/io/PrintStream;
#16 = Class #17 // java/lang/System
#17 = Utf8 java/lang/System
#18 = NameAndType #19:#20 // out:Ljava/io/PrintStream;
#19 = Utf8 out
#20 = Utf8 Ljava/io/PrintStream;
#21 = String #22 // print()
#22 = Utf8 print()
#23 = Methodref #24.#26 // java/io/PrintStream.println:(Ljava/lang/String;)V
#24 = Class #25 // java/io/PrintStream
#25 = Utf8 java/io/PrintStream
#26 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
#29 = Utf8 main
#30 = Utf8 ([Ljava/lang/String;)V
#31 = Methodref #1.#9 // com/bill99/MyStack."<init>":()V
#32 = Methodref #1.#33 // com/bill99/MyStack.print:()V
#33 = NameAndType #14:#6 // print:()V
#34 = Utf8 args
#35 = Utf8 [Ljava/lang/String;
#36 = Utf8 myStack
#37 = Utf8 SourceFile
#38 = Utf8 MyStack.java
2、虚拟机栈:每个方法在执行的时候都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息然后压入虚拟机栈,每一个方法结束对应出栈的操作。
如下示例代码,debug调用栈也是main()方法先入栈,然后print()入栈,print()执行结束出栈,main()执行。
如下public class MyStack
{
public void print(){
System.out.println("print()");
}
public static void main(String[] args) {
MyStack myStack = new MyStack();
myStack.print();
}
}
这块区域常见异常有两种:线程请求栈的深度大于虚拟机栈所允许的深度,将抛出StackOverflowError;栈扩展的时候无法申请到足够内存空间,则抛出OutOfMemoryError。
改写一下print()方法,让它不断递归调用自己,可以看到栈深度到1万以上抛异常了。
private static int depth = 0;
public void print(){
System.out.println("print()"+(++depth));
print();
}
print()11422
Exception in thread "main" java.lang.StackOverflowError
3、本地方法栈:由于java代码本身的限制,有些和操作系统直接交互的方法,可能是基于c或者c++编写的,java代码可以通过本地方法间接的去调用操作系统底层的一些功能。本地方法运行时使用的内存空间就是本地方法栈。
比如Thread类的一些方法,如下:
public static native Thread currentThread();
public static native void yield();
4、Java堆:Java堆是虚拟机管理内存中最大的一块,是被所有线程共享的一块内存区域,常说的线程安全,指的就是堆中对象的线程安全问题。几乎所有new出来的对象都存放在这里,常说的内存分配和垃圾回收,针对的就是堆内存。
堆内存按两部分划分,可以是新生代和老年代,比例1:2;更细粒度划分,新生代又分Eden(伊甸园区),from,to,比例8:1:1.
1、创建一个MyHeap,这个对象有一个私有变量byte1,占1M内存
public class MyHeap
{
private static final int MB = 1024 * 1024;
byte[] byte1 = new byte[MB];
public static void main(String[] args) {
List<MyHeap> list = new ArrayList<>();
MyHeap myStack = new MyHeap();
list.add(myStack);
}
}
2、配置堆内存初始值和最大值为20M,其中新生代占10M,并且打印堆内存信息
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
3、分析运行结果:
可以看出新生代PSYoungGen显示的内存总大小是9216K,为什么不是设置的10M呢?是因为from和to区总有一个是空闲的,不能用来存储对象的(因为垃圾回收需要复制,即从from复制到to,to必须和from空间一样大并且空闲,才能接收from的对象)。MyHeap对象占用了Eden2026K内存,2026K/8192K=24%;
老年代ParOldGen有堆内存-新生代:20M-10M=10240K;
永久代PSPermGen有20M。
Heap
PSYoungGen total 9216K, used 2026K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 24% used [0x00000000ff600000,0x00000000ff7faa88,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
PSPermGen total 21504K, used 2550K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
object space 21504K, 11% used [0x00000000f9a00000,0x00000000f9c7d9f0,0x00000000faf00000)
5、方法区:与Java堆一样,是线程共享区域,主要存储类信息、常量、静态变量、即时编译器编译后的代码等数据。hotspot虚拟机实现中把此方法区又称作“永久代”。
运行时常量区:它是方法区的一部分。前面程序计数器字节码文件举例时,文件中Constant pool指的就是常量区,用于存放编译器生成的各种字面量和符号引用。
能进入常量池的内容不一定是编译器产生的,像String.intern();是在运行时把数据放入常量池的。
public static void main(String[] args) {
String s1 = "a";//行号6
String s2 = "b";//行号7
String s3 = "ab";//行号8
String s4 = s1+s2;//行号9
System.out.println(s3==s4);//行号10
}//行号11
以上代码编译后的部分字节码如下,我们发现第9行代码编译成了new StringBuilder对象,而6,7,8行代码编译后分别是0,3,6对应的常量字符串,所以可以验证第11行输出肯定是false,因为s4对象在堆中,而s3在常量池。
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: ldc #16 // String a
2: astore_1
3: ldc #18 // String b
5: astore_2
6: ldc #20 // String ab
8: astore_3
9: new #22 // class java/lang/StringBuilder
12: dup
13: aload_1
14: invokestatic #24 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
17: invokespecial #30 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
20: aload_2
21: invokevirtual #33 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #37 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: getstatic #41 // Field java/lang/System.out:Ljava/io/PrintStream;
32: ldc #20 // String ab
34: aload 4
36: if_acmpne 43
39: iconst_1
40: goto 44
43: iconst_0
44: invokevirtual #47 // Method java/io/PrintStream.println:(Z)V
47: return
LineNumberTable:
line 6: 0
line 7: 3
line 8: 6
line 9: 9
line 10: 29
line 11: 47
用一下神奇的String.intern(),再看看结果
public static void main(String[] args) {
String s1 = "a";//行号6
String s2 = "b";//行号7
String s3 = "ab";//行号8
String s4 = s1+s2;//行号9
System.out.println(s3==s4);//行号10
String s5 = s4.intern();
System.out.println(s3==s5);
}//行号11
System.out.println("ab"==s5);就会输出true,因为s4.intern()方法是主动把字符串放入常量池,s5现在是指向常量池中的ab。
6、直接内存:在没有直接内存的情况下,磁盘I/O的内容,需要先从磁盘读取到系统内存,然后再从系统内存读取到Java堆内存;而直接内存是系统内存和Java堆内存的一块共享内存区域,避免了在Java堆和Native堆中来回复制数据,很大的提高了读写性能。