Java虚拟机笔记--《深入理解Java虚拟机》-周志明著

Java虚拟机

Java内存区域和内存溢出异常

Java虚拟机会在Java程序运行的时候,将它管理的内存划分为几个区域

在这里插入图片描述

  1. 程序计数器
     程序计数器占据较小的内存区域,它可以看作当前线程所执行的字节码的行号指示器。为了保证CPU执行权从A线程跳转到B线程后,返回A时,A线程能够继续执行之前的代码,所以A,B两个线程都具有自己的程序计数器,所以程序计数器具有线程隔离性。
     同时,我们把这类内存区域称为“线程私有”的内存
     如果线程执行一个Java方法,程序计数器中存放的就是正在执行的虚拟机字节码指令的地址
     如果线程执行一个本地方法,程序计数器中为空
     继续计数器是《Java虚拟机规范》中唯一没有规定任何OutOfMemoryError情况的区域

  2. 方法区
     该区域线程共享,存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。

    2.1.运行时常量池
     该部分是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用。
     运行时常量池具备动态性,就是运行期间也可以将新得常量放入池中,例如String的intern()。
     当常量池无法申请到内存时会抛出OutOfMemoryError

  3. 虚拟机栈
     该部分是线程私有的。虚拟机栈描述的是Java方法执行的线程内存模型。
     每一个方法被执行的时候,Java虚拟机会创建一个栈帧用户存储局部变量表,操作数帧,动态链接,方法出口等信息。每一个方法从调用到执行完毕,对印着一个栈帧在虚拟机栈中入栈到出栈的过程。
     局部变量表中存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte…),对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)
    这个区域存在两类异常情况:如果线程请求到的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError

  4. 本地方法栈
     与虚拟机栈作用相似,区别在于虚拟机栈执行Java方法服务,而本地方法栈则是虚拟机使用到的本地方法服务


  5.  Java堆是被线程共享的一块内存,在虚拟机启动时创建,唯一目的就是存放对象实例。“几乎”所有的对象实例在这里分配内存。
     新生代,老年代,eden空间登名词,仅仅是一部分垃圾收集器的共同特性或者说是设计风格。
     Java堆可以处于物理上不连续的内存空间中,但在逻辑上是连续的。
     Java堆可以是固定的,也可以是扩展的。(通过参数-Xmx或者-Xms设定)。

6.直接内存
 这部分不属于运行时内存区域,但是会被频繁使用,会出现OutOfMemoryError异常。

HotSpot虚拟机

介绍下HotSpot虚拟机在Java堆中对象的分配,布局和访问

  1. 对象的创建
     虚拟机遇到new指令时,会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过,如果没有,则必须先执行相应的类加载。
     类加载检查过后,虚拟机会为新对象分配内存,有两种情况。如果Java堆的内存时规整的,即已分配的内存在一边,未分配的内存在另一边,中间放着一个指针所谓分界点的指示器。所谓分配内存仅需要让指针朝未分配内存那边移动和需要分配给新对象所需内存大小的距离。这种分配方式被称为“指针碰撞”。如果Java堆种已分配内存和未分配内存是不规整的,那么虚拟机需要维护一张表,记录着未分配内存,在分配内存时找到一块足够的内存分配给对象。这种分配方式被称为“空闲列表”。Java堆是否规整取决于垃圾收集器是否带有空间压缩整理的能力。
     对象分配修改指针位置,而这个操作又是十分频繁的,在线程并发的情况发可能会线程不安全。两个对象同时需要分配内存,在給A分配内存时还未修改指针,这时B也修改这个指针来分配内存。解决这种问题有两种方法。一是对分配内存的动作进行同步处理,虚拟机采用的CAS配上失败重试保证了更新操作的原子性。二是把内存分配的动作按照线程划分在不同的空间进行,即每个线程在Java堆种预先分配一小块内存,称为本地线程分配缓冲(TLAB)。虚拟机是否使用TLAB,通过-XX:+/UseTLAB设定。
     对象内存分配好后,虚拟机为分配到的空间(不包括对象头)赋值为初值,然后对对象进行必要的设置,例如这个对象是哪个类的实例,如果才能找到类的元数据信息,对象的哈希码。

  2. 对象的内存布局
     对象在内存种的存储布局可以划分为三个部分:对象头,实例数据和对齐填充

     2.1: 对象头包括两类信息,一类是用于存储对象自身运行时的数据,例如hash码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。另一类数据是类型指针,即对象指向它的类型元数据的指针。Java虚拟机通过这个指针来确定对象是哪个类的实例。但并不是所有的虚拟机实现都必须在对象数据上保留类型指针。若对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。

     2.2: 实例数据是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容。
     2.3 对齐填充,不是必然的。起着占位符的作用,应为HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍。

  3. 对象的访问定位
     Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式是由虚拟机实现而定的。主流的两种方式主要为使用句柄和直接指针。
     使用句柄访问时,Java堆中将划分出一块内存作为句柄池。reference中存储的就是对象的句柄地址。
     使用直接指针时,reference存储的就是对象地址。

在这里插入图片描述

使用句柄访问最大的好处就是reference中存储稳定的句柄地址,对象被移动时只会改变句柄的实例数据指针,使用直接指针来访问的最大好处就是速度更快

OutOfMemoryError异常

  1. Java堆溢出
    通过参数-XX:+HeapDumpOnOutOfMemeroyError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析
    处理该异常常规方法是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出转储快照进行分析,确认是内存泄漏还是内存溢出。
    如果是内存泄漏,就需要查看GC Roots的引用链,找到垃圾回收器无法回收对象的原因。
    如果是内存溢出,那就是看内存中的对象是否是必须的,检查Java堆虚拟机的堆参数(-Xmx与-Xms)设置,与内存相比,是否存在往上调整的空间。再从代码上检查是否存在某些对象的生命周期过长,持有状态时间过长,存储结构设计不合理等。

  2. 虚拟机栈和本地方法栈溢出
    HotSpot虚拟机不支持栈扩展,所以只会再创建线程时申请内存而无法获得足够的内存而出现OutOfMemoryError异常。如果是建立太多的线程导致内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程

  3. 方法区和运行时常量池溢出
    在JDK8以后,元空间就代替了永久代,一般就不会出现方法区的溢出异常了。
    不过HotSpot还是提供了一些参数作为元空间的防御措施
    –XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制
    –XX:MetaspaceSize:指定元空间的初始大小,以字节为单位
    –XX:MetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。

  4. 本机直接内存溢出
    直接内存的容量大小可通过–XX:MaxDirectMemorySize参数来指定。如果不指定,默认与Java堆最大值一致。
    由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么异常情况。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值