JAVA虚拟机的内存管理

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。

运行时数据区域

见图:

avator

程序计数器

一块较小的内存空间,当前线程执行字节码的行号计数器,字节码解释器通过修改它来让程序知道分支、跳转、异常处理等逻辑下一条指令去哪里执行。
在多线程环境中,由于当前线程在某一时刻,只会执行线程的一条指令,线程切换之后要恢复到之前程序执行的点。所以程序计数器线程独立的。
如果线程执行的是JAVA方法,该值记录的是虚拟机字节码指令地址。如果是native方法该值是undifined,该区域是jvm中唯一没规定oom的区域

Java虚拟机栈

也是线程独有的,生命周期和线程绑定在一起。描述的是JVM的内存模型:每个方法在执行时候会生成一个Stack Frame。存储方法的局部变量表、操作数栈、动态链接、方法出口等。
每一个方法从执行开始到执行完毕都是Stack Frame的一个入栈到出栈的操作。
局部变量表存放:java的几本类型,对象引用、returnAddress等。局部变量表的内存空间在编译时候就确定好了,当进入一个方法中,局部变量表在Stack Frame的大小是确定的不会改变。
如果线程请求的虚拟机栈超过最大深度会报StackOverFlowError异常,虚拟机栈可以动态扩展但是没申请到内存会报OutOfMemoryError异常。(这里是只没打到深度但是内存不足了)

本地方法栈

基本上和JAVA虚拟机栈类似这里是指调用的是native方法。会抛出的异常如上。

JAVA堆

存放对象最大的一块区域,可以是不连续的内存空间。具体针对gc方式可以分为young old metaspace等。后期还有g1回收期,这里暂时不做讨论,所有线程共享。如果内存不足会报OutOfMemoryError异常

方法区

我们常说的PermSpace或者MetaSpace。用于存储Java虚拟机的类信息、静态变量、常量、即时编译的代码等。为了和堆区分开也叫(Non heap)。JVM规范对方法去限制很宽松除了内存可以不连续外,还可以不实现gc,方法区的gc主要取决于常量的回收和类的卸载。

直接内存

在jdk1.4之后Java引入了Nio,可以直接通过native函数分配堆外内存 通过DirectByteBuffer进行读取/写入,也会引起OutOfMemoryError

运行时的常量池

方法区的一部分,类在编译时产生的常量在加载Class时候会存储在方法区的常量池中,常量池是动态的的比如String的inter()方法会在运行时动态的加入常量池,所以也有可能出现OutOfMemoryError异常

补充一点:
String s=”liuhao” 是一个常量是编译时候就决定好的,所以java汇总
String s1=”liu”+”hao” 俩个常量相加会进入常量池儿常量池只有一个拷贝,所以s==s1
String s=new String(“liuhao”) 不是常量所以不能放在常量池。
String s1=s1.intern() 将s1的值写入到常量池中,扩充了常量池

Object

HotSpot的Object的创建

对象创建的步骤

  1. 类装载:去常量池中查找Class的符号引用,检查类是否已经被加载、解析、初始化过;
  2. 分配内存,依据内存是否完整有俩种分配方式
    1. 指针碰撞:Bump the Pointer,规整的内存空间,Serial,ParNew这种垃圾回收机制会整理内存。将已分配、未分配用一个指针分隔开,分配时候讲指针挪动一个对象的size个位置。
    2. 空闲列表: Free List,不规整的内存空间,CMS垃圾回收机制不需要整理内存。维护一个空闲列表,分配时候找到一个空闲的内存空间进行分配。
    3. 指针碰撞的线程安全:
      1. TLAB:每个内存开辟一个独有的小的内存空间默认是Eden区1%,对象会先在TLAB上创建,这样就保证了原子性。虚拟机内部会维护一个refill_waste值,如果对象需要的空间小于TLAB的剩余空间,同时对象的size大于refill_waste,会在堆中创建,如果小于refill_waste则会新申请一个TLAB进行创建。用参数-XX:+UseTLAB开启,默认是开启的
      2. CAS+失败重试,保证分配空间的原子性。
  3. 初始化对象,将对象里的属性初始化成零值
  4. 设置对象头:元数据信息、Hash值、GC分带年龄等
  5. 对象从虚拟机的角度来看已经创建完成,java代码还需要对对象进行init的设置。

