深入探究JVM之对象创建及分配策略

@目录

前言

Java是面向对象的语言,所谓“万事万物皆对象”就是Java是基于对象来设计程序的,没有对象程序就无法运行(8大基本类型除外),那么对象是如何创建的?在内存中又是怎么分配的呢?

正文

一、对象的创建方式

在Java中我们有几种方式可以创建一个新的对象呢?总共有以下几种方式:

  • new关键字
  • 反射
  • clone
  • 反序列化
  • Unsafe.allocateInstance

为了便于说明和理解,下文仅针对new出来的对象进行讨论。

二、对象的创建过程

在这里插入图片描述


Java中对象的创建过程就包含上图中的5个步骤,首先需要验证待创建对象的类是否已经被JVM记载,如果没有则会先进行类的加载,如果已经加载则会在堆中(不完全是堆,后文会讲到)分配内存;分配完内存后则是对对象的成员变量设置初始值(0或null),这样对象在堆中就创建好了。但是,这个对象是属于哪个类的还不知道,因为类信息存在于方法区,所以还需要设置对象的头部(当然头部中也不仅仅只有类型指针信息,稍后也会详细讲到),这样堆中才创建好了一个完整的对象,但是这个对象的成员变量还都是初始值,所以最后会调用init方法按照我们自己的意愿初始化对象,一个真正的对象就创建好了。
对象的整个创建过程是非常简单的,但是其中还有很多细节,比如对象会在哪里创建?分配内存有哪些方式?怎么保证线程安全?对象头中有哪些信息?下面一一讲解。

对象在哪里创建

基本上所有的对象都是在堆中,但并非绝对,在JDK1.6版本引入了逃逸分析技术。逃逸分析就是指针对对象的作用域进行判定,当一个对象在方法中被定义后,如果被其它方法其它线程访问到,就称为方法逃逸线程逃逸
该技术针对未逃逸的对象做了一个优化:栈上分配(除此之外还有同步消除标量替换,这里暂时不讲)。这个优化是指当一个对象能被确定不会在该方法之外被引用,那么就可以直接在虚拟机栈中创建该对象,那么这个对象就可以随着线程的消亡而销毁,不再需要垃圾回收器进行回收。这个优化带来的收益是明显的,因为有相当一部分对象都只会在该方法内部被引用。逃逸分析默认是开启的,可以通过-XX:-DoEscapeAnalysis参数关闭。下面看一个实例:

public class EscapeAnalysisTest {
    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 50000000; i++) {//5000万次---5000万个对象
            allocate();
        }
        System.out.println((System.currentTimeMillis() - start) + " ms");
        Thread.sleep(600000);
    }

    static void allocate() {//逃逸分析(不会逃逸出方法)
        //这个myObject引用没有出去,也没有其他方法使用
        MyObject myObject = new MyObject(2020, 2020.6);
    }

    static class MyObject {
        int a;
        double b;

        MyObject(int a, double b) {
            this.a = a;
            this.b = b;
        }
    }
}

加上-XX:+PrintGC参数运行上面的方法,会看到控制台只是打印了执行时间5ms,但是若再加上-XX:-DoEscapeAnalysis关闭逃逸分析就会出现下面的结果:

[GC (Allocation Failure)  66384K->848K(251392K), 0.0012528 secs]
[GC (Allocation Failure)  66384K->816K(251392K), 0.0010461 secs]
[GC (Allocation Failure)  66352K->848K(316928K), 0.0009666 secs]
[GC (Allocation Failure)  131920K->784K(316928K), 0.0018284 secs]
[GC (Allocation Failure)  131856K->816K(438272K), 0.0009315 secs]
[GC (Allocation Failure)  262960K->684K(438272K), 0.0022738 secs]
[GC (Allocation Failure)  262828K->684K(700928K), 0.0005052 secs]
308 ms

执行时间大大提升,主要是用在了GC回收上。

