深入理解jvm笔记(一)

深入了解java虚拟机笔记(一)

第一章 走进java

第一章主要介绍了java的发展史和JDK编译。java发展史粗略看了下,了解一下即可。至于JDK编译,暂且跳过,等有时间,有需要去阅读JDK源码时在去实操。

第二章 java内存区域与内存溢出异常

2.1 概述

在内存管理领域,c++开发人员即可以拥有一个对象的“所有权”,又需要肩负对每个对象从开始到结束的维护责任。而对于java开发人员来说,java虚拟机就是个boss级别的存在,拥有控制内存的权力,不需要为每个new操作去delete/free。但是一但遇到问题,不了解jvm,debug就会很艰难。

2.2 内存区域

深入理解jvm笔记(一)

2.2.1 程序计数器

概念:程序计数器是当前线程所执行的字节码的行号指示器

特性:

  • 分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖程序计数器,是程序控制流的指示器
  • 线程私有。各线程间的计数器互不影响,独立存储
  • 多线程运行时通过计数器恢复到正确的执行位置

异常:在执行Java方法时,计数器记录的是正在执行的是虚拟机字节码指令的地址;如果执行的是一个本地(Native)方法,计数器则为空(Undefined)。这个内存区域是唯一一个没有规定任何OutOfMemoryEttor情况的区域。

2.2.2 Java虚拟机栈

概念和存储:虚拟机描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口等信息,每个方法被调用直至执行完毕,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,即先进后出,调用递归的原理基于此。

特性:

  • 线程私有,生命周期与线程相同

对于Java虚拟机栈,主要关心的就是其局部变量表的存储,在我们定义一个变量的时候,经常会被问到,这个变量到底被存储在哪里。而局部变量表存放了各种Java虚拟机基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型,一个指向对象的指针或者是一个句柄,不是对象本身)和returnAddress类型(指向了一条字节码指令的地址)。

局部变量表在编译期就已经完全确定好大小了,在方法运行期间不会改变局部变量表的大小。同时,对于64位长度的long和double类型的数据会占用两个变量槽(Slot),其余数据类型只占用一个。

异常:在Java虚拟机规范中,对这个内存规定了两类异常状况:*Error和ouOutOfMemoryError异常。

2.2.3 本地方法栈

本地方法栈和Java虚拟机栈类似,唯一区别是本地方法栈是用来执行Native方法的。而有些虚拟机(Hot-Spot虚拟机)将Java虚拟机栈和本地方法栈合二为一。

2.2.4 Java堆

概念:Java堆是虚拟机所管理的内存中最大的一块,被所有线程共享,此内存的唯一目的就是存放对象实例。Java堆还是垃圾收集器管理的内存区域。

Java虚拟机规范中描述:所有的对象实例以及数组都应当在堆上分配。

特性:

  • 从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区。
  • Java堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。
  • Java堆既可以被实现成固定大小,也可以进行扩展。

ps:将Java堆进行细分是为了更好地回收内存,更快地分配内存。

异常:在Java虚拟机规范中,对这个内存规定了ouOutOfMemoryError异常。

2.2.5 方法区

概念:方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。

在JDK7的时候,把原本放在永久代(不完全等价方法区)的字符串常量池,静态变量等移出;在JDK8的时候,将永久代还剩余的内容(主要是类型信息)全部移动到元空间。

特性:

  • 方法区不需要连续的内存和可以选择固定大小或可扩展,还可以选择不实现垃圾收集
  • 方法区内存回收目标主要是针对常量池的回收和对类型的卸载

异常:如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常

2.2.7 直接内存

概念:直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。

特性:

  • 在JDK1.4中引入了NIO类,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,能够显著提高性能,避免了在Java堆和Native堆中来回复制

异常:动态扩展时可能会出现OutOfMemoryError异常

2.3 HotSpot虚拟机对象探秘

