《深入理解jvm》读书笔记

我写这个读书笔记的目的有二,其一是为了方便自己以后查看,其二是我发现网上很多关于这本书的读书笔记并不是读书笔记,很多都是原文不动摘取某些章节,并没有自己的理解,我把自己的理解作为补充用红色标注,如果有错误的地方还请指正。:)


一.运行时数据区

运行时数据区分为程序计数器,本地方法栈,虚拟机栈,方法区,堆。

图片来自http://www.cnblogs.com/zhouyuqin/p/5161677.html


1.      程序计数器

程序计数器是一块较小的内存空间,可以看做当前线程所执行字节码的行号指示器,字节码解释器工作时就通过改变这个计数器的值来选取下一条要执行的字节码指令。Java代码告诉字节码解释器我要进行什么操作,然后解释器改变程序计数器的值,找到字节码对应的行号,执行相应的命令。

为了线程切换后能恢复到正确的执行位置,每个线程有一个线程私有的程序计数器,各线程独立计数,所以程序计数器这块内存为线程私有内存。

如果线程在执行Java方法,则程序计数器记录的是虚拟机字节码指令的地址(行号),如果线程在执行本地(native)方法,则计数值为空。也就是说这块只识别Java虚拟机字节码,本地方法怎么调用他无权干涉。

该内存区域没有规定OutOfMemoryError。

2.      Java虚拟机栈

该内存区域为线程私有,生命周期与线程相同。

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等消息。每一个方法的从调用到执行完成过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表:存放编译期可知的基本数据类型(byte,short,char,int,float,long,double,boolean),对象引用(reference类型,可能指向对象起始地址的引用指针,也可能指向一个代表对象的句柄,或其他与此对象相关的位置,对象访问定位再详细说),returnAddress类型(指向一条字节码指令的地址)。

64位的long和double类型占用2个局部变量空间(slot),其余只占用一个,局部变量表所需要的内存空间在编译期间完成分配,进入一个方法时,该方法需要在栈帧中分配多大的局部变量空间完全都是确定的,在运行期不会改变局部变量表大小。

线程请求的栈深度超过虚拟机所允许的深度(就是请求的数据量大于栈中存储的数据量),就会抛出StackOverFlowError(堆栈溢出)异常。大部分虚拟机的虚拟机栈可以动态扩展大小,也可以规定固定长度的虚拟机栈,如果动态扩展无法申请到足够的内存,就会抛出OutOfMemoryError(内存不足)异常。

3.      本地方法栈

与虚拟机栈作用类似,本地方法栈使用本地(native)方法。虚拟机规范中没有强制规定本地方法栈中使用的语言,数据结构等,可以由虚拟机自由实现。Sun公司的hotspot虚拟机就直接将二者合并。

本地方法栈会抛出StackOverFlowError(堆栈溢出)异常和OutOfMemoryError(内存不足)异常。

4.      Java堆

Java堆是Java虚拟机所管理的内存中最大的一块,线程共享,在虚拟机启动时创建。该内存区域唯一的目的就是存放对象实例(对象由对象实例和指向对象实例的引用组成,但也可姑且认为对象即为实例)。

Java堆也称为gc堆,是垃圾收集管理的主要区域,可以细分为新生代和老年代,再细分可分为eden空间,from survivor空间,to survivor空间。

Java堆可以分配在物理不连续而逻辑上连续的内存空间,就像内存1分配在a区域,内存2分配在b区域,物理上不连续,但是他们内部有指向对方的“指针”或者引用,在分配内存或者垃圾回收的时候对a区域执行完了会根据该指针或者引用找到其他地址的内存b

堆内存可以是固定大小的,也可以是可扩展大小的,当扩展时无法申请到足够大的内存或者没有内存来完成实例分配,会抛出OutOfMemoryError(内存不足)异常。

5.      方法区

方法区与Java堆一样,是线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。Java虚拟机规范将他描述为堆的一个逻辑部分,但他并不是堆,也叫“Non-Heap”。

习惯在hotspot虚拟机上开发部署的程序员,很多人更愿意把方法区称为“永久代”,但二者并不等价,只是因为hotspot设计团队希望用gc分代垃圾回收机制扩展至方法区,或者说使用永久代实现方法区,这样hotspot可以像管理Java堆那样管理方法区这部分内存,只是为了管理方便,不必要为了这部分内存再去设计相应的内存管理程序。对于其他虚拟机jrockit,j9等不存在永久代的概念。但是这样设计也会有其他问题,分代收集的永久代导致垃圾回收效率很低,导致很多对象并不能及时回收,会遇到内存溢出问题,所以在hotspot jdk1.7中已经将原本放在永久代的字符串常量池移出,以后会逐步改用本地方法来实现方法区。

