Java对象创建

Java对象创建

new对象过程

1、类加载检查

当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化

如果没有,那必须先执行相应的类加载过程,类加载就是在这个时候发生的

实例化对象的条件:所使用的类必须完成了类加载,连接,初始化三个阶段,能在常量池中定位到类的符号引用(全类名,字段名,描述符,方法名等)

2、分配内存

在类加载检查通过后,虚拟机将为新生的对象分配内存(内存大小在类加载器加载阶段计算得出)

为对象分配内存空间的任务实际上等同于将一块确定大小的内存从java堆中分离出来

堆内存是否规整决定了内存的分配方式

如果堆内存规则,则使用“指针碰撞”分配方式

将当前指针向空闲内存区域移动一段和新生对象大小相等的距离

如果堆内存不规整,则使用“空闲列表”分配方式

虚拟机需要维护一个列表,上面记录了哪些内存块可用,在分配时找到一块足够大的空间划分给对象实例,并更新列表记录

堆内存是否规整是由垃圾收集器决定的,即垃圾回收时是否带有空间压缩整理能力

3、对象初始化

内存分配完成后,虚拟机需要将分配到的内存空间(不包括对象头)都初始化为零值

这里区别于类的初始化,类的初始化是发生在类的加载检查阶段,类初始化会执行类的静态部分内容

4、对象头设置

在这一步一般会决定是否要启用对象的偏向锁,并且设置对象头信息

详细阅读下文对象的内存布局

5、构造函数

在此之前对象的所有字段都为默认零值,Class文件中<init>()方法的执行,会按照程序员的意愿对对象进行初始化,执行构造函数

在执行子类构造函数之前会先执行父类的构造函数

对象的内存布局

对象在堆内存中的存储布局可以划分为三个部分:对象头(Header),实例数据(InstanceData)和对齐填充(Padding)

对象头

对象头包括两类信息

第一类是用于存储对象的运行时数据Mark Word

  • 如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等

第二类是类型指针,即对象指向它的类型元数据指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例

  • 如果对象是一个Java数组,那么在对象头中还必须有一块用于记录数组长度的数据

实例数据

实例数据是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来

对齐补充

无特殊含义,仅仅起到占位符作用,虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍

OOM异常

内存溢出异常与虚拟机本身的实现密切相关

Java堆溢出

Java堆用于存储对象实例,只需要不断地创建对象,并且保证GC Roots到达对象之间存在可达路径来避免GC

那么随着对象数量增加,总容量触及最大堆内存容量限制后就会产生OOM异常

要解决这个内存区域的异常,首先需要确认导致OOM的对象是否是必要的,也就是要分清楚是出现了内存泄漏还是内存溢出

  • 如果是内存泄漏,我们则需要通过查找工具找到对象到GC roots的引用链,找到对象创建的位置,进而找出产生内存泄漏代码的具体位置
  • 如果是内存溢出,也就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xms与-Xms)设置,与机器内存对比,查看是否还有向上调整的空间,再从代码上检查是否存在某些对象生命周期过长持有状态时间过长存储结构设计不合理等情况,尽量减少程序运行期的内存消耗
public class TestDemo {

    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:261)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
    at java.util.ArrayList.add(ArrayList.java:458)
    at TestDemo.main(TestDemo.java:15)

Java栈溢出

栈内存容量由-Xss参数来设定

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出SOF(StackOverflow)异常
  • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OOM异常
/**
* VM Args: -Xss128k
*/
public class JavaVMStackTest {

    private int stackLength = 1;

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

    public static void main(String[] args) throws Throwable {

        JavaVMStackTest oom = new JavaVMStackTest();

        try {
            oom.stackLeak();
        }catch (Throwable e){
            System.out.println("stack length:"+oom.stackLength);
            throw e;
        }

    }

}

异常信息

stack length:999
Exception in thread "main" java.lang.StackOverflowError
    at JavaVMStackTest.stackLeak(JavaVMStackTest.java:6)
    at JavaVMStackTest.stackLeak(JavaVMStackTest.java:7)
    at JavaVMStackTest.stackLeak(JavaVMStackTest.java:7)
    at JavaVMStackTest.stackLeak(JavaVMStackTest.java:7)
    .......

操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程最大内存限制为2GB,虚拟机提供了参数可以控制Java堆和方法区这两部分的内存最大值

如果每个线程分配到的栈内存很大,那么可以建立的线程数自然就很少,建立线程时就越容易把剩下的内存耗尽

内存不够导致的线程创建失败问题

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
  • 发生这种情况,如果不能减少线程数和更换虚拟机的情况下,就需要通过减少最大堆和减少栈容量来换取更多线程

运行时常量池溢出

常量池作为方法区的一部分,在JDK6或更早版本,是由永久代来实现方法区内存的

如果以JDK6运行下面的代码

/**
     * VM Args: -XX:PermSize=6M -XX:MaxPermSize=6M
     * @param args
     */
    public static void main(String[] args){

        //使用Set保持着常量池的引用,避免Full GC回收常量池行为
        Set<String> set = new HashSet<String>();

        //在short范围内足以让6M的PermSize产生OOM了
        short i = 0;
        while (true){
            //intern()方法,如果常量池中不存在此字符串则添加,存在则返回
            set.add(String.valueOf(i++).intern());
        }

    }

异常信息

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)
    at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)
  • 说明常量池确实是方法区(永久代)的一部分

而使用JDK7或8等更高级版本,并不会得到相同结果

  • JDK7字符串常量池被移至Java堆
  • JDK8字符串常量池和方法区被移动到元空间中(使用本地内存)

元空间大小限制配置

参考博客

-XX:MaxMetaspaceSize:设置元空间的最大值,默认-1,即不受限制,只受限于本地内存大小

-XX:MeataspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类卸载,同时收集器会对该值进行调整

  • 如果释放了大量空间就降低该值
  • 如果释放了很少空间,那么在不超过最大值的前提下适当提高该值

-XX:MinMetaspaceFreeRatio:控制最小的元空间剩余容量百分比

  • 如果GC后空闲空间小于此值,则发生元空间扩容

-XX:MaxMetaspaceFreeRatio:用于控制最大的元空间剩余容量的百分比

  • 如果GC后空闲空间大于此值,则发生元空间缩减

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com

文章标题:Java对象创建

字数:2.2k

本文作者:Os467

发布时间:2022-09-06, 21:16:42

最后更新:2022-09-06, 21:17:27

原始链接:https://os467.github.io/2022/09/06/Java%E5%AF%B9%E8%B1%A1%E5%88%9B%E5%BB%BA/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

×

喜欢就点赞,疼爱就打赏