本文概述了Java内存的分配模型。
JVM简化架构
线程私有
程序计数器:
作用:存储下一条指令的地址;创建:创建线程时候创建,因此是线程私有的
执行本地(native)方法时,程序计数器为空(Undefined)。比较小的内存空间,虚拟机中唯一没有规定OOM的区域。
Java栈
栈由一系列栈帧构成,栈帧时用来保存一个方法的局部变量表、操作数栈、动态连接、方法出口。
其中局部变量表包括了基本数据类型、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
每次方法调用,都创建一个帧并压栈,退出方法就弹栈。
存储快速,但是空间小。
本地方法栈
支持native方法的栈。
线程共享
Java堆
唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。同时,Java堆是垃圾收集器管理的内存区域。如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。
优点:运行期动态分配内存,自动CG。
缺点:效率低
方法区
存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。与元空间相关。
- 运行时常量池:方法区的一部分。存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。类的版本、字段、方法、接口等。
交互关系
栈里面只是指针,指向堆中对象的user实例和user类的元数据信息,元数据信息指向方法区对应的user类的类定义,字段,方法。
对象in内存
对象的结构
分为:对象头,实例数据和对齐填充。
- 对象头:
markword:Hashcode,GC分代年龄,锁标志等
类型指针:对象指向它的类元数据指针。
- 实例数据
- 对齐位置:8字节
对象的创建
JVM遇到字节码new之后,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
然后进行内存分配,对象所需内存的大小在类加载完成后便可完全确定。两种方法:指针碰撞和空闲列表。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理的能力决定。当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。
可能在分配空间时多线程发生冲突,两个方法:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
最后设置对象头,执行<init>
方法。
对象的访问定位
程序会通过栈上的reference数据来操作堆上的具体对象。因此这里需要一个指向对象的引用。两种:使用句柄或者直接指针。
使用句柄:如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
直接指针:reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,。
- 方法对比:
直接方法更快速,hotSpot使用;句柄方法更安全,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
Java堆的参数
- GC发生的时间,日志级别信息,日志类型标记,GC识别号。
- GC的类,说明GC的原因
- GC的容量,GC持续的时间
-Xlog:gc+heapdebug 每一次GC都打印堆信息
-Xlog:gc:garbage-collection.log 指定GC log的位置,以文件输出
-Xlog:gc 打印Gc的简要信息
-Xlog:gc 打印详细信息
Java堆的参数
- -Xms10m:初始堆的大小,大于1MB且1024的倍数。默认物理内存的1/64
- -Xmx20m: 最大堆大小,默认物理内存的1/4
- -xmn:新生代大小,默认整个堆的3/8