深入理解JVM-笔记2-内存

235 阅读7分钟

JVM管理整个程序(一个进程)的内存, 将其划分为以下几个区域:

程序计数器(线程私有)

每个线程正在执行指令的位置:

  • java方法: 虚拟机字节码指令的地址
  • native方法: Undefined

唯一一个无OOM的区域

虚拟机栈(线程私有)

每个方法执行时创建一个栈帧, 包括:

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 方法出口

局部变量表

编译期间即完成分配, 运行期间不会改变槽的数量, 但槽的大小虚拟机控制其真正大小(32b,64b or more)

存放编译期可知的:

  • 基本数据类型
  • 对象引用(reference类型)
  • return Address类型(指向一条字节码指令的地址)

存储单位为槽(Slot), 除long,double占用2个, 其他均占用一个slot

本地方法栈(线程私有)

  • 虚拟机栈为JVM使用的java方法(字节码方法)服务;

  • 本地方法栈为JVM使用的Native方法(本地方法)服务.

两者非常相似, HotSpot实际上将虚拟机栈和本地方法栈合二为一

线程的栈容量通过 -Xss配置, -Xoss配置本地方法栈大小(HotSpot两栈合一, 此参数无效)

  • 如果线程请求的栈深度(方法数目)大于JVM允许的深度, 就会报StackOverFlowError
  • 线程在申请栈空间时如果太大申请失败, 会报OutOfMemoryError, 如果JVM允许动态扩展栈空间(ClassicVM可,HotSpot不可), 则扩展时无法申请到足够空间也会报OOMError

HotSpot虚拟机通常需要设置堆内存和方法区内存大小, 在操作系统和物理内存的限制下, 剩下的内存即为分配给线程的私有内存, 这部分内存越大, 每个线程分配的内存空间越大, 可运行的线程越少, 因此线程数非常多的情况下, 容易无法分配足够栈内存给线程, 报OutOfMemoryError, 这时候应该将-Xss设置得更小

Java堆(线程共享)

JVM启动时创建, 几乎所以对象都存放在这里

其为线程私有, 但有线程私有的分配缓冲区(TLAB:thread local allocation buffer), 以提升对象分配时的效率

堆在物理上是不连续的内存区域, 但逻辑上是连续的物理空间

主流的堆分配都是可扩展的(-Xms, -Xmx), 如果堆中没有了足够内存完成实例分配, 且无法扩展了, 则会报OutOfMemoryError

这种情况, 可以通过-XX:+HeapDumpOnOutOfMemoryError参数打印的堆转储快照判断是什么问题:

  • 内存泄漏: 查看对象的GCRoots引用链, 判断是哪些对象没有及时释放
  • 内存溢出: 查看-Xms, -Xmx与机器内存对比, 看是否可向上调整; 检查代码是否合理

方法区(线程共享)

存放:

  • 类信息
  • 静态变量
  • 运行时常量池(指非编译期确定的存在Class文件中的常量)
    • 包含在编译期生成的字面量和符号引用

JDK8之前, HotSpot虚拟机使用永久区来实现方法区, 因为永久区有内存上限(-XX: MaxPermSize), 因此会很容易出现OutOfMemoryError

JDK7的HotSpot将字符串常量和静态变量移出永久区, 放到了堆中;

JDK8彻底消除永久代, 采用与JRocket, J9类似的元空间(MetaSpace).

  • -XX:MaxMetasoaceSize 最大元空间,默认-1即只受限于内存大小;
  • -XX:MetaSpaceSize元空间初始空间大小, 达到即触发GC, 同时对值调整, 最大可提高到Max大小;
  • -XX:MinMetaspaceFreeRatio 控制GC后最小的元空间剩余容量百分比(避免经常GC)
  • -XX:MaxMetaspaceFreeRatio 与上相反

方法区的回收一般是针对:

  • 常量回收
  • 类的卸载

Class文件

