内存分配

对象的生命周期

创建阶段

1.类加载检查

使用new关键字创建对象,检查 该new指令的参数 是否能在 常量池中 定位到一个类的符号引用,并检查该类是否已经被加载、解析、和初始化过,如果没有需要执行类初始化过程

2.内存分配

虚拟机将为对象分配内存,即把一块确定大小的内存从 Java 堆中划分出来,HotSpot虚拟机使用了多种技加快分配速度与保证线程安全。内存分配方法根据Java堆内存是否规整而定 ,而 Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
image.png
规整内存分配
指针碰撞方法,如果堆此区域内存是连续的(比如Eden区),因此技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度。
不规整内存分配
空闲列表,对于不规整内存,虚拟机维护着一个 记录可用内存块 的列表,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录对于多线程的内存分配,有以下两种解决方法:

  • 同步处理分配内存空间的行为,虚拟机采用 CAS + 失败重试的方式 保证更新操作的原子性
  • TLAB(Thread-Local Allocation Buffers),即每个线程在 Java堆中预先分配一小块内存,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。

    3.对象头初始化

    设置对象字段的值为空值,设置对象的对象头保证了对象的实例字段在使用时可不赋初始值就直接使用(对应值 = 0)如使用本地线程分配缓冲(TLAB),这一工作过程也可以提前至TLAB分配时进行。
    设置 这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中。至此,从 Java 虚拟机的角度来看,一个新的 Java对象创建完毕,但从 Java 程序开发来说,对象创建才刚开始,需要进行一些初始化操作。

    4.对象初始化

  • 从超类到子类对static成员进行初始化
  • 超类成员变量按顺序初始化,递归调用超类的构造方法
  • 子类成员变量按顺序初始化,子类构造方法调用
  • 一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段

    其他阶段

  1. 应用阶段,对象至少被一个强引用持有着。
  2. 不可见阶段,当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。简单说就是程序的执行已经超出了该对象的作用域了。
  3. 不可达阶段,对象处于不可达阶段是指该对象不再被任何强引用所持有。与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些GC root会导致对象的内存泄露情况,无法被回收。
  4. 收集阶段,当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize()方法,则会去执行该方法的终端操作。
  5. 终结阶段, 当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
  6. 重分配阶段,垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。

    对象内存结构

    对象头存储区域

  • 对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,该部分数据被设计成一个非固定的数据结构 以便在极小的空间存储尽量多的信息(会根据对象状态复用存储空间)
  • 对象类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

    数据区域

    对象真正有效的信息

    对齐填充区域

    因为HotSpot VM的要求对象起始地址必须是8字节的整数倍,且对象头部分正好是8字节的倍数。因此,当对象实例数据部分没有对齐时(即对象的大小不是8字节的整数倍),就需要通过对齐填充来补全。
    **

    对象访问

    一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。以最简单的本地变量引用:Object obj = new Object()为例:

  • Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;

  • new Object()作为实例对象数据存储在堆中;
  • 堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;

在Java虚拟机规范中,对于通过reference类型引用访问具体对象的方式并未做规定,目前主流的实现方式主要有两种:

通过句柄访问

  1. ![](https://cdn.nlark.com/yuque/0/2019/png/217875/1552386754167-3e09c482-743a-456f-8231-0d7fe97924de.png#align=left&display=inline&height=235&margin=%5Bobject%20Object%5D&originHeight=337&originWidth=695&size=0&status=done&style=none&width=485)<br />通过句柄访问的实现方式中,JVM堆中会专门有一块区域用来作为句柄池,存储相关句柄所执行的实例数据地址(包括在堆中地址和在方法区中的地址)。这种实现方法由于用句柄表示地址,因此十分稳定。

通过直接指针访问

                            ![](https://cdn.nlark.com/yuque/0/2019/png/217875/1552386777046-786b054e-d463-4e10-b453-5b40f53104c7.png#align=left&display=inline&height=210&margin=%5Bobject%20Object%5D&originHeight=339&originWidth=693&size=0&status=done&style=none&width=429)<br />通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快**,在HotSpot虚拟机中用的就是这种方式**。

引用类型

如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。 很多系统的缓存功能都符合这样的应用场景。这样就产生了多种引用类型。

  • 强引用,只要引用存在,垃圾回收器永远不会回收。Object obj = new Object()就是一个强引用,可直接通过obj取得对应的对象,只有当obj这个引用被释放之后,对象才会被释放掉。内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用 对象来解决内存不足的问题。
  • 软引用(SoftReference),软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。比较适合用来构建缓存。
  • 弱引用(WeakPreference),弱引用也是用来描述非必须对象的,他的强度比软引用更弱一些,被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联,那么这个对象就会被回收。ThreadLocal中的弱引用
  • 虚引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。** https://www.jianshu.com/p/825cca41d962

https://www.cnblogs.com/yw-ah/p/5830458.html#4194918

原始类型

指的就是java的几种基本数据类型,一般保存在方法区的常量池或者所在栈帧中。
https://blog.csdn.net/zj15527620802/article/details/80622314

内存回收

            ![image.png](https://cdn.nlark.com/yuque/0/2020/png/217875/1599287458745-32b6ead3-1e76-4cfd-aa25-c5304ecbf428.png#align=left&display=inline&height=522&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1044&originWidth=1186&size=195859&status=done&style=none&width=593)<br />Java内存分配和回收的机制概括的说,就是**分代分配,分代回收**。其理论理论依据来自于弱代假说:
  • 年轻的对象通常死得也快,而老对象则很有可能存活更长的时间
  • 只有很少的由老对象指向新生对象的引用

对象是否回收

算法

  • GC Roots:这个算法的基本思路就是通过一系列称为“GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连,则证明此对象可回收
  • 引用计数法,有循环依赖的问题,Java不使用**

第一次标记
将一系列的 GC Roots 对象作为起点,从这些起点开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连时,则判断该对象不可达,那它将会被第一次标记,GC Roots对象有

  • Java虚拟机栈(栈帧的本地变量表)中引用的对象
  • 本地方法栈 中 JNI引用对象
  • 方法区 中常量、类静态属性引用的对象

第二次标记
当对象经过了第一次的标记,该对象会被放到一个 队列中,并由 虚拟机自动建立、优先级低的Finalizer 线程去执行 队列中该对象的finalize(),在执行finalize()过程后,若对象依然没与引用链上的GC Roots 直接关联 或 间接关联(即关联上与GC Roots 关联的对象)改对象被回收。
老年代的对象对新生代的对象的引用
image.png
为了解决这个问题,老年代中存在一个”card table”,他是一个512 byte大小的块。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询card table来决定是否可以被收集,而不用查询整个老年代。这个card table由一个write barrier来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,但GC的整体时间被显著的减少。

回收机制

               ![image.png](https://cdn.nlark.com/yuque/0/2020/png/217875/1599284906413-531c09b1-81d0-4dfa-8887-5b000ff37f0c.png#align=left&display=inline&height=171&margin=%5Bobject%20Object%5D&name=image.png&originHeight=365&originWidth=1154&size=197738&status=done&style=none&width=541)<br />在Java程序中不能显式地分配和注销内存。有些人把相关的对象设置为null或者调用System.gc()来试图显式地清理内存。设置为null至少没什么坏处,但是调用System.gc()会显著地影响系统性能,必须彻底杜绝。针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
  • Partial GC:并不收集整个GC堆的模式
    • Young GC:只收集young gen的GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、meta space等所有部分的模式。

Major GC通常是跟Full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。

Minior GC

                                   ![image.png](https://cdn.nlark.com/yuque/0/2019/png/217875/1551840615256-8d0aa7cb-2dc4-4f8c-bb13-0eea72e22e38.png#align=left&display=inline&height=630&margin=%5Bobject%20Object%5D&name=image.png&originHeight=729&originWidth=442&size=174752&status=done&style=none&width=382)
  1. 绝大多数刚刚被创建的对象会存放在伊甸园空间,Eden区是连续的内存空间,因此在其上分配内存极快;
  2. 最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,在伊甸园空间执行了第一次GC之后,存活的对象被移动到SurvivorFrom。
  3. 下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间,然后清空Eden区;
  4. 当SurvivorFrom幸存者空间饱和之后,首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区); 然后,清空 Eden 和 ServicorFrom 中的对象 **

    复制算法

    为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一半的内存。 JVM对该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分。

    参数

  • -XX:SurvivorRatio: 此参数来配置Eden区域Survivor区的容量比值,默认是8。代表Eden:Survivor1:Survivor2=8:1:1。由于绝大部分的对象都是短命的,所以Eden区与Survivor的比例较大。如果系统中大部分对象存活时间较短,比例就调高一点,避免提前进入老年代,反之亦然。
  • XX:PretenureSizeThreshold: 大对象是指需要连续内存空间的对象,避免提前触发垃圾回收
  • XX:MaxTenuringThreshold : 为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中,默认为15次。虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代。

    Full GC

    老年代

    因为老年代存储的对象比年轻代多得多,如果复制算法效率非常低,所以老年代使用的是标记-整理算法。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间 。

  • -XX:+HandlePromotionFailure:在发生Minor GC时,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure参数。如果设置了此参数,则马上进行Full GC ;如果不设置,那么会继续查找老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次MinorGC是有风险的。如果小于,则进行一次Full GC。此参数jdk1.6及以后,默认启用。

    元空间

    元空间中会回收无用的类信息,无用的类进行回收,必须保证3点,1.类的所有实例都已经被回收2.加载类的ClassLoader已经被回收 3.类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

  • -XX:MetaspaceSize :设置初始的元空间大小。对于一个 64 位的服务器端 JVM 来说,其默认的 20.75MB,这就是初始的高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置,新的高水位线的值取决于 GC 后释放了多少元空间。如果释放的空间不足,那么在不超过 MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,通过垃圾回收的日志可观察到 Full GC 多次调用。为了避免频繁 GC,建议将 -XX:MetaspaceSize 设置为一个相对较高的值。

  • -Xnoclassgc
  • 类加载和卸载信息-verbose,-XX:+TraceClassLoadingXX:+TraceClassUnLoading

    算法

    标记-清除算法(Mark-Sweep)
    这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
    标记-整理算法(Mark-Compact)
    该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片。

**

垃圾收集器

在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同,各种不同的垃圾收集器可以配合使用,HotSpot 1.6版使用的垃圾收集器如下图,注意不同的新生代收集器只能与特定的老年代收集器相搭配。
内存管理 - 图3

新生代收集器

Serial
单线程进行GC,是Client模式下默认新生代垃圾收集器Stw

  • -XX:+UseSerialGC 使用Serial+Serial Old模式运行进行内存回收

ParNew
多个线程进行GC,Stw,关注缩短垃圾收集时间。

  • -XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;
  • -XX:ParallelGCThreads来 置执行内存回收的线程数。

Parallel Scavenge
多个线程进行GC,Stw,关注CPU吞吐量(即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃圾收集1分钟,则吞吐量是99%),根据当前系统运行的情况,自动调整运行参数,从而达到最大吞吐量的目的。这种收集器能最高效率的利用CPU,适合运行后台运算。

  • -XX:+UseParallelGC 使用Parallel Scavenge+Serial Old收集器组合回收垃圾(Server模式默认设置);
  • -XX:GCTimeRatio 设置用户执行时间占总时间的比例,默认99,即1%的时间用来进行垃圾回收。
  • -XX:MaxGCPauseMillis 设置GC的最大停顿时间
  • XX:+UseAdaptiveSizePolicy 可以进行动态控制,如自动调整Eden/Survivor比例,老年代对象年龄,新生代大小等。

    老年代收集器

    Serial Old
    单线程收集器,是Client模式下默认新生代垃圾收集器Stw
    Parallel Old
    多线程,关注CPU吞吐量,根据系统情况自适应调节,Stw

  • -XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。

CMS(Concurrent Mark Sweep)
致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,优点是并发收集(用户线程可以和GC线程同时工作),停顿小,但是会产生内存碎片,需要定期整理内存

  • -XX:+UseConcMarkSweepGC进行ParNew+CMS/Serial Old进行内存回收,优先使用ParNew+CMS,当用户线程内存不足时,采用备用方案Serial Old收集。
  • -XX:CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少之后进行一次垃圾回收,默认值为68%
  • -XX:CMSFullGCsBeforeCompaction 设置在几次CMS垃圾收集后,触发一次内存整理。
  • -XX:UseCMSCompactAtFullCollection 设置每次垃圾回收后是否要进行一次内存整理

回收过程
image.png

  • 初始标记(initial-mark) :标记 Roots 能直接引用到的对象,速度很快Stw
  • 并发标记(concurrent-mark):进行 GC Root Tracing
  • 重新标记(remark) :修正并发标记期间由于用户程序运行而导致的变动Stw
  • 并发清除(concurrent-sweep):进行清除工作

G1收集器

使用G1收集器后,虽然Jvm仍使用分代回收的策略,但是内存的布局在逻辑上已经发生了变化。G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间, 优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
CMS比较

  • 基于标记-理算法,不产生内存碎片。
  • 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

参数

  • -XX:+UseG1GC 使用G1收集器
  • -XX:MaxGCPauseMillis=n 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标。
  • -XX:InitiatingHeapOccupancyPercent=n 启动并发GC时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示”一直执行GC循环”. 默认值为 45.

回收过程

  • 初始标记(Initial Marking),初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,这阶段需要停顿线程,但耗时很短。Stw
  • 并发标记(Concurrent Marking) ,并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking)最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,这阶段需要停顿线程,但是可并行执行。Stw
  • 筛选回收(Live Data Counting and Evacuation)筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这样既能保证垃圾回收,又能保证停顿时间,而且也不会降低太多的吞吐量。Stw

Stw: stop-the-world会在任何一种GC算法中发生。Stop-the-world意味着 JVM 因为要执行GC而停止了应用程序的执行。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态,直到GC任务完成。GC优化很多时候就是指减少Stop-the-world发生的时间。之所以要”Stop-the-world”, 是因为在GC可达性分析时,必须要在一个可以保证一致性的快照中执行。也就是说,不可以出现在分析过程中对象的引用关系还在不断变化的情况,否则分析结果的准确性就不能够得到保证。

参考文档:
深入浅出Java垃圾回收机制:http://www.importnew.com/1993.html
Major GC和Full GC:https://www.zhihu.com/question/41922036/answer/93079526
引用计数:https://www.zhihu.com/question/44079404
javaGC机制:https://www.cnblogs.com/zhguang/p/3257367.html
http://www.importnew.com/16173.html#comment-672559
https://www.jianshu.com/nb/12222242