Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示:
我们可以将上面的数据区域分为线程独有、线程共享及其他三大区域:
Java是一门面向对象的语言,在Java程序运行的过程中无时不刻都有对象被创建。在语言层面,创建对象通常是一个new关键字,但是,在虚拟机中,创建对象包括如下流程:
类加载 --> 分配内存 --> 内存空间初始化零值 --> 对象头设置 --> init初始化
分配内存的方式为:
“指针碰撞”:在内存规整情况下,将指针向空闲空间挪动一段与对象大小相等的距离。
“空闲列表”:在内存不规整情况下,虚拟机维护一个记录内存可用的列表,分配的时候从列表中找到一块空间划分给对象。
并发情况下的内存分配:
同步:对分配内存空间的动作进行同步处理———采用CAS配上失败重试的方式,保证更新操作的原子性
本地线程分配缓冲(TLAB):把内存分配动作按照线程划分在不同空间中。即每个线程在Java堆中预先分配一块内存TLAB,只有TLAB用完并重新分配新的TLAB时才需要同步。
在HotSpot虚拟机中,对象在内存中的存储布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对其填充(Padding)
建立对象是为了使用对象。我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有两种:句柄和直接指针。
句柄:Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的 句柄地址 。句柄中包含了对象实例数据与类型数据各自的具体地址。
直接访问:reference指针存储的直接就是对象地址
使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(如垃圾收集时)时只会改变句柄中的实例数据指针,而reference本身不需要修改
直接访问最大的好处就是速度快。节省了一次指针定位的时间开销。HotSpot虚拟机使用第二种方式进行对象的访问。
-Xms 堆最小值 -Xmx 堆最大值 -XX:HeapDumpOnOutOfMemoryError可以在虚拟机出现异常时将堆存储快照
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError 复制代码
public class HeadOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<>(); while (true) { list.add(new OOMObject()); } } } 复制代码
运行结果:
-Xss 设置栈的大小
-Xss228k 复制代码
public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength ++; stackLeak(); } public static void main(String[] args) { JavaVMStackSOF stackSOF = new JavaVMStackSOF(); stackSOF.stackLeak(); } } 复制代码
运行结果:
实验结果表明,在单线程下,当内存无法分配的时候,虚拟机抛出的都是StackOverflow异常
测试:创建线程导致内存溢出异常
public class JavaVMStackOOM { private void dontStop() { while (true) { } } public void stackLeakByThread() { while (true) { new Thread(() -> { dontStop(); }).start(); } } public static void main(String[] args) { JavaVMStackOOM javaVMStackOOM = new JavaVMStackOOM(); javaVMStackOOM.stackLeakByThread(); } } 复制代码
String.intern()方法返回的是常量池中的对象,如果池中没有对象,则创建对象返回引用
在JDK 1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量,测试代码:
public class RuntimeConstantPoolOOM { public static void main(String[] args) { List<String> list = new ArrayList<>(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } } 复制代码