对象的内存布局

分为对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

对象头

  • MarkWord:HashCode、GC年代分龄、锁标记位、线程持有的锁、偏向的线程ID、偏向时间戳。长度32bit or 64bit。
    • 32bit:HashCode(25bit)、GC年代分龄(4bit)、锁标记位(2bit)、unused(1bit)
    • 64bit:unused(25bit)、HashCode(31bit)、GC年代分龄(4bit)、锁标记位(2bit)、unused(1bit)、block(1bit)
  • 类型指针:类元素的指针告诉虚拟机它是哪个类的实例。如果是数组还需要保存数组的长度

实例数据

保存对象的真实数据,分配策略是相同宽度的对象会分配在一起,满足这个前提条件下,父类的字段会放在子类之前。

对象填充

用于对齐对象用,因为要求对象必须是8字节的整数倍,如果不是需要这部分进行填充。

对象访问定位

创建对象之后java通过栈上的refrences数据来操作对象实例,JVM没有规定如何具体访问对象有俩种访问方式,通过句柄方式和直接指针方式

句柄方式:jvm在堆上开辟一块空间作为句柄池,refrence保存的是句柄地址,句柄来负责访问对象一部分指向堆中对象的实例地址,一部分指向方法区的类实例地址。
好处:refrence保存的是稳定的句柄地址,当对象被移动时候refrence不需要改变。(例如gc时候移动对象)
avator

直接对象指针:refrence保存的是实例数据的指针。HotSpot采用这种方式
好处:速度快。少一次句柄查找的操作。由于创建对象频繁这一步性能的节省效果很客观。
avator

OutOfMemory

todo

java堆溢出

执行如下程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @Author: liuhaoeric
* Create time: 2019/10/17
* Description:
*/
public class HeapOOM {

static class OOMObject {
}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}

会出现异常信息
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.ericliu.practice.toy.jvm.oom.HeapOOM.main(HeapOOM.java:19)

其中Java heap space的表示是堆溢出,可以通过HeapDumpOnOutOfMemoryError导出的文件通过MAT查看(eclipse的插件)

java方法栈和本地方法栈溢出溢出

会出现StackOverFlowError或者StackOutOfMemeoryError。执行下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* -Xss160k
* @Author: liuhaoeric
* Create time: 2019/10/17
* Description:
*/
public class JavaVMStackSOF {
private int stackLength = 1;

public void stackLeak() {
stackLength++;
stackLeak();
}

public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}

}

会出现异常信息
Exception in thread "main" java.lang.StackOverflowError
at com.ericliu.practice.toy.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
......

单线程的情况会出现StackOverflowError,通过不断创建线程可能会耗尽内存导致Stack的OOM,这种情况可以通过减少线程或者增大堆内存,或者减少每个线程的内存。

多线程会出现如下异常

1
Exception in thread"main"java.lang.OutOfMemoryError:unable to create new native thread

方法区和常量池溢出

1.6执行如下代码,因为1.7采用了MetaSpace所以失效了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* -XX:PermSize=10M -XX:MaxPermSize=10M
* @Author: liuhaoeric
* Create time: 2019/10/17
* Description:
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<>();
// 10MB的PermSize在integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}

出现PermGen Space

本机直接内存溢出

可以通过DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {

private static final int MB = 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(MB);
}
}
}

异常:
Exception in thread"main"java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20

特征是dump的文件很小。如果发现dump的文件很小同时用了nio可以考虑是这方面的原因。