阅读<<深入理解java虚拟机>>所感所悟 1,2章节

来源:互联网 发布:飞鸟淘宝客真的吗 编辑:程序博客网 时间:2024/04/25 13:07

java:一次编写,到处运行。
JDK是用于支持java程序开发的最小环境。
java的核心虚拟机是HotSpot。
java运行时数据区域主要有:方法区,堆,虚拟机栈,本地方法栈,程序计数器。

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每条线程都会有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈中的局部变量表存放了编译期可知的各种基本数据类型,对象引用,returnAddress类型。(returnAddress类型会被Java虚拟机的jsr、ret和jsr_w指令所使用。returnAddress类型的值指向一条虚拟机指令的操作码。与前面介绍的那些数值类的原生类型不同,returnAddress类型在Java语言之中并不存在相应的类型,也无法在程序运行期间更改returnAddress类型的值。)。在java虚拟机规范中,对虚拟机栈区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展时(大部分虚拟机支持动态扩展,但是也允许固定长度的虚拟机栈)无法申请到足够的内存,会抛出OutOfMemoryError异常。

本地方法栈:此区域与虚拟机栈所发挥的作用非常相似,它们的区别是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。此区域与虚拟机栈一样,也会抛出StackOverflowError,OutOfMemoryError异常。(HotSpot虚拟机直接把两块区域合二为一)

java堆:java堆是java虚拟机管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。此外,java堆也是垃圾收集器管理的主要区域,因此也被成为“GC堆”。从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以java堆中还可以细分为:新生代和老年代。如果堆中没有可用内存来完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区:与java堆一样,是各个线程共享的内存区域,它用于存储已被java虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。当方法区无法满足要求内存分配需求时,将会抛出OutOfMemoryError异常。(字符串常量池在jdk1.7中从“永久代”中移动到了堆中的某片区域,而在jdk1.8以后,“永久代”区域被彻底移除,取而代之的是“元空间”)

永久代与元空间详解

运行时常量池:是方法区的一部分。当类加载后将常量池中的内容放入方法区的运行时常量池中存放。

常量池(ConstantPool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。

字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。

运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。

HotStop创建对象:当虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、和初始化过。如果没有,那么先执行相应的类加载过程。在确定类被加载后,接下来虚拟机将为新生的对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(这一步保证了对象的实例字段在java代码中不赋初值就可以直接使用)。此时,从虚拟机的视角新对象已经创建成功了,从java视角来看对象创建才刚刚开始,方法还没有执行,所有字段都还为零。所以一般来说执行完new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化。

虚拟机创建对象时存在的问题
一.内存如何分配?内存分配方式主要有两种,一种为“指针碰撞”,一种为“空闲列表”。前者主要针对java堆中的内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把那个指针向空闲空间那边挪动与对象内存大小相等的距离。“空闲列表”主要是针对java堆中的内存不是规整的情况,此时虚拟机需要维护一个列表,记录上哪些内存块是可用的,
在分配的时候从列表中找到一块足够大的空间划分给对象实例。java堆是否规整是由所采用的垃圾收集器是否带有压缩整理功能决定。
二.如何处理并发分配内存的情况?在分配内存时可能会出现这种情况:在给A对象分配内存时,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决方法主要有两种方案,一种是保证操作的原子性,即采用CAS(CAS详解)配上失败重试的方式保证更新操作的原子性(这也是实际上虚拟机采用的方式);另一种是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

对象的内存布局:
在HotSpot中,对象在内存中存储的布局可以分为3块区域:对象头,实例数据和对齐填充。
对象头又包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄等。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是属于哪个类的实例。(查找对象的元数据信息不一定要经过对象本身)如果对象是一个java数组,那么在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机无法从数组的元数据中确定数组的大小(元数据详解)
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

虚拟机如何对对象进行访问?

目前主流的访问方式有两种:使用句柄和直接访问。
如果使用句柄,那么java堆中将会划出一块内存来
作为句柄池,reference(引用)中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与数据类型各自的具体地址信息。这种方式的好处
是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不用修改。
reference->句柄->对象实际地址

如果使用直接指针访问,那么reference中存储的就是对象的地址。

reference->对象地址

这种好处是速度更快,它节省了一次指针定位的时间开销。(HotStop使用本方式)

本博客是在看<深入理解java虚拟机>时做的笔记,有不到位的地方欢迎大家批评。

三千里路自己体会!

原创粉丝点击