深入理解JVM内存模型和掌握处理JVM内存问题已经是java程序员必不可少基础技能之一。
JVM内存模型
java内存区域主要分为线程私有区域(程序技术器、java虚拟机栈、本地方法栈),线程共享区域(方法区、实例堆(java堆))和直接内存。
线程私有区域生命周期与线程相同,依赖用户线程的创建/销毁。
线程共享区域随jvm的启动/关闭而创建。
直接内存不属于JVM运行时数据区的一部分,但是也会频繁使用。NIO提供了基于Channel和Buffer的IO方式,可以使用Native函数库直接操作堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用进行操作。
程序计数器(线程私有)
较小一块内存区域,当前线程所执行字节码的行号指示器,每个线程都有一个独立的计数器。正在执行java方法,计数器记录的是jvm当前执行指令的地址,如果是执行Native,则计数器为空。该区域是jvm唯一没有任何OutOfMemeryError情况的区域。
虚拟机栈(线程私有)
描述java方法执行的内存模型,生命周期与线程相同。每个方式在执行同时会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等。每个方法从调用开始到执行完成都对应一个栈帧的入栈和出栈。
栈帧是用来存储数据和部分结果的数据结构,同时也用来处理动态链接、方法返回值和异常分派等。栈帧随着方法调用而创建,方法结束而销毁。
本地方法栈(线程私有)
和虚拟机栈类似,区别是虚拟机栈是为java方法服务,而本地方法栈是为Native方法服务。
堆(线程共享)
虚拟机所管理的内存中最大的一块,唯一目的就是存放对象实例,几乎所有对象实例都是在堆分配内存。也是垃圾回收的主要区域。现代JVM采用分带收集法,因此java堆从Gc的角度可以分为(新生代(Egen区、From Survivior 和To Survivor)和老年代)。
方法区(线程共享)
是我们常说的永久代,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。
运行时常量池
运行时常量池也属于方法区一部分,Class文件除了类版本、字段、方法、接口等信息外,还有一项就是运行时常量池,用于存放编译期生成的字面量和符号引用。
内存中对象分配、布局和访问
对象创建
1 类加载检查
检查是否在常量池中定位到类的符号引用,检查类是否已被加载、解析和初始化过。如果没有加载需要先执行类加载过程。
2 对象内存分配
类加载检查通过后,虚拟机将为对象分配内存,类加载完后便可以确认对象所需要内存大小。
3 内存空间初始化为零值
4 对象初始化
对象内存布局
对象头
对象头包含两部分信息,一是用于存储对象自身运行时数据(哈希码、GC分代年龄、锁标志、线程持有锁、偏向线程ID、偏向时间戳等);另一部分是对象类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确认对象是那个类的实例。
实例数据
存储对象有效信息,即代码中所定义的各种类型字段内容
对齐填充
对齐填充不是必要的,没特别含义,仅起到占位的作用。
对象访问
java程序通过虚拟机栈上的reference(对象引用)数据来操作堆上的对象实例,对象访问方法取决于虚拟机的实现,目前主流对象访问方法有两种:通过句柄访问对象和通过直接指针访问对象。
句柄访问对象:
直接指针访问对象:
句柄方式访问的好处就是referencec存储的是稳定的句柄地址,对象被移动只会改变句柄中的实例数据指针,reference本身不改变。直接指针方式获取的好处就是速度更快,节省了一次指针定位的开销。
JVM内存异常分析
Java堆溢出
堆内存分配及参数设置
根据Gc的角度可以分为(新生代(Eden区、From Survivior 和To Survivor)和老年代),其中java堆空间=新生代(1/3) + 老年代(2/3),新生代空间=Eden区(8/10) + From Survivor(1/10)+ To Survivor(1/10)。
可以通过参数-Xmx(堆最大容量,默认物理内存1/4)、-Xms(堆初始容量 ,默认物理内存1/64)来指定堆的大小,如:-Xms128m -Xmx1003m。
其他堆内存分配参数说明
-Xmn 通常为-Xmx 的1/3或1/4
-XX:NewRatio =2 新生代和老年代比例 ,-XX:NewRatio =2表示新生代占堆空间1/3,老年代占堆空间(2/3)
-XX:SurvivorRatio=8 新生代Eden区比例,默认为8
-XX:PrintGCDetails 打印Gc信息
Java堆溢出
当堆内存不足时,会抛出OutOfMemoryError,异常堆栈信息“java.lang.OutOfMemoryError:Java heap space”
基本解决思路:1 通过内存映像分析工具(如Eclipse Memory Analyzer Tool)对堆转储快照Dump进行分析,2 定位内存OOM异常是内存泄漏还是内存溢出。3 如果是内存泄漏,可以进一步通过工具查看泄漏对象GC Roots的引用链,找到泄漏对象通过怎样的路径与GC Roots相关联导致垃圾收集器无法自动回收,根据对象类型和引用链定位内存泄漏的代码。4 如果不是内存泄漏,则