深入理解java虚拟机阅读笔记一

版权声明: https://blog.csdn.net/bsfz_2018/article/details/80652453


2.1 java内存区域与内存溢出异常

在虚拟机自动内存管理机制的帮助下,不再需要为每一个new出来的对象去写配对的delete/free代码,而C/C++开发人员则既拥有每一个对象的所有权,还需要维护每个对象生命开始到终结

java虚拟机在执行java程序的时候将管理的内存分为如下几个运行时数据区域

  • 方法区 method area
  • 虚拟机栈 vm stack
  • 本地方法栈 native method stack
  • 堆 heap
  • 程序计数器 program counter register

这些区域都有各自的作用,以及创建和销毁的时间

2.1.1 程序计数器

较小的内存区域,唯一没有outofmemoryerror的区域,可以看作是当前线程所执行的字节码的行号指示器,如果线程当前执行的是java方法,那么这个计数器记录的是正在执行的虚拟机字节码指令的地址

多核程序中,同一时间段一个内核只能处理一个线程的指令,为了线程切换结束后能正确恢复到执行位置,每个线程都有自己的一个独立的线程计数器,字节码解释器就是通过改变这个计数器的值来选择需要执行的字节码指令

2.1.2 虚拟机栈

是我们常说的栈和堆中的栈内存,为每个线程所私有,生命周期同于所属线程,每个方法在执行的时候会创建栈帧,栈帧中存储的是线程执行的局部变量表,指向堆内存地址的引用指针,指向一个代表对象的句柄

局部变量表所需的内存空间在编译期完成分配,进入一个方法时,这个方法在栈帧中分配多大局部变量空间是确定的,方法运行期间不会改变局部变量表大小

这个区域会发生两种异常:

stackoverflowerror:栈溢出异常:当线程请求的栈深度大于虚拟机允许的深度,抛出此异常

outofmemoryerror:内存溢出异常:大多数java虚拟机都可动态扩展,如果扩展时申请不到足够的内存抛出此异常

2.1.3 本地方法栈

和虚拟机栈差不多,本地方法栈执行的是虚拟机用到的native方法,虚拟机栈执行的是虚拟机用到的java方法,在sun的hotspot虚拟机中,两者合二为一,是一个东西,同样会抛出上述两种异常

2.1.4 堆

heap堆内存仅仅用于存储对象实例,被所有线程共享,是垃圾收集器管理的主要区域,因此又得名GC堆(Garbage collection heap)

从垃圾收集的角度看,垃圾收集最常用的算法是分代收集算法,因此java堆又分为新生代,老年代,再细致一点有Eden,from survivor,to survivor空间等

2.1.5 方法区

non-heap:非堆,被所有线程共享,存放类,常量,静态变量,即时编译器编译后的代码等内容,可能抛出内存溢出异常在方法区无法满足内存分配需求时

2.1.5.1 运行时常量池 runtime constant pool

方法区中的一部分,类中除了有类的版本,字段,方法等描述信息外,还有常量池信息,存放编译器产生的各种字面量和符号引用,这部分内容将在类加载后进入方法区中的运行时常量池中保存

运行时常量池相比于类Class文件常量池的一大区别是具有动态性:java不要求常量只有编译器才产生,即并非只有预置入class文件中常量池的 内容才能进入运行时常量池,运行期间也可能将新常量放入池中

2.1.6 直接内存

不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存被大量使用,可能导致内存溢出异常

JDK1.4引入NIO(new input /output) ,是一种基于通道channel和缓冲区buffer的I/O方式,可以使用本地函数库直接操作堆外内存,通过堆中存在的一个直接字节缓冲direct byte buffer对象对这块内存进行操作,避免了在java堆和本地native堆中来回数据复制

2.2

2.2.1 虚拟机中,对象(不包括数组和Class对象)的创建过程:

读到一条new指令,会先去常量池中定位一个类的符号引用,并检查符号引用代表的类是否已经被加载,解析和初始化,如果没有,就先执行对应类加载过程

