jvm内存分配机制

541 阅读7分钟

对象创建的过程

在new出一个对象的过程中,一般会经历上类加载检查、分配内存、初始化、设置对象头、执行init方法几个过程。

  • 类加载检查

    当虚拟机执行一条new指令的时候,会去检查下这个new指令的参数是否可以在常量池中定位到一个类的符号引用,同时还会去检查下这个类是否已经被加载过,如果没有被加载过的话 那么会先进行类的相关加载过程。

  • 分配内存

    在类被加载后进行类的内存分配,主要通过以下俩种方式:

    • 指针碰撞

      在内存分配绝对规则的情况下,会用指针记录上一次内存分配结束的地址。在进行下次的内存分配时,会从内存指针所在的位置往后进行分配。

    • 空闲列表

      在内存分配方式不规则的情况下,java内存是碎片的形式存在的,所以这个时候进行内存分配的时候需要维护一个列表,在列表查看哪些内存可以进行分配。在内存分配的时候找到一块内存足够的地址分配给新的对象。

  • 并发情况下怎么保证内存的分配?

    • 在并发的情况下会存在内存分配给对象A但是指针还没有来的急去修改,但是另一个线程同时进行对象的分配,也分配在同一块内存地址内的情况。 一般并发情况下 我们通过TLAB和CAS俩种方式进行并发情况下的内存的分配。

      • CAS :虚拟使用CAS加上失败重试的方式来保证分配的原子性,进行内存的分配。
      • TLAB :本地线程分配缓冲,把内存分配划分到不同的线程之中进行,每个线程都在java堆中预先分配一小块内存,可以通过调优参数-XX:+/UserTLAB L来设置是否启用 (JVM默认开启),-XX:TLABSize 来设置大小
  • 初始化

    内存分配结束后,虚拟机将要分配的空间都初始化为零值,此时还不包含对象头。如果启用了TLAB,那么这一过程也会提前在TLAB过程中完成。这一操作保证了java种对象可以在不赋值的情况下,就可以直接使用对象的字段。

  • 设置对象头

    对象头内的参数主要是包括:对象的锁状态、对象的分代年龄、对象的hashcode。

  • 执行对象的init方法

    主要是进行对对象的赋值操作,以及执行构造方法。

对象的内存分配方式

  • 大对象

    对于超过jvm参数大小的对象来说,会直接放入到old区。因为直接将大对象放置到old区中可以避免对象的复制降低效率。

  • 栈上分配

    我们通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内 存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的 内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。在jdk1.7以后默认开始对象的逃逸分析。 对象在栈内分配的时候,当确定对象不会被外部引用之后,便不会创建对象替代的是创建而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就 不会因为没有一大块连续空间导致对象内存不够分配。

  • eden分配

    正常new出来的对象 一般都是分在堆中的eden区。 其中eden和survivor的比列大小为:8:1:1,年轻代和老年代的比列为1:2。

  • 长期存活的对象将进入老年代 既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在 老年代中。为了做到这一点,虚拟机给每个对象一个分代年龄(存放在object head中)。对象在经历MinorGc以后还能在survivor中存活的,对象分代年龄就增加1岁,当对象分代年龄增加到一定程度 (默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

  • 对象动态年龄判断 当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的 50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了, 例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会 把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年 龄判断机制一般是在minor gc之后触发的。

  • 老年代空间分配担保机制 年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间 如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) 就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了 如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。 如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾, 如果回收完还是没有足够空间存放新的对象就会发生"OOM" 。

对象内存回收

  • 如何判断一个类是无用的类 方法区主要回收的是无用的类,类需要同时满足下面3个条件才能算是 “无用的类” :

    • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

    • 加载该类的 ClassLoader 已经被回收。

    • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  • 引用计数法

    • 此方式比较简单,当对象被引用的时候便会用一个计数器记录被引用的次数。当引用失效,计数便减一,但是此种方式无法解决循环引用的问题。

    • 可达性分析算法 将线程栈的本地变量、常量、本地方法栈的变量作为GC-ROOT根节点 进行对象标记,可以被标记的都是非垃圾对象,未被标记的都为垃圾对象,将在gc的时候进行对象回收。 -对象的几种引用类型 --强引用、软引用、弱引用、虚引用。

    • 强引用:普通的变量引用 1 public static User user = new User();

    • 软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放 新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。 1 public static SoftReference user = new SoftReference(new User()); 软引用在实际中有重要的应用,例如浏览器的后退按钮。

    • 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用 1 public static WeakReference user = new WeakReference(new User());

    • 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用。