第二节,运行时数据区域。
在这个章节中,作者给出了一个java虚拟机运行时数据区的框图,图的左侧是方法区和堆,这两个数据区是所有的线程所共享的。然后是虚拟机栈、本地方法栈、还有程序计数器,这三个数据区是每一个线程独有的。
所谓的程序计数器,它实际上是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。虚拟机其实就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能。正因为如此,所以每一条线程都需要一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。
如果正在执行的是本地方法,则计数器值为空。
java虚拟机栈,其实也就是我们一般意义上的堆栈,就是每一个线程用来存储函数的局部变量的地方。在Java虚拟机栈上我们可以存储局部变量表、操作数栈、动态链接、方法出口等信息。一个局部变量槽(Slot)的大小四个字节,所以像long ,double 这种长类型,会占用两个局部变量空间。
如果线程请求的栈深度大于虚拟机所允许的深度,则触发StackOverflowError异常。
在hotspot虚拟机中直接把本地方法栈和虚拟机栈合二为一。
Java堆 是被所有的线程所共享的一块内存区域。此区域的唯一目的就是存放对象实例。所有的对象和数组都是在堆上分配的。不过随着JIT编译技术的发展,这句话也就变得不是那么的 “绝对” 了。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC堆。还好没有被翻译成“垃圾堆”,哈哈。Java堆可以处物理上不连续的内存空间中,只要逻辑上是连续的即可。
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据。很多人更愿意把方法区称为永久代。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段方法和接口等描述信息以外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对class文件常量池的另外一个重要特征就是具备动态性。
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。本机直接内存的分配不会受到Java堆大小的限制,但既然是内存,肯定还是会受到本机总内存大小以及处理器寻址空间的限制。
第三节,Hotspot的虚拟机对象探秘
虚拟机遇到一条new指令的时候,首先去检查这个指令的参数是否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
然后开始创建对象,那么首先是给这个对象分配一段内存空间,然后把这段空间初始化为零,然后再根据该类的初始化函数来初始化这段内存空间。其实总体来看和之前的C#或者C++是类似的,语言都是想通的。
这里有必要说一下每个对象内存空间的结构,包括了对象头、实例数据和补齐(Padding)三部分,在对象头中要包含下面这些信息,比如说这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码是多少、对象的GC分代年龄是多少,等等。
关于对象的访问定位有两种方式,
一种是在应用中直接存储了对象的地址指针,
一种是在应用中存储了对象的句柄,然后再句柄中在存储了对象的指针。
在hotspot中是直接存储地址指针的方式,这种方式的优点是访问速度快。
第四节实战OutOfMemoryError异常
本节主要利用虚拟机的各种内存限制的选项,比如-Xmx、-Xms、-Xmn、-Xss、-XX:PermSize=10M -XX:PermMaxSize=10M -XX:MaxDirectMemorySize,然后通过java代码来制造一些内存泄漏,之后Java虚拟机就会生成hprof内存转储文件。然后再使用eclipse memory analyzer来分析。
这里面的各种制造内存溢出的方法就不一一列举了,但是有一点要可以关注下,作者借助CGlib库直接操作字节码运行时,生成了大量的动态类。这个CGlib库是我们以后也可以使用的。