类加载完毕,为新生对象分配内存,这个对象所需要的内存空间在类加载的时候已经确定,分配内存的意思就是把一块确定大小的内存从java堆划分出来,根据堆中内存是否规整,内存的分配方式为2种

  • 指针碰撞 Bump the Pointer
  •       堆内存完全规整,所有用过的内存在一边,空闲内存一边,中间放置指针,开辟空间即指针向空闲内存移动开辟与对象大小相等位移
  • 空闲列表 Free List
  •     堆内存不规整,用过的内存和空闲内存交叉,则不能使用指针碰撞,虚拟机需要维护一个列表,列表中记录哪些内存是可用的,分配的时候从列表中找到足够的空间分给对象实例,并更新列表中记录

而java堆内存是否规整又取决于采用的垃圾收集器是否具有压缩整理功能

带压缩整理功能的收集器如serial[ 连续 ]的,ParNew[ 新式 ]等,带有compact过程,系统采用的分配算法是指针碰撞

而使用CMS之类的基于mark-sweep[ 标记清理 ]算法的收集器,系统采用空闲列表方式

2.2.2 虚拟机中创建对象是非常频繁的行为,如何保证线程安全?

并发条件下,给对象A分配内存,指针没来的及修改,对象B同时使用这个指针来分配内存。解决方式两种:

  • 分配动作同步:虚拟机采用CAS结合失败重试保证更新操作原子性
  • 本地线程分配缓冲TLAB:每个线程在java堆中预先开辟一块内存,称为Thread Local Allocating Buffer,哪个线程要分配内存就先在该线程的TLAB上分配,TLAB用完以后分配新的TLAB的时候,才进行同步锁定

内存分配结束,为了保证对象实例字段不赋初始值就可以直接使用,虚拟机需要将分配到的内存空间初始化为零值

2.3 对象的内存布局

HotSpot虚拟机中,对象在内存中存储的布局分为三部分:对象头,实例数据instance data,对齐填充padding

对象头包括两部分信息

  • 存储对象自身运行时数据,如哈希码hashCode,GC分代年龄,锁状态标记,线程持有的锁,/偏向线程id,偏向时间戳等/,这部分数据长度和虚拟机的位数相同,32位虚拟机则这部分数据长度为32bit,64位则为64位
  • 另一部分信息是类型指针,即对象指向生产他的类的指针,虚拟机根据这个指针确定对象属于哪个类实例,需要注意的是,不是所有的虚拟机都必须在对象数据上保存类型指针

实例数据部分是对象真正存储的有效信息,即程序代码中定义的各种字段,无论继承自父类还是子类自身定义的都需要记录,这部分的存储顺序依照虚拟机的字段分配策略FieldsAllocationStyle和字段在java源码中定义顺序来决定。分配策略规定,相同宽度的字段总是被分配在一起,在这之上,父类中定义的变量会出现在子类之前

2.4 对象的访问定位

创建对象为了使用对象,虚拟机规范规定了虚拟机栈中存放了堆内存对象的引用,但没有规定如何定位并访问到堆中这个对象的具体位置,因此对象访问方式主要由虚拟机的具体实现决定,目前主流访问方式有下面两种:

  • 句柄:在堆中单独划分出一块内存作为句柄池,存储堆内存中对象的位置和对象所属的类型数据的位置,即两个指针,一个指向对象实例数据的指针,一个指向对象类型数据的指针

reference中存储的是对象句柄池地址

优点:reference存储的是稳定的地址,如果对象被移动,只改变句柄中的指针,而reference 不需要修改

  • 直接指针:reference中存储的直接就是对象的地址,因此需要考虑到如何放置访问数据的相关信息

优点:速度快,节省了一次指针定位时间开销,hotSpot采用这种方式进行对象访问

猜你喜欢

转载自blog.csdn.net/bsfz_2018/article/details/80652453