第一次读《深入理解Java虚拟机》,理解的不是很深入。所以在第二遍阅读的时候,通过博客来记录自己阅读中的思考和理解,达到更加清晰深入的认识!!!
PC寄存器是一块较小的内存空间,它可以看作时当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。在任何一个确定时刻,一个内核只会执行一条线程。Java虚拟机的多线是通过 轮流切换并分配处理器执行时间实现的(线程频繁切换会导致性能问题)。
虚拟机栈描述的是Java方法执行的内存模型。Java虚拟机栈的作用与传统语言(例如C语言)中的栈非常类似,用于存储局部变量与其他一些尚未算好的结果。Java虚拟机栈所使用的内存不需要保证是连续的。(注意区分Stack、Heap与Java(VM)Stack 、Java(VM) Heap)
Java虚拟机实现可能会使用到传统的栈(通常称为 C stack)来支持native方法(使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈。本地方法栈与虚拟机栈所发挥的作用非常相似,区别在于一个为的Java方法服务,一个是为native方法服务。在有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
在Java虚拟机中,堆(Heap)是可供各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。(这点随着技术的发展已经不是那么绝对了)Java堆是Java虚拟机管理的最大的一块内存,它存储了被自动内存管理系统(automatic storage management system),也就是常说的garbage collector(垃圾收集器)所管理的各种对象,这些受管理的对象无需也无法显式的销毁。
方法区也是线程共享的内存区域。Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它有一个别名叫Non-Heap(非堆)。(在JDK1.8中为元数据区)
除了运行时数据区域,被频繁使用的还有直接内存(Direct Memory)。它不属于虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是它也可能导致OutOfMemoryError异常。
在Java虚拟机中,新生对象分配内存主要有两种方式指针碰撞(Bump the Pointer)和空闲列表(Free List)。虚拟机使用哪种分配方式的是由Java堆是否规整决定的,而Java堆是否规整又由压缩整理功能决定的。所以,在使用Serial、ParNew等带有Compact过程的收集器时,通常采用指针碰撞。使用CMS这种基于Mark-Sweep算法的收集器,通常采用空闲列表。
分配内存是一个频繁的行为,为了保证在并发情况下的线程安全,采用了两种解决方案CAS和TLAB。
对象真正存储的有效信息,也就是在程序代码中所定义的各种类型的字段内容,这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。
在满足这个前提下,在父类中定义的变量会出现在子类之前。(如果CompactFields参数值为True,那么子类中较窄的变量也可能插入到父类变量的空隙中)
对齐补充并不是必然存在的,也没有特别含义,它仅仅起着占位符的作用。
Java程序需要通过栈上的referenece数据来操作堆上的具体对象,目前主流的方式有使用句柄和直接指针两种。
原书中 | Java内存区域与内存溢出异常 | 一章的知识点比较多,上面的总结基本都是摘抄自书中和 《Java虚拟机规范8版》还有一些博客的观点。
通过学习到的内容,分析下列代码,在运行时每个内存区域存储的数据:
public class ClassMemory {
1. private static int i=1;
2. private String string;
3. public ClassMemory(String string) {
4. this.string=string;
5. }
6. public void sayHello(){
7. System.out.println(string+" "+i);
8. }
9. public static void main(String[] args) {
10. ClassMemory classMemory= new ClassMemory("andrew");
11. classMemory.sayHello();
}
}
当前类运行在main线程中。
当虚拟机运行到ClassMemory classMemory= new ClassMemory("andrew");
时,虚拟机会去进行初始化,当需要对某个类进行初始化时,就会进入加载阶段。
通过Class文件字节码工具**javap ** 解析出
ClassMemory
类的Class文件的常量池信息和方法字节码。(方法区存放的内容不止这两种)
Constant pool:
#1 = Class #2 // chapter2/ClassMemory
#2 = Utf8 chapter2/ClassMemory
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 string
#8 = Utf8 Ljava/lang/String;
#9 = Utf8
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Fieldref #1.#13 // chapter2/ClassMemory.i:I
#13 = NameAndType #5:#6 // i:I
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8
#17 = Utf8 (Ljava/lang/String;)V
#18 = Methodref #3.#19 // java/lang/Object."":()V
#19 = NameAndType #16:#10 // "":()V
#20 = Fieldref #1.#21 // chapter2/ClassMemory.string:Ljava/lang/String;
#21 = NameAndType #7:#8 // string:Ljava/lang/String;
#22 = Utf8 this
#23 = Utf8 Lchapter2/ClassMemory;
#24 = Utf8 sayHello
...
...
public chapter2.ClassMemory(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #18 // Method java/lang/Object."":()V
4: aload_0
5: aload_1
6: putfield #20 // Field string:Ljava/lang/String;
9: return
LineNumberTable:
line 7: 0
line 8: 4
line 9: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lchapter2/ClassMemory;
0 10 1 string Ljava/lang/String;
public void sayHello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #20 // Field string:Ljava/lang/String;
5: new #25 // class java/lang/StringBuilder
8: dup_x1
9: swap
10: invokestatic #27 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
13: invokespecial #33 // Method java/lang/StringBuilder."":(Ljava/lang/String;)V
16: getstatic #12 // Field i:I
19: invokevirtual #35 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #39 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: putfield #20 // Field string:Ljava/lang/String;
28: return
LineNumberTable:
line 11: 0
line 12: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lchapter2/ClassMemory;
...
在虚拟机碰到new、getstatic、putstatic、invokestatic四个字节码指令时,都会触发类加载机制。也就是说,如果在其他类通过调用ClassMemory.i
也会触发ClassMemory
类的加载,所以类的解析信息被放入线程共享的方法区内。
通过new创建成功实例对象时,实例对象会被放入Java堆中,通过JDK自带工具Java VisualVM可以看到生成的实例对象的具体信息。
当前线程为main线程,当执行到
classMemory.sayHello();
时,main线程栈帧进行入栈出栈操作。
在进行方法操作时,局部变量表、操作数栈、动态链接、返回地址都会通过实例对象中的指针找到方法区对应的位置获取各自的内容。(栈帧中局部变量表等东西不知道在哪里可以看到,所以这块基本上是猜测的,希望可以有大神帮我解惑 _)
经过“缜密”的分析后,发现自己的知识面还不足以解释一个类的基本运行原理和存储位置,任重道远 !!!