虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
14. 局部变量表(存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始 地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址))所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定 的,在方法运行期间不会改变局部变量表的大小。
15. 在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError异常。
16. 只要是申请到内存就只会发生 StackOverflowError异常,如果无法申请到才会发生 OutOfMemoryError 异常。
因此在 Hotspot 虚拟机中,整个堆和方法区被分成如下的几块。其中 Eden 和Survivor见:(https://blog.csdn.net/liushengxi_root/article/details/122858901)
在 Java7 之前Hotspot中方法区位于永久代中。同时,永久代和堆是相互隔离的,但它们使用的物理内存是连续的。
永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。
在 Java8 及之后,HotSpots 取消了永久代,那么是不是就没有方法区了呢?当然不是,方法区只是一个规范,只不过它的实现变了。
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)
。
针对Java8的调整,我们再次对内存结构图进行调整。
默认情况下元空间是可以无限使用本地内存的,但为了不让它如此膨胀,JVM同样提供了参数来限制它的使用。
除了最为常见的new语句之外,我们还可以通过反射机制、Object.clone方法、反序列化以及Unsafe.allocateInstance方法来新建对象
其中,Object.clone方法和反序列化通过直接复制已有的数据,来初始化新建对象的实例字段。Unsafe.allocateInstance方法则没有初始化实例字段,而new语句和反射机制,则是通过调用构造器来初始化实例字段。
当我们调用一个构造器时,它将优先调用父类的构造器,直至Object类。这些构造器的调用者皆为同一对象,也就是通过new指令新建而来的对象。也就是说:通过new指令新建出来的对象,它的内存其实涵盖了所有父类中的实例字段。也就是说,虽然子类无法访问父类的私有实例字段,或者子类的实例字段隐藏了父类的同名实例字段,但是子类的实例还是会为这些父类实例字段分配内存的。
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
压缩指针
在64位的Java虚拟机中,对象头的标记字段占64位,而类型指针又占64位。因此为了尽量较少对象的内存使用量,64位Java虚拟机引入了压缩指针的概念(对应虚拟机选项-XX:+UseCompressedOops,默认开启),将堆中原本64位的Java对象指针压缩成32位的,使得对象头的大小从16字节降至12字节。
打个比方,路上停着的全是房车,而且每辆房车恰好占据两个停车位。现在,我们按照顺序给它们编号。也就是说,停在0号和1号停车位上的叫0号车,停在2号和3号停车位上的叫1号车,依次类推。
原本的内存寻址用的是车位号。比如说我有一个值为6的指针,代表第6个车位,那么沿着这个指针可以找到3号车。现在我们规定指针里存的值是车号,比如3指代3号车。当需要查找3号车时,我便可以将该指针的值乘以2,再沿着6号车位找到3号车。
这样一来,32位压缩指针最多可以标记2的32次方辆车,对应着2的33次方个车位。当然,房车也有大小之分。大房车占据的车位可能是三个甚至是更多。不过这并不会影响我们的寻址算法:我们只需跳过部分车号,便可以保持原本车号*2的寻址系统。
上述模型有一个前提,你应该已经想到了,就是每辆车都从偶数号车位停起。这个概念我们称之为内存对齐(对应虚拟机选项-XX:ObjectAlignmentInBytes,默认值为8)。
当然,就算是关闭了压缩指针,Java虚拟机还是会进行内存对齐。此外,内存对齐不仅存在于对象与对象之间,也存在于对象中的字段之间。比如说,Java虚拟机要求long字段、double字段,以及非压缩指针状态下的引用字段地址为8的倍数。
字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会 受到虚拟机分配策略参数(-XX:FieldsAllocationSty le参数)和字段在Java源码中定义顺序的影响。 HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放(其实可以看出真的是为了内存对齐),在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的 +XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空 隙之中,以节省出一点点空间。
占位的作用!
对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:
使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference本身不需要被修改。(解耦啦)
针对虚拟机 HotSpot 而言,它主要使用
第二种方式进行对象访问,即直接指针方式!!!!
由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置 本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。一共存在两种异常:
32. 如 果 线 程 请 求 的 栈 深 度 大 于 虚 拟 机 所 允 许 的 最 大 深 度 , 将 抛 出 St a c k O v e r f l o w E r r o r 异 常 。
33. 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError 异常。
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * @author zzm
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常 情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectM emory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。
JVM之垃圾收集
https://zhuanlan.zhihu.com/p/111809384