分配内存

  • 分配方式
    JVM有两种分配内存的方式:指针碰撞空闲列表。使用哪种方式取决于堆中内存是否规整,而是否规整又取决于使用的垃圾回收器,这个是下一篇的内容。如果内存规整,那么就会使用指针碰撞分配内存,也就是将已用的内存和未用的内存分开分别放到一边,中间使用指针作为分界线;当需要分配内存时,指针就向未分配的那一边挪动一段与对象大小相等的距离。如果内存不是规整的,JVM会维护一个列表,列表中会记录哪些内存是可用的,分配内存时首先就会去这个表里面找到可用且大小合适的内存。
  • 线程安全
    理解了上面的两种方式,敏锐的读者应该很快就能发现其中的问题,我们的JVM肯定不会以单线程的方式去堆中创建对象,那样效率是极低的,那么怎么保证同一时间不会有两个线程同时占用同一块内存呢?JVM同样有两种方式保证线程安全:CAS和TLAB(本地线程缓冲)。
    • CAS是compare and swap,涉及到预期值内存值更新值。意思当前线程每当需要分配内存时首先从内存中取出值和期望值比较,如果相等则将内存中的值更新为更新值,否则则继续循环比较,这样当前线程在申请内存时,一旦该内存被其它线程提前占据,那么当前线程就会去申请其它未被占据的内存,
    • TLAB是指线程首先会去堆中申请一块内存,每个线程都在各自占据的内存中创建对象,也就不存在线程安全问题了。可以通过-XX:+/-UseTLAB参数进行控制。

对象的内存布局

在HotSpot虚拟机中,对象在内存中分为三块:对象头、实例数据和对齐填充。如下图:

在这里插入图片描述

对象的内存布局上面这张图写的很清楚了,其中自身运行时数据了解一下有哪些信息即可,类型指针则是指向对象所属的类,如果对象是数组,则对象头中还会包含数组的长度信息;实例数据就是指对象的字段信息;最后对齐填充则不是必须的,因为为了方便处理和计算,HotSpot要求对象的大小必须是8字节的整数倍,因此当不满8字节的整数倍时,就需要对齐填充来补全。

三、对象的访问定位

当对象创建完成后就存在于堆中,那么栈中怎么定位并引用到该对象呢?虚拟机规范中本身并没有定义这一部分该如何实现,具体的实现取决于各个虚拟机厂商,而目前主流的定位方式有两种:句柄直接指针

  • 句柄

    在这里插入图片描述


    通过句柄的方式引用就是虚拟机首先会在堆中划分一块区域作为句柄池,句柄池中包含了指向对象实例类型数据的指针,而栈中则只需要引用句柄池即可。这种方式的好处显而易见,引用非常稳定,不会随着对象的移动而需要改变栈中的引用,但这样势必会降低引用的性能,同时堆中可用内存变少。
  • 直接指针

    在这里插入图片描述


    顾名思义,直接指针就是指栈中引用直接指向堆中的对象,这样做的好处就是效率非常高,不需要通过句柄池中转,但也因此失去了稳定性。

以上两种方式在各个语言和框架都有使用,而本文所讨论的HotSpot虚拟机使用的是直接指针方式,因为对象的访问是非常频繁的,这时效率就显得格外重要。

四、判断对象的存活

对象生死

