jvm内存结构
运行时数据区域
java7
java8将方法区移出,变为元数据区,放在直接内存中。

程序计数器
程序计数器是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。
在java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器完成。
每条线程都有自己独立的程序计数器,这类内存区域称为线程私有的内存。
此区域是唯一一个《java虚拟机规范》没有规定任何oom情况的区域
java虚拟机栈
java虚拟机栈也是线程私有的,他的生命周期和线程相同。
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法调用到执行完成的过程,对应一个栈帧在虚拟机中从入栈到出栈的过程。
局部变量表:存放了编译器可知的各种基本数据类型(long和double占据两个局部变量空间(Slot),boolean、byte等占据1个)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向另一个代表对象的句柄或其他次对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
局部变量表所需的内存空间在编译期完成分配,不会发生变化。

本地方法栈
本地方法栈和虚拟机方法栈一样,区别是本地方法栈调用的是Native的方法和服务。(hotSpot虚拟机直接将本地方法栈和虚拟机方法栈合二为一)
java堆
Java堆(Heap)是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。几乎所有Java对象实例都在堆上分配。
java堆是垃圾收集器管理的主要区域,也被称为GC堆。
堆还可细致分为:
- 老年代
- 新生代
- Eden空间(伊甸园)
- From Survivor 空间
- To Survivor 空间
线程共享的java堆中可能划分出多个线程私有的分配缓存区(Thread local Allocation buffer ,TLAB)
-Xmx 和-Xms 分别表示堆的最大内存和初始化内存。(rawdata线上 -Xms4g -Xmx4g ,测试-Xms800m -Xmx800m)
方法区
方法区和java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(jdk6)。
在hotspot虚拟机上,也被称为永久代,其他虚拟机没有永久代的概念。
JDK6开始,hotspot团队就有放弃永久代,逐渐改用本地内存来实现方法区的计划。
到了JDK7,hotspot已经把原本放在永久代中的字符串常量池、静态变量等移出到本地内存中。
到了JDK8,放弃了永久代的概念,在本地内存中实现元空间(MetaSpace)来代替,把JDK7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
运行时常量池
运行时常量池是方法区的一部分。(jdk8以及之前)
运行期间也可以把常量放入常量池中,比如String类的intern()方法
直接内存
直接内存并不是虚拟机运行时数据的一部分。
NIO可以使用Native函数库直接分配堆外内存,一些场景下能提高性能,避免Java堆和Native堆中来回复制数据。
对象在虚拟机中的实现(HotSpot)
对象的创建
当虚拟机遇到一条new命令,做的流程如下:
- 检查 检查这个类是否已经被加载、解析、初始化过。
- 为新生对象分配内存。
- 将分配好的内存空间初始化为零值。
- 对对象头进行设置。(哪个类的实例、如何找到类的元数据信息、对象的哈希码、GC分代年龄信息等)
- 执行
方法,把对象按程序员的意愿进行初始化。
分配内存的两种方式:
- 指针碰撞 :假设java堆内存规整,用过的空间都在一边,把指针向空闲空间那边挪动一段与对象大小相等的距离。
- 空闲列表:java堆不规整,已使用和未使用的空间交错,虚拟机维护一个列表,记录哪些内存块是可用的,分配内存的时候从列表上找一块足够大的空间给对象示例,并更新列表。
java堆是否规整是由垃圾收集器是否带有压缩整理功能决定的,使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,使用CMS这种基于Mark-Sweep(标记-清除)的收集器时,采用空闲列表。
解决并发情况下的内存分配有两种方案
- 虚拟机采用CAS配上失败重试的方式保证更新的原子性。
- 把内存分配动作,按照线程划分在不同的空间中进行,即每个线程在java堆上预先分配一小块内存,称为本地线程分配缓冲(Thread local Allocation buffer ,TLAB)。只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB可以通过-XX:+/-UseTLAB参数设定。
对象的内存模型
对象内存布局分三块区域:
- 对象头
- 实例数据
- 对齐填充(不是必然存在,hotspot要求对象起始地址必须是8字节的整数倍,所以需要对齐)。
对象头中有两部分数据:
- Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机中分别为32个和64个Bits。考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。

- 类型指针:对象指向它的类元数据的指针。

对象的访问定位
主流两种方式:
- 句柄 :优点是对象被移动时,只会改变句柄中的示例数据指针,而reference本身不需要修改。
- 直接指针:优点是速度快,节省了一次指针定位的时间开销。(HotSpot是采用直接指针的方式进行对象访问的。)


OOM类型
java堆溢出
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemmoryError
-Xms20m -Xmx20m 限制java堆不可扩展
-XX:+HeapDumpOnOutOfMemmoryError 参数可以让虚拟机在出现内存溢出异常时,Dump当前的内存堆转储快照,以便之后分析。
解决java堆溢出异常一般需要对dump出来的快照进行分析,判断是哪里出现内存泄漏还是内存溢出。(常用工具 Eclipse Memory Analyzer JDK自带的jvisualvm可以分析dump出来的文件)
如果是内存泄漏,可进一步用工具查看泄漏对象的GC roots的引用链,找到为什么没有被回收的原因,确定泄漏代码的位置。
如果不存在内存泄漏的话,应该检查虚拟机堆参数是否可以调大,从代码上检查是否存在某些对象生命周期过长、持有时间过长的情况。
栈溢出
虚拟机栈和本地方法栈溢出
对于HotSpot来说,-Xoss参数(设置本地方法栈大小)存在,实际上是无效的。栈容量只由-Xss参数设定。关于栈异常,有两种:
StackOverflowError异常:线程请求栈深度大于虚拟机所允许的深度。
OutOfMemoryError异常:虚拟机扩展栈时无法申请到足够空间。(一般由于线程过多,物理内存已没有足够的可用内存分配给JVM的栈使用。)
每当java程序启动一个新的线程时,java虚拟机会为他分配一个栈。
建立线程过多,可能会导内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量换取更多线程。
方法区和运行时常量池溢出
String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此string对象的字符串,则返回代表池中这个字符串的String对象;否则,将此string对象添加到常量池中,并返回此string对象的应用。
1 | public class StringInternTest { |
JDK1.6 返回两个false,JDK1.7以后返回一个true和一个false。
1.6中,intern()方法会把首次遇到的字符串示例复制到永久代中,返回的也是永久代中的这个字符串实例的引用。
1.7之后,intern()的实现不会再复制实例,只是常量池中记录首次出现的示例引用,“java”默认已经在常量池中。
方法区中主要记录Class的相关信息,可以通过CGlib产生大量类填满它。
当前很多主流框架,Spring Hibernate、Mybaits,在对类进行增强时,都会使用CGlib这类字节码技术,增强类越多,就需要的方法区来保证动态生成的Class可以加载如内存。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。
常见常见如下
- CGlib动态产生类
- 大量JSP或动态产出JSP文件的应用(JSP第一次运行需要编译为java类)
- 基于OSGi的应用(同一个类文件,被不同加载器加载,rawdata插件模式也是如此)
直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如不指定,默认和-Xmx一样。
由DirectMemory 导致的内存溢出,一个很明显的特质是Heap Dump文件不会看到明显的异常,如果发现oom后Dump文件很小,而程序同时使用了NIO,可以考虑这方面问题。