包含以下内容:

  • 类的版本
  • 类的字段
  • 类的方法
  • 类的接口
  • 类的常量池表(编译期生成的各种字面量和符号引用)

直接内存

不是JVM的内存区域, 但NIO可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的DirectByteBuffer对象作为堆外内存的引用来操作(避免在Java堆和Native堆中来回复制)

受到机器总内存和CPU寻址空间的限制

设置-Xmx时需为直接内存留足够空间, 否则动态扩展时会OutOfMemoryError(直接内存也算在java程序进程的地址空间内吗????)

Unsafe是直接对内存进行操作的类, 只能由核心类库中的类使用, 但可以通过反射的方式调用. 其allocateMemory方法就是真正地向操作系统申请内存, 如果申请空间过大会报OOMError

而DirectByteBuffer报错时不会真正向操作系统申请, 而是计算发现内存不足就会报错

对象的创建时间

以HotSpotVM为例

  1. JVM遇到一条new的指令

  2. JVM检查常量池中是否有该类对应的符号引用. 如果有证明该类已经被加载,解析和初始化; 如果没有, 执行这个过程

  3. JVM为新的对象分配空间(对象所需的空间大小在编译期即可完全确定)

    并发情况下内存分配可能不安全:

    • 同步处理: CAS+失败重试

    • TLAB线程本地分配缓冲: 在堆中线程预先分配的区域(即内存中的不同区域), TLAB满后用同步处理分配新的TLAB

      是否使用TLAB用-XX: +/-UseTLAB设定

    内存区域是否规整:

    • 指针碰撞: Serial, ParNew等垃圾回收期带压缩整理过程, Java堆规整, 可以用指针碰撞直接分配

    • 空闲列表: Java堆不规整, JVM维护一个空闲列表, 如基于清除算法的CMS

  4. 分配内存完成后, JVM将分配的空间置0值(若使用TLAB, 则可能在TLAB分配时置0)

  5. JVM对对象进行设置, 即对象头中的信息:

    • 哪个类
    • 类的元数据信息地址
    • GC分代年龄
    • 锁的信息(是否启用偏向锁等)
  6. 如果new的执行后的字节码指令是invokespeacial, 则java程序的构造方法执行(即Class文件的<init>()方法) (一般编译器编译的new关键字后会有invokespeacial指令)

对象的内存布局

HotSpot对象的对象头:

  • MarkWord(动态定义的结构)(8字节的倍数)

    • 对象自身的运行信息:
      • hashcode
      • GC分代年龄
      • 锁状态标志 见java锁
      • 线程持有的锁 持有锁的线程
      • 偏向线程ID
      • 偏向时间戳 ?
    • 类型指针: 即对象指向它的类型元数据的指针
    • (如果对象是数组, 则还有数字长度数据(未指定长度的??))

    (未被同步锁锁定时, hashcode24bit, GC年龄4bit, 锁状态标志2bit)

  • 对象的有效信息, 即程序中定义的各种类型的字段(含父类字段)

    (存储顺序受到 -XX: FieldsAllocationStyle和源码中定义顺序影响

    HotSpot默认顺序是长的基本类型在前, 最后是oops=ordinary object pointers

    且父类变量在子类变量前, 若-XX: CompactFields为true, 则子类的小变量可插入父类变量空隙中)

  • 对齐填充: HotSpotVM中对象起始地址必须是8字节的整数倍

对象的访问定位

JVMt通过栈中存的reference来操作堆上的集体对象

reference可以通过以下两种方式实现:

  • 句柄

    java堆中分配一个句柄池, 栈中存储句柄地址, 句柄中包含:

    • 对象实例地址
    • 类型数据地址

    这种方式可以保证栈中存储的句柄地址稳定, GC时移动对象只需修改句柄池中对象实例数据的地址

  • 直接指针

    直接指向堆中对象实例的地址, 对象实例的MarkWord中含有类型指针, 指向方法区

    速度更快(访问次数多后提升明显)

HotSpot主要采用第二种, 直接指针