Java内存模型解析
基于HotSpot虚拟机
运行时数据区域
线程私有
- 程序计数器:相当于当前线程所执行字节码的行号指示器
- java虚拟机栈:局部变量表部分,存放编译期可知的各种基本数据类型,其中64位的long和double占用2个Slot,其他占用1个Slot
- 本地方法栈:类似于java虚拟机栈,但本地方法栈为本地native方法服务
线程共享
- java堆,被所有线程共享,在虚拟机启动时创建
- 方法区,用于存放类信息、常量、静态变量,属于虚拟机的一个逻辑部分,但有个别名叫非堆(Non-Heap)
- 直接内存,NIO可以使用Native函数直接分配堆外内存
Sun HotSpot虚拟机使用永久代来实现方法区,因永久代有 -XX:MaxPermSize的上限, jdk 1.7以后,已经把原本放在方法区(永久代)中的字符串常量池移出
关于String.intern方法在JDK1.6和1.7中使用方法区内存方面的差异
String.intern是一个Native方法,如果字符串常量池中已包含一个等于此String对象的字符串,则返回常量池中代表这个字符串的String对象,否则,将此String对象包含的字符串添加到常量池中,并且返回String对象的引用。
在JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,而JDK1.7中不再复制实例,只是在常量池中记录首次出现的实例引用,对内存占用有较大减少。
垃圾收集主要针对java堆,基本都采用分代收集算法。
- 新生代
- 老年代
还可以细分为:
- Eden空间
- From Survivor空间
- To Survivor空间
在java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB), 主流虚拟机在分配java堆内存空间时,都可以进行扩展,通过 -Xmx 和 -Xms控制,如果空间不够时,则抛出OutOfMemoryError。
Java堆中对象分配、布局和访问
对象分配
从虚拟机的视角来看new一个java对象的过程:
- 检查类对象是否在常量池中存在这个类对象的符号引用
- 如果这个符号引用代表的类还没有加载、解析和初始化过,需要先执行相应的类加载过程
- 为对象分配内存,两种方式:指针碰撞(Serial、ParNew等带Compact过程的收集器)、空闲列表(CMS这种基于Mark-Sweep算法的收集器)
- 对对象进行必要的设置,包括是哪个类的实例、对象的哈希值、GC分代年龄,这些信息存放在对象头(Object Head)中
内存分配并发问题的两种解决方案:
- 对分配内存的动作进行同步处理,保证更新操作的原子性
- 按照线程划分不同的空间进行,TLAB,只有TLAB空间用完时,才需要同步锁定,可通过参数:-XX:+/-UseTLAB
对象内存布局
分三块区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象头存储两部分数据,第一部分是对象自身的运行时数据,例如:哈希值、GC分代年龄、锁状态标志、线程持有的锁等。另一部分是类型指针,虚拟机通过这个指针来确定属于哪个类的实例。 实例数据存储程序代码中定义的各种字段内容。 对齐填充区域用于补全对象的起始地址是8字节的整数倍。
对象访问
java程序通过栈上的引用指针来操作堆上的具体对象,具体如何访问取决于虚拟机的实现方式,主流有两种:
- 使用句柄
- 直接指针
两种方式各有优劣,简单来说,句柄方式在reference中存储的是稳定的句柄地址,但需要两次寻址以获得对象实例数据和类型数据。直接指针节省了一次指针定位的时间开销。
Sun HotSpot使用直接指针来访问数据。
内存分配相关异常
Java堆内存问题:OutOfMemoryError
如果出现了OutOfMemoryError的异常,需要确定是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
内存泄漏的意思是说GC无法对对象进行自动回收,如果是溢出,就是说内存中的对象还必须存活但空间不够了
内存泄漏需要检查对象是通过怎样的路径和GC Roots相关联,内存溢出一般需要检查虚拟机堆参数:-Xms/-Xmx和物理内存的关系,以及从代码层面检查某些对象的生命周期。
虚拟机栈内存问题:StackOverflowError和OutOfMemoryError
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
栈容量由-Xss参数设定,-Xoss设置本地方法栈大小在HotSpot虚拟机中无效
虚拟机可用栈内存容量计算公式
进程最大内存容量 - 最大堆容量(Xmx)- 最大方法区容量(MaxPermSize)= 虚拟机栈内存总容量
根据以上公式,如果每个线程所分配的栈容量越大,则可建立的线程数量就会减少。在开发多线程应用时,如果出现内存溢出,在不能减少线程数或者增加物理内存的情况下,可以考虑减少最大堆和栈容量来换取更多的线程数。