JVM(七)JVM运行时内存空间

932 阅读7分钟

JVM内存空间

JVM规范在程序运行期间定义了不同的数据区域.有一些区域跟随JVM的创建销毁.而有些区域则是线程独有的,线程独有的区域会跟随线程的创建与销毁.

在不同版本和不同厂商的JVM版本中,都会有较大差异. 本文基于JDK8,HotSpot虚拟机进行的总结

JVM规范内的运行时数据区域

程序计数器(The pc Register)

JVM支持多线程,每个线程都有自己的程序计数器.当线程执行中的时候,程序计数器会包含线程的执行点.(前提是方法不是native方法).

虚拟机栈(Java Virtual Machine Stacks)

在线程创建的同时,线程会拥有私有的虚拟机栈.(线程不共享).虚拟机栈中会储存栈帧(Frames).栈帧中会持有本地变量,部分结果,方法的调用和返回.虚拟机栈只会对栈进行压栈和出栈操作,栈帧可能由堆(heap)分配.虚拟机栈在内存中并不需要是连续的.JVM规范规定具体的虚拟机实现必须允许用户对虚拟机栈的大小进行调整

堆(Heap)

堆内存在JVM内线程共享的.堆内存为所有的类示例和数组进行内存分配.

堆内存创建于JVM的启动阶段.堆中的对象存储由垃圾收集器进行管理,对象从不会明确已经被回收.JVM不承担对象的管理,具体实现由各厂商根据系统需求实现.堆内存的大小是可用动态扩展的,并且堆内存不必是连续的.

方法区(Method Area)

方法区也是JVM中的线程共享的.存储的结构,例如运行时常量池,字段和方法数据,和结构方法.

方法区创建于JVM的启动阶段.

方法区在逻辑上是堆内存的一部分.

方法区的简单实现可以选择不对其进行垃圾回收和整理.

HotSpot的实现会对此进行垃圾收集

方法区的大小应该设计为可调整,在内存中可以不连续.

运行时常量池(Run-Time Constant Pool)

运行时常量池是每个类与接口在运行时才能确定的常量.

例如 Sting const = UUID.randomUUID().toString();

每一个运行时常量池都是从方法区中分配的.

本地方法栈(Native Method Stacks)

本地方法栈在线程创建的时候进行分配,是线程独有的.

JVM规范规定本地方法栈可以是固定大小,也可以是可调整的.

jvm规范定义的内存区域

HotSpot虚拟机的运行时内存结构

jmc查看内存划分

jmc查看内存划分

在上图中,可以看到内存区域主要分为两大类,heap,Non-heap

虚拟机栈(Stack)

线程独有的内存空间,每个方法在执行时候会形成一个栈帧,用于存在这个方法的局部变量,操作数栈,动态链接,方法返回等信息.

程序计数器(The pc Register)

线程独有,描述的是JVM执行过程中程序的执行顺序,保证在CPU切换过程中对执行方法顺序的记录.

本地方法栈(Native Method Stacks)

native方法的区域.与虚拟机栈类似 HotSpot的JVM中,将本地方法栈虚拟机栈合二为一了

堆(Heap)

一般来说,new出来的对象放在,但是由于Java中不能直接使用对象的,而是通过引用,而对象的引用放在虚拟机栈,是局部变量表中的一个局部变量.

在堆中创建的对象,会有持有指向方法区的指针,因为对象的信息存储在方法区中.

由于GC都采用分代收集算法,所以堆内存的对象也进行了相应的划分.

堆内存在物理上可以是连续,也可以是不连续的.

方法区(MethodArea)

在HotSpot的实现中,将元空间(MetaSpace)放在方法区中. 线程共享的区域,存在类的信息,常量,静态变量等.进行GC的可能性较低.

从JDK8后就已经没有了永久代,使用metaSpace(元空间)

官方说法

元空间

  • JDK8不再有永久代
  • 类的元信息被存储在一个信息空间,称为MetaSpace
  • 内存是不连续的
  • 元空间是从本地内存中分配的
  • MetaSpace最大可用空间就是系统可用内存
  • 可以被MaxMetaspaceSize参数进行限制

永久代移除译文

直接内存.(Direct memory)

与NIO相关. JVM通过堆上的DirectByteBuffer操作直接内存

字节码缓存(code cache)

Non-heap区域,用于存储由JIT编译期生成的编译后代码.

  • 由内存直接分配
  • 由Code Cache 清理器管理

总结图

各个区域之间的联系

关于堆内存是线程共享的说法,其实并不严谨,因为还存在LTAB,在后续会继续学习记录.

创建对象的过程

使用new关键字创对对象实例. JVM会判断这个对象对应的是否已经被加载.如果没有则进行类加载过程.

1.在堆内存中创建对象的实例

  • 1.指针碰撞(内存中被占用与未占用内存空间分隔开)
  • 2.空闲列表(内存中被占用和未被占用的空间交织在一起)

两种方法与GC的机制有关,要看具体GC发生后是否会对对象进行压缩和移动.

2.为对象的成员变量赋初始值

3.将对象的引用返回

对象在内存中的布局

1.对象头 2.实例数据 3.对齐填充(可选)

栈中对象使用,是通过引用的方式.而引用又有两种方式

  • 1.句柄.堆数据一部分是对象在堆中的指针,另一部分是对象的类信息指针,类信息指针指向方法区
  • 2.指针.堆中一部分数据存放实例对象,另一部分存放类信息的指针

对象内存分布

JVM运行时数据区域

JVM对象内存区域例子

这个例子中,生成了2部分的内存区域

  • 1.obj这个引用变量,因为是方法内的变量,所以放到stack里面
  • 2.真正Object class的实例对象,放在Heap里面

这个例子中,一个消耗12bytes,JVM规定引用占4个bytes,空对象占8bytes

当方法执行结束后,对应stack中的变量马上回出栈,而Heap中的对象需要等待GC进行回收

Compressed Class Space&UseCompressedOops

MetaSpace默认会将元数据和类信息放在同一个区域,当 UseCompressedClassesPointers启用后,会将类信息和元数据分为两部分存储 ,MaxMetaspaceSize将会设置两部分空间的上限

启用前的存储形式

未启用前

启用后的存储形式

启用后

虚拟机参数-XX+UseCompressedOops使用的效果是,当从32bit的虚拟机迁移到64bit的虚拟机上,JVM会将部分的指针进行压缩,防止在64bit系统中占用更大的内存

TLAB

TLAB(Thread Local Allocation Buffer).是用于多线程分配对象的一个堆内存区域,能够提升多线程下并发创建对象造成竞争的性能下降. TLAB位于年轻代

TLAB

深入请参考

空间分配担保

当发生MinorGC时,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么MinorGC可以确保是安全的.

当大量对象在MinorGC后仍然存活,就需要老年代进行分空间分配担保,把Survivor无法容纳的对象直接进入老年代.

如果老年代判断剩余空间不足(根据以往每一次回收晋升到老年代对象容量的平均值作为判定值),进行FullGC,

参考资料