内存分配
1、运行时数据区域
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,按照《Java虚拟机规范》的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:

1.1 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,表示当前线程所执行的字节码的行号指示器。字节码解释器工作时通过计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
为了保证多线程之间切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,线程之间计数器互不影响,独立存储,表示为线程私有状态。
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。
1.2 虚拟机栈(本地方法栈)
虚拟机栈同程序计数器一样是线程私有,生命周期同线程。其描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息,一个方法的执行过程就是虚拟机栈出栈入栈的过程。
局部变量表中存放在编译时期可知的所有变量类型,包括 8 种基础数据类型,对象引用以及 returnAddress(下一条字节码地址),其中 long、double 占据 2 个变量槽之外,其余只占一个槽(这里的槽指的是计量单位,具体槽表示 32、64 位看具体虚拟机的实现)。
虚拟机栈在大小上有两种异常:
StackOverFlow:申请的栈空间超过了最大限制(默认 256KB 大小),可通过参数 -Xss 修改;
OutOfMemory:当虚拟机栈空间支持动态扩展时,且无法申请到足够内存。
本地方法栈和虚拟机栈很类似,只是前者执行 Native 方法,后者执行字节码方法。
1.3 Java堆
Heap 是虚拟机所管理的内存中最大的一块,被所有线程共享,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
从分配内存的角度看,所有线程共享的 Java 堆中还可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。 虽然是私有缓冲,但是垃圾收集时是共享区域,也可以被删除。
1.4 方法区
和 Java 堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
1.5 运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java 并不要求常量一定只有编译期才能产生,并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,也就是说常量池的内存占用可拓展,但是也受限于方法区的大小,当无法申请足够内存时,抛出 OOM。
2、HotSpot
2.1 对象创建
当 Java 虚拟机遇到一条字节码 new 指令时,首先检查该指令的参数是否能在常量池中定位到一个类的符号引用,代表的类是否已被加载、解析和初始化。如果没有,需要执行相应的类加载过程。
之后进行内存的分配(虚拟机之间的实现不一样),在内存分片过程中需要考虑并发性,这里有两种解决方案,一是 CAS 重试保证原子性,二是使用 TLAB 预先完成线程私有内存分配(通过 -XX:+/-UseTLAB 决定是否开启该配置)。
1、如果一个对象需要的空间大小超过 TLAB 中剩余的空间大小,则直接在堆内存中对该对象进行内存分配;
2、如果一个对象需要的空间大小超过 TLAB 中剩余的空间大小,则废弃当前 TLAB,重新申请 TLAB 空间再次进行内存分配。
两种方案各有利弊,因此定义一个 refill_waste 概念,当大于这个值则堆内分配,否则废弃当前 TLAB。
内存分配完成之后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为对应的零值,如果使用了 TLAB 的话,这一项工作也可以提前至 TLAB 分配时顺便进行。
最后进行对象头的设置,包含对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码,GC 次数等。
2.2 优先新生代分配
对内创建对象时会先将对象分配到新生代中。
2.3 大对象直接进入老年代
HotSpot 虚拟机提供了 -XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配。
2.4 长期存活对象进入老年代
虚拟机给每个对象定义了一个 GC 计数器,对象通常在 Eden 区里诞生,并在 Survivor 区中每熬过一次 Minor GC,计数器就增加 1,当到达一定的阈值次数(默认为15,可以通过 -XX:MaxTenuringThreshold 设置),就会被晋升到老年代中。
如果在 Survivor 空间中相同计数所有对象大小的总和大于 Survivor 空间的一半,计数大于或等于该计数的对象就可以直接进入老年代,无须等到阈值。
2.5 空间分配担保
在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次 Minor GC 可以确保是安全的。如果不成立,则虚拟机会先查看 -XX:HandlePromotionFailure 参数的设置值是否开启允许担保失败;如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者不允许担保冒险,那这时就要改为进行一次 Full GC。
- 感谢你赐予我前进的力量