2.3.1 对象的创建
  • 虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池里定位到一个类的符号引用。并检查这个类是否已经加载,解析和初始化过,如果没有,先执行类的加载过程。
  • 类加载检测通过后,jvm为新生对象分配内存,类加载完成后,对象的内存大小就定下来了。为对象分配空间等同与把一块确定的内存从java堆中分出来。
  • 假设堆内存是规整的,有用的空闲的内存中间放一个指针当分界点,如果指针挪动,则可为对象分配空间,这种方式叫指针碰撞。
  • 假设堆内存是不规整的,jvm需要维护一个空闲列表来记录哪块内存是可用的,这种方式叫空闲列表。
  • 线程安全的解决方法:1.对分配内存空间的动作进行同步处理。2.每个线程在Java堆中预先分配一块小内存,称为本地线程分配缓冲(TLAB)。当TLAB用完后,才需要同步锁。配置方法:-XX:+/-UseTLAB。
  • 内存分配完之后,JVM将分配的内存空间初始化为0(但不包括对象头),如果使用TLAB,这一步可提前到TLAB分配时进行,这一步保证字段有初始值。
  • 接下来,JVM对对象进行必要的设置,如这个对象是哪个类的实例,hashcode等,这些都是放在对象头中。
  • 执行new之后会继续执行方法,按照代码进行初始化。
2.3.2 对象的内存布局
  • 对象分为三块,头(header),实例数据(instance data),对齐补充(padding)
  • header包括两部分信息。一部分存储对象运行时数据如hashcode,GC分代年龄,锁的标志,线程持有的锁,偏向线程ID,偏向时间戳等。这部分是Mark Word,Mark Word的数据结构是不固定的。另外一部分是类型指针,如果是数组,还要记录数组的长度。
  • 实例数据存储顺序受分配策略参数和字段定义的顺序的影响,并且相同宽度的字段会被排到一起。父类的变量会被安排到子类之前。如果CompactFields设为true。则子类的较窄的变量会插入到父类变量的空隙中。
  • 对齐补充是保证对象初始地址必须是8字节的整数倍。
2.3.3 对象的访问定位
  • 通过栈上的reference来操作堆上的对象。
  • 句柄访问。Java堆中会分出一块内存作为句柄池,在reference中存储对象的句柄地址。对象被移动的时候不用改reference。
  • 直接指针访问。reference直接存储对象地址。节省了一次指针定位的时间开销。

2.4 OOM异常

2.4.1 Java堆溢出
  • 不断创建对象,超过容量限制。
  • 将堆的最小值-Xms与最大值-Xmx参数设置为一样可避免自动扩展。
  • 用参数-XX:+HeapDumpOnOutOf-MemoryError。可以让jvm在出现内存溢出的时候Dump出当前的内存堆转储快照以便分析。
  • 错误OutOfMemoryError:Java heap space
  • 如果是内存泄漏,通过工具集检查泄露对象到GC Roots的引用链,检查为什么无法回收。
  • 如果内存中的对象确实都还活着。查看某些对象是不是生命周期过长,持有时间过长等。
2.4.2 虚拟机栈和本地方法栈溢出
  • HotSpot虚拟机不区分虚拟机栈和本地方法栈。
  • 栈容量由-Xss参数决定。
  • 栈的深度过大,将抛出*Error异常。内存不足,抛出OutMemoryError异常。
  • 单线程下,这两种情况一般抛出的都是*Error异常。
  • 操作系统分配给每个进程的内存是有限的,虚拟机栈+本地方法栈=2GB-Xmx-MaxPermSize。
2.4.3 方法区和运行时常量池溢出
  • String:intern()是一个本地方法,如果字符串常量池中已经存在了,则返回池中这个对象的引用;否则,将此值放在常量池中,并返回这个String对象的引用。在JDK7的intern方法不会复制实例。
  • 方法区存放的主要是class的相关信息,如类名,字段描述,访问修饰符,常量池等等。
2.4.4 本机内存直接溢出
  • Direct Memory的容量大小通过-XX:MaxDirectMemorySize参数指定,默认与-Xmx一致。
  • 用DirectByteBuffer类分配内存抛出内存溢出异常的时候并没有真正向操作系统申请分配内存,而是通过计算得知无法分配内存。
  • 真正申请分配内存的方法是Unsafe::allocateMemory()。
  • 由Direct Memory导致的内存溢出,明显特征就是在Heap Dump文件中不会有明显的异常情况。