方法区也是可以内存不连续逻辑连续即可,可固定大小,也可以扩展大小,而且还可以选择不实现垃圾回收机制,这个区域很少出现垃圾回收,但进入该区域的数据并非如同名字一样就是永久代的了,这个区域内存回收主要针对常量池的回收和对类型的卸载,但对类型的卸载条件相当苛刻,所回收成绩不好。

类型的卸载:(1)该类的所有实例已经被gc,也就是jvm中不存在该类的任何实例(2)加载该类的ClassLoader已经被gc(3)该类的java.lang.class对象没有在任何地方被引用,如:不能再任何地方通过反射访问该类的方法

当方法区无法满足内存分配需求时,会抛出OutOfMemoryError(内存不足)异常。

6.      其他

(1)      运行时常量池

为方法区的一部分,class文件中除了有类的版本,字段(属性),方法,接口,

描述信息外,还有一项就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分将在类加载后进入运行时常量池存放。

Java虚拟机对class文件的每一部分(包括常量池)格式都严格要求,必须符合java虚拟机规范,对于运行时常量池,虚拟机规范没做细节要求,根据不同的虚拟机来不同实现。一般来说,除了保存class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池。

运行时常量池相对于class文件常量池的另一个特征是具有动态性,java并不要求常量只有编译期才能产生,也就是并非预置进入class文件的常量池才能进入运行时常量池,运行期间产生的常量也能加入运行时常量池,这种特性被开发人员运用最多的是string类的intern()方法

intern()方法:当某字符串str调用字符串的intern()方法时,会将运行时常量池中的字符串与str进行equals值对比,,如果二者相等,则该方法返回池中的字符串,如果不等,则将str的放入池中,返回该字符串的句柄(引用),注意,当且仅当str.equals(t)为true时,str.intern()==t.intern()才为true,这里字符串对象为new出来的string对象

public static void main(String[] args) {
String string1 = "qqq";
String string2 = "qqq";
String string3 = new String("qqq");
String string4 = new String("qqq");
System.out.println(string1.equals(string2));
System.out.println(string1==string2);
System.out.println(string1.intern()==string2.intern());
System.out.println(string3.equals(string4));
System.out.println(string3==string4);
System.out.println(string3.intern()==string4.intern());
    }

输出结果:

true
true
true
true
false
true

(2)      直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范定义的内存区域,但这部分也被频繁使用,也会导致java抛出OutOfMemoryError(内存不足)异常。

在jdk1.4新加入了NIO(NewInput/Output)类,引入了一种基于通道(channel)与缓冲区(buffer)的I/O方式,他可以使用native函数库直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了java堆中和native堆中来回复制数据。

本机直接内存的分配不会受到java堆大小的限制,但受限于本机总内存大小以及处理器寻址空间的限制。配置java虚拟机是不能使jvm内存区域的总和大于物理内存,否则可能在动态扩展时导致jvm抛出 OutOfMemoryError(内存不足)异常。

 

二.对象中的内存

1.对象的内存布局

对象在内存中存储布局分为3个区域:对象头(header),实例数据(instance data),对齐填充(pidding)。

对象头包括两部分信息,第一部分存储对象自身的运行时数据(运行时需要用到的数据),如哈希吗(hashcode),gc分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳。这部分数据长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称为“mark word”,对象头数信息是与对象自身定义的数据无关的额外存储成本,考虑到空间效率,mark word被设计成非固定的数据结构以便在极小的时间存储尽量多的信息,他会根据对象的状态复用自己的存储空间,例如32位中25bit存储哈希吗,4bit存储分代年龄,2bit存储锁标志,1bit固定为0。

对象头的另一个部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有虚拟机实现都必须在对象数据上保留类型指针,查找对象的元数据不一定要通过对象本身。(元数据:用来描述数据的数据,描述代码与其他资源的内在联系的数据,这里的元数据就描述了这个对象是这个类的实例这种对应关系的数据。)另外,如果对象是一个数组,对象头中还要有一块记录数组长度的数据,因为jvm可以通过对象的元数据确定java对象的大小,但不能通过数组的元数据确定数组的大小(为什么?有大神解答的话在此谢过)。

对象的第二部分是实例数据:实例数据是对象真正存储的有效信息,也是在程序代码中定义的各种类型的字段(属性)内容,从父类继承下来或者自定义的属性都要记录。实例数据的存储顺序会受到虚拟机分配策略参数的影响(fieldsallocationstyle)和字段在代码中定义的顺序有关。默认的分配粗略是等宽的字段(属性)分配在一起,如:long/doubles,short/char,等父类定义的变量出现在自雷定义的变量之前,如果compactfields参数值为true,默认为true,那么子类的较窄变量也可能插入到父类变量空隙之间。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值