当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化
如果没有,那必须先执行相应的类加载过程,类加载就是在这个时候发生的
实例化对象的条件:所使用的类必须完成了类加载,连接,初始化三个阶段,能在常量池中定位到类的符号引用(全类名,字段名,描述符,方法名等)
在类加载检查通过后,虚拟机将为新生的对象分配内存(内存大小在类加载器加载阶段计算得出)
为对象分配内存空间的任务实际上等同于将一块确定大小的内存从java堆中分离出来
堆内存是否规整决定了内存的分配方式
如果堆内存规则,则使用“指针碰撞”分配方式
将当前指针向空闲内存区域移动一段和新生对象大小相等的距离
如果堆内存不规整,则使用“空闲列表”分配方式
虚拟机需要维护一个列表,上面记录了哪些内存块可用,在分配时找到一块足够大的空间划分给对象实例,并更新列表记录
堆内存是否规整是由垃圾收集器决定的,即垃圾回收时是否带有空间压缩整理能力
内存分配完成后,虚拟机需要将分配到的内存空间(不包括对象头)都初始化为零值
这里区别于类的初始化,类的初始化是发生在类的加载检查阶段,类初始化会执行类的静态部分内容
在这一步一般会决定是否要启用对象的偏向锁,并且设置对象头信息
详细阅读下文对象的内存布局
在此之前对象的所有字段都为默认零值,Class文件中<init>()
方法的执行,会按照程序员的意愿对对象进行初始化,执行构造函数
在执行子类构造函数之前会先执行父类的构造函数
对象在堆内存中的存储布局可以划分为三个部分:对象头(Header),实例数据(InstanceData)和对齐填充(Padding)
对象头包括两类信息
第一类是用于存储对象的运行时数据Mark Word
第二类是类型指针,即对象指向它的类型元数据指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例
实例数据是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来
无特殊含义,仅仅起到占位符作用,虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍
内存溢出异常与虚拟机本身的实现密切相关
Java堆用于存储对象实例,只需要不断地创建对象,并且保证GC Roots到达对象之间存在可达路径来避免GC
那么随着对象数量增加,总容量触及最大堆内存容量限制后就会产生OOM异常
要解决这个内存区域的异常,首先需要确认导致OOM的对象是否是必要的,也就是要分清楚是出现了内存泄漏还是内存溢出
x1public class TestDemo {
2
3 static class OOMObject{
4
5 }
6
7 public static void main(String[] args) {
8
9 List<OOMObject> list = new ArrayList<OOMObject>();
10
11 while (true){
12 list.add(new OOMObject());
13 }
14
15 }
16
17
18}
异常信息
xxxxxxxxxx
81Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
2 at java.util.Arrays.copyOf(Arrays.java:3210)
3 at java.util.Arrays.copyOf(Arrays.java:3181)
4 at java.util.ArrayList.grow(ArrayList.java:261)
5 at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
6 at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
7 at java.util.ArrayList.add(ArrayList.java:458)
8 at TestDemo.main(TestDemo.java:15)
栈内存容量由-Xss参数来设定
xxxxxxxxxx
261/**
2* VM Args: -Xss128k
3*/
4public class JavaVMStackTest {
5
6 private int stackLength = 1;
7
8 public void stackLeak(){
9 stackLength++;
10 stackLeak();
11 }
12
13 public static void main(String[] args) throws Throwable {
14
15 JavaVMStackTest oom = new JavaVMStackTest();
16
17 try {
18 oom.stackLeak();
19 }catch (Throwable e){
20 System.out.println("stack length:"+oom.stackLength);
21 throw e;
22 }
23
24 }
25
26}
异常信息
xxxxxxxxxx
71stack length:999
2Exception in thread "main" java.lang.StackOverflowError
3 at JavaVMStackTest.stackLeak(JavaVMStackTest.java:6)
4 at JavaVMStackTest.stackLeak(JavaVMStackTest.java:7)
5 at JavaVMStackTest.stackLeak(JavaVMStackTest.java:7)
6 at JavaVMStackTest.stackLeak(JavaVMStackTest.java:7)
7 .......
操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程最大内存限制为2GB,虚拟机提供了参数可以控制Java堆和方法区这两部分的内存最大值
如果每个线程分配到的栈内存很大,那么可以建立的线程数自然就很少,建立线程时就越容易把剩下的内存耗尽
内存不够导致的线程创建失败问题
xxxxxxxxxx
11Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
常量池作为方法区的一部分,在JDK6或更早版本,是由永久代来实现方法区内存的
如果以JDK6运行下面的代码
xxxxxxxxxx
171/**
2 * VM Args: -XX:PermSize=6M -XX:MaxPermSize=6M
3 * @param args
4 */
5 public static void main(String[] args){
6
7 //使用Set保持着常量池的引用,避免Full GC回收常量池行为
8 Set<String> set = new HashSet<String>();
9
10 //在short范围内足以让6M的PermSize产生OOM了
11 short i = 0;
12 while (true){
13 //intern()方法,如果常量池中不存在此字符串则添加,存在则返回
14 set.add(String.valueOf(i++).intern());
15 }
16
17 }
异常信息
xxxxxxxxxx
31Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
2 at java.lang.String.intern(Native Method)
3 at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)
而使用JDK7或8等更高级版本,并不会得到相同结果
元空间大小限制配置
-XX:MaxMetaspaceSize
:设置元空间的最大值,默认-1,即不受限制,只受限于本地内存大小
-XX:MeataspaceSize
:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类卸载,同时收集器会对该值进行调整
-XX:MinMetaspaceFreeRatio
:控制最小的元空间剩余容量百分比
-XX:MaxMetaspaceFreeRatio
:用于控制最大的元空间剩余容量的百分比