Qiuliang's Site

做一个独立思考和具备创新能力的人,打造谦逊和强大的内心

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)= 虚拟机栈内存总容量

根据以上公式,如果每个线程所分配的栈容量越大,则可建立的线程数量就会减少。在开发多线程应用时,如果出现内存溢出,在不能减少线程数或者增加物理内存的情况下,可以考虑减少最大堆和栈容量来换取更多的线程数。

参考