JVM不需要我们手动释放内存,这是Java广受欢迎的原因之一,那么它是如何做到自动管理内存,回收不需要的对象的呢?既然要回收对象,那么就需要判断哪些对象是可以被回收的,即对象的死活判定,哪些对象不会再被引用?有两种实现方式:引用计数法可达性分析

  • 引用计数法:这个算法很简单,每个对象关联一个计数器,对象每被引用一次,计数器就加1,引用失效时,计数器就减一,垃圾回收时只需要回收计数为0的对象即可。这样做效率很高,但是这个算法有个显著的缺点,没法解决循环依赖,即A依赖B,B依赖A,这样它们的计数器都为1,但实际上除此之外没有任何地方引用它们了,就会导致内存泄露(即内存无法被释放)。
  • 可达性分析:相较于引用计数法,这个算法效率会低一些,但却是虚拟机采用的方式,因为它就能解决循环依赖的问题。该算法会将一部分对象作为GC Roots,然后以这些对象作为起点开始搜索,当一个对象到GC Roots没有任何途径可以到达时,则表示该对象可以被回收。问题就在于那些对象可以作为GC Roots呢?
    • 虚拟机栈(栈帧中的局部变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 常量池引用的对象
    • 本地方法栈中JNI(native方法)引用的对象

以上4种非常好理解,是重点,需要熟记于心,因为上面4种对象是在方法运行时或常量引用的对象,在对应的生命周期是肯定不能被GC回收的,作为GC Roots自然再合适不过。另外还有下面几种可以作为了解:

  • JVM内部引用的对象(class对象、异常对象、类加载器等)
  • 被同步锁(synchronized关键字)持有的对象
  • JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等
  • JVM中实现的“临时性”对象,跨代引用的对象

回收方法区

除了堆中对象需要回收,方法区中的class对象也是可以被回收的,但是回收的条件非常苛刻:

  • 该类的所有实例都已经被回收,堆中不存在该类的对象
  • 加载该类的ClassLoader已被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

可以看到方法区的回收条件是多么苛刻,所以方法区的回收率一般极低,因此可以通过-Xnoclassgc关闭方法区的回收,提升GC效率,但需要注意,关闭后将会导致方法区的内存永久被占用,导致OOM出现。

引用

通过上文我们可以发现,对象的存活判定都是基于引用,而Java中引用又分为了4种:

  • 强引用:平时我们使用=赋值就属于强引用,被强引用关联的对象,永远不会被GC回收。
  • 软引用(SoftReference):常用来引用一些有用但并非必需的对象,如实现缓存。因为软引用只会在要发生OOM之前检查并被回收掉,如果回收后空间仍然不足,才会抛出OOM异常。
  • 弱引用(WeakReference):比软引用更弱的引用,只要发生垃圾回收就会被回收掉的引用,也可以用来实现缓存。在Java中,WeakHashMap和ThreadLocal的键都是利用弱引用实现的(注意这两个类的区别,前者可以配合ReferenceQueue使用,当key被回收时会被加入到该队列中,继而在清除null key时直接扫描这个队列即可;而后者在清除null key时需要遍历所有的键。关于ThreaLocal后面会在并发系列中详细分析)。
  • 虚引用(PhantomReference):最弱的引用,一个对象是否有虚引用,完全不会影响到其生命周期,无法通过该引用获取到一个对象的实例,使用时需要和ReferenceQueue配合使用,而使用它的唯一目的就是在这个对象被垃圾回收时能够接收到一个通知。

对象的自我拯救

虚拟机提供了一次自我拯救的机会给对象,即finalize方法。如果对象覆盖了该方法,当经过可达性分析后,就会进行一次判断,判断该对象是否有必要执行finalize方法,如果对象没有覆盖该方法或者已经执行过一次该方法都会判定为该对象没有必要执行finalize方法,在GC时被回收。否则就会将该对象放入到一个叫F-Queue的队列中,之后GC会对该队列的对象进行二次标记,即调用该方法,如果我们要让该对象复活,那么就只需要在finalize方法中将该对象重新与GC Roots关联上即可。
该方法是虚拟机提供给对象复活的唯一机会,但是该方法作用极小,因为使用不慎可能会导致系统崩溃,另外由于它的运行优先级也非常低,常常需要主线程等待它的执行,导致系统性能大大降低,所以基本上可以忘记该方法的存在了。

五、对象的分配策略

上文说到对象是在堆中分配内存的,但是堆中也是分为新生代老年代的,新生代中又分了Edenfromsurvivor区,那么对象具体会分配到哪个区呢?这涉及到对象的分配规则,下面一一说明。

优先在Eden区分配

大多数情况,对象直接在Eden区中分配内存,当Eden区内存不足时,就会进行一次MinorGC(新生代垃圾回收,可以通过-XX:+PrintGCDetails这个参数打印GC日志信息)。

大对象直接进入老年代

什么是大对象?虚拟机提供了一个参数:-XX:PretenureSizeThreshold,当对象大小大于该值时,该对象就会直接被分配到老年代中(该参数只对Serial和ParNew垃圾收集器有效)。为什么不分配到新生代中呢?因为在新生代中每一次MinroGC都会导致对象在Eden、from和sruvivor中复制,如果存在很多这样的大对象,那么新生代的GC和复制效率就会极低(关于垃GC的内容后面的文章会详细讲解)。

长期存活的对象进入老年代

既然对象优先在新生代中分配,那么什么时候会进入到老年代呢?这就和上文讲解的对象头中的分代年龄有关了,默认情况下超过15岁就会进入老年代,可以通过-XX:MaxTenuringThreshold参数进行设置。那岁数又是怎么增长的呢?每当对象熬过一次MiniorGC后年龄都会增加1岁。

动态对象年龄判定

但是虚拟机并不是要求对象年龄必须达到MaxTenuringThreshold才能晋升老年代,当Survivor空间中相同年龄的所有对象的大小总和大于Survivor空间一半时,年龄大于或等于该年龄的对象就会直接晋升到老年代

空间分配担保

在发生MiniorGC之前,虚拟机首先会检查老年代中最大可用的连续空间是否大于新生代所有对象的总和,如果大于则进行一次MiniorGC;否则,则会检查HandlePromotionFailure设置值是否允许担保失败。如果允许则会检查老年代最大连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行一次MiniorGC,否则则进行一次FullGC。
为什么要这么设计呢?因为频繁的FullGC会导致性能大大降低,而取历次晋升老年代对象的平均大小肯定也不是百分百有效,因为存在对象突然大大增加的情况,这个时候就会出现担保失败的情况,也会导致FullGC。需要注意的是HandlePromotionFailure这个参数在JDK6Update24后就不会再影响到虚拟机的空间分配担保策略了,即默认老年代的连续空间大于新生代对象的总大小或历次晋升的平均大小就会进行MinorGC,否则进行FullGC。

总结

本文概念性的东西非常多,这是学习JVM的难点和基础,但这是绕不开的一道坎,读者只有多看,多思考,写代码复现文中提到的概念,才能真正的理解这些基础知识。另外还有垃圾是怎么回收的?有哪些垃圾回收器?怎么选择?这些问题将在下一篇进行解答。

原文地址:https://www.cnblogs.com/yewy/p/13363675.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


jinfo 命令可以用来查看 Java 进程运行的 JVM 参数,命令如下:[root@admin ~]# jinfo --helpUsage: jinfo [option] &lt;pid&gt; (to connect to running process) jinfo [option] &lt;executable &lt;core&gt; (to connect to a core file) jinfo [option] [serve
原文链接:https://www.cnblogs.com/niejunlei/p/5987611.htmlJava Virtual Machine Stacks,线程私有,生命周期与线程相同,描述的是Java方法执行的内存模型:每一个方法执行的同时都会创建一个栈帧(Stack Frame),由于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法的执行就对应着栈帧在虚拟机栈中的入栈,出栈...
java 语言, 开发者不能直接控制程序运行内存, 对象的创建都是由类加载器一步步解析, 执行与生成与内存区域中的; 并且jvm有自己的垃圾回收器对内存区域管理, 回收; 但是我们已经可以通过一些工具来在程序运行时查看对应的jvm内存使用情况, 帮助更好的分析与优化我们的代码;jps查看系统中有哪些java进程jps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息
1.jvm的简单抽象模型:  2.类加载机制     双亲委派模型是为了防止jdk核心类库被篡改,如果需要打破可以重写Classloader.loadClass方法。r 双亲委派模型:一个类加载器收到一个类的加载请求,他会先判断自身是否已存在该类,如果不存在上抛给上一级类加载器ClassLoad
堆外内存JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,比如Netty,广泛使用了堆外内存,但是这部分的内存并不归JVM管理,GC算法并不会对它们进行回收,所以在使用堆外内存时,要格外小心,防止内存一直得不到释放,造成线上故障。堆外内存的申请和释放JDK的ByteBuffe
1.springboot和tomcat2.springcloud的请求如何通过网关鉴权?3.springmvc启动时组件的加载顺序?4.mybatis如何同时更新三条记录5.hibernate实现级联更新6.一个web程序应用程序启动时的加载流程7.如何向www.baidu.com地址发出请求时,并获取相应?8.???9.谈谈你对tcp/iptelnetudp协
堆设置-Xms256M:初始堆大小256M,默认为物理内存的1/64-Xmx1024M:最大堆大小1024M,默认为物理内存的1/4,等于与-XX:MaxHeapSize=64M-Xmn64M:年轻代大小为64M(JDK1.4后支持),相当于同时设置NewSize和MaxNewSize为64M-XX:NewSize=64M:初始年轻代大小-XX:MaxNewSize=256M:最大年轻代大小(默认
一.概述收集算法(JVM之垃圾回收-垃圾收集算法)是内存回收的抽象策略,垃圾收集器就是内存回收的具体实现。JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。就像没有最好的算法一样,垃圾收集器
Java中的堆是JVM所管理的最大的一块内存空间,主要用于存放各种类的实例对象,如下图所示: 在Java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。新生代(Young)又被划分为三个区域:Eden、S0、S1。 这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包
JVM深入理解JVM(4)——如何优化JavaGC「译」 PostedbyCrowonAugust21,2017本文翻译自SangminLee发表在Cubrid上的”BecomeaJavaGCExpert”系列文章的第三篇《HowtoTuneJavaGarbageCollection》,本文的作者是韩国人,写在JDK1.8发布之前,虽然有些地
 JVM深入理解JVM(2)——GC算法与内存分配策略 PostedbyCrowonAugust10,2017说起垃圾收集(GarbageCollection,GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行在JVM上的语言,如Scala等)程序员在提升开
运行时数据区  线程独有本地方法栈、虚拟机栈、程序计数器这些与线程对应的数据区会随着线程开始和结束创建和销毁  整体公有元数据区(又称方法区)、堆区会随着虚拟机启动而创建,随着虚拟机退出而销毁 
java整个堆大小设置:Xmx和Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。永久代PermSize和MaxPermSize设置为老年代存活对象的1.2-1.5倍年轻代Xmx的设置为老年代存活对象的1-1.5倍老年代的内存大小设置为老年代存活对象的2-3倍BTW: Sun官方建议年轻代
栈顶缓存(Top-of-StackCashing)技术基于栈式架构得虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派次数和内存读写次数 由于操作数是存储在内存重的,因此频繁地执行内存读/写操作必然影响速度。 综上
自用。同样的代码在不同的平台生成的机器码是不一样的,为什么java代码生成的字节码文件,能在不同的平台运行?因为不同版本的jdk里面的虚拟机会屏蔽不同操作系统在底层硬件与指令上的区别。栈:线程栈,局部变量存放栈内存区域。线程(分配一个栈)运行分配栈将局部变量放入内存。怎么放:栈
jconsole监控:1.java启动命令加上参数java-Djava.rmi.server.hostname=172.16.17.247-Dcom.sun.management.jmxremote-Dcom.sun.management.jmxremote.port=2099-Dcom.sun.management.jmxremote.authenticate=false-Dcom.sun.management.jmxremote.ssl=false -XX:+Unlock
类加载器分类publicclassStackStruTest{publicstaticvoidmain(String[]args){//对用户自定义个类来说:默认使用系统类加载器进行加载-----AppClassLoaderClassLoaderclassLoader=StackStruTest.class.getClassLoader();System.out.p
堆体系结构一个JVM实例只存在一个堆内存,堆内存的大小是可调节的。类加载器读取类文件后,需要把类、方法、常量、变量放在堆内存中,保存所有引用类型的真实信息,以方便执行器指向,堆内存分为三个部分:年轻代、老年代、永久代。Java7之前,堆内存在逻辑上分为:年轻代、老年代、永久代。物
JVM深入理解JVM(5)——虚拟机类加载机制 PostedbyCrowonAugust21,2017在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机中,而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机中会发生什么变化?本文将逐步解答这
保存(持久化)对象及其状态到内存或者磁盘Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在