这篇文章主要讲Java内存的分配与回收机制,主要包括Java运行时的数据区域、对象的建立、垃圾收集算法与回收策略java
一.运行时数据区域
下图是Java虚拟机运行时的内存示意图:面试

从图中咱们能够看到Java内存总共分为6个部分:算法
- 程序计数器:每条线程都有一个独立的程序计数器,计数器能够看做是当前线程所执行的字节码的行号指示器。字节码解释器工做时,就是经过改变这个计数器的值来选取下一条所需执行的字节码指令、分支、循环、跳转、异常处理,线程恢复等基础功能都须要依赖这个计数器完成。
- Java虚拟机栈:虚拟机栈是线程私有的,生命周期与线程相同。虚拟机栈为Java方法执行描述内存模型,每一个方法在执行的同时会建立一个栈帧用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈:与虚拟机栈发挥的做用类似。区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务。
- 堆:全部线程共享的区域。在虚拟机启动时建立,全部的对象实例几乎都在堆上分配。Java堆还能够细分为:新生代和老年代,再细致一点有Eden空间、From Survivor空间、To Survivor空间。不过不管如何划分,存储的都是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。
- 方法区:方法区是各个线程共享的内存区域,主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。这块区域与Java堆同样不须要连续的内存和能够选择固定大小或可扩展外,还能够选择不实现垃圾收集。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,垃圾收集行为在这个区域较少出现。
- 运行时常量池:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各类字面符和符号引用,这部份内容在类加载后进入方法区的运行时常量池中存放。
- 直接内存:直接内存也称堆外内存,它不是虚拟机运行时数据区的一部分。JDK1.4后引入NIO类,是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可使用Native函数库直接在堆外分配内存,而后经过存储在Java堆中的DirectByteBuffer对象做为引用对这块内存进行操做。这样可以显著提升性能,避免Java堆和Native堆中来回复制数据。
因此经过表格的形式归纳以下:数组
数据区域 归纳 线程共享 程序计数器 当前线程所执行的字节码的行号指示器 否 虚拟机栈 为Java方法执行建立栈帧存储局部变量、操做数栈、动态连接、方法出口等信息 否 本地方法栈 与虚拟机栈相似,为Native方法服务 否 堆 存放对象实例 是 方法区 存储虚拟机已加载的类信息、常量、静态变量、即时编译后的代码等数据 是 运行时常量池 方法区的一部分,存放编译期生成的字面量和符号引用 是 直接内存 被分配在堆外的内存,性能高,不受Java堆的大小限制 是 二.对象的建立与内存布局安全
1.对象的建立性能优化

Java对象的建立架构
上图是对象建立的完整流程图,接下来作详细说明。并发
- 当虚拟机收到new指令后,检查这个指令的参数是否能在常量池中定位到一个类的符号引用,而且检查这个符号引用所表明的类是否已被加载、解析和初始化过。若是没有,必须先执行类加载过程。
- 在类加载完成后能够肯定对象分配所须要的空间。若是Java堆中内存是绝对规整的,用过的内存放一边,空闲的内存放另外一边,中间放着一个指针做为分界点的指示器,那分配内存就只是把指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为"指针碰撞"。若是Java堆中内存不是规整的,空闲内存与使用过的内存是相互交错的,虚拟机必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找出足够的空间分配给对象实例,并更新列表上的记录,这种分配方式称为"空闲列表"。采用哪一种分配方式一般由虚拟机的垃圾收集器是否带有压缩整理功能决定。
- 划分可用空间时,还需考虑为对象实例分配空间时是不是线程安全的。要保证线程安全,有两种方案。一种是对分配内存空间的动做进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操做的原子性。另外一种是把内存分配的动做按照线程划分在不一样空间中进行,每一个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer , TLAB)。哪一个线程要分配内存,就在哪一个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才须要同步锁定。
- 内存分配完成后,虚拟机对分配到的内存空间都初始化为零值(不包括对象头),保证对象的实例字段在Java代码中能够不赋初始值就能够直接使用。
- 虚拟机将对象的信息放入对象的对象头中。
- 执行构造函数
2.对象的内存布局分布式

对象的内存布局总共分为三个部分:函数
- 对象头中主要包括两部分信息:
- 一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。若是对象是Java数组,那在对象头中还必须有一块记录数组长的数据。
- 实例数据部分是对象真正存储的有效信息,也是程序代码中定义的各类类型的字段内容。从父类继承下来的,在子类中定义的都须要记录下来。
- 对齐填充仅仅起到占位符的做用。HotSpot VM的自动内存管理系统要求对象起始地址是8字节的整数倍,因此对象大小必须是8字节的整数倍。当对象实例数据部分没有对齐时,须要经过对齐填充来补
在此我向你们推荐一个Java高级群 :725633148 里面会分享一些资深架构师录制的视频录像:(有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构、面试资料)等这些成为架构师必备的知识体系 进群立刻免费领取,目前受益良多!
三.内存的回收
1.对象存活断定
Java虚拟机经过可达性分析来断定对象是否存活。这个算法的基本思想是经过一系列称为"GC Roots"的对象做为起始点,从这些节点向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有与任何引用链相连时,则该对象是不可用的。
如图,object5,object6,object7虽然互有关联,可是GC Roots是不可达的,因此它们被断定是可回收的对象。
另外值得一提的是引用计数算法,引用计数法是经过给对象一个引用计数器,每当有一个地方引用它时,计数器值就加一;引用失效时,计数器值就减一;任什么时候刻计数器为0的对象就是不可能再被使用的。引用计数器效率高、实现简单。可是很难解决对象间相互循环引用的问题,主流Java虚拟机几乎都再也不使用引用计数法来管理内存。

可达性分析示意图
即便在可达性分析算法中不可达的对象,也不必定会当即被回收。一个对象被回收,至少要经历两次标记过程。
若是对象在进行可达性分析后没有与GC Roots相连的引用链,那它将会被第一次标记并进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已被虚拟机调用过,虚拟机将这两种状况视为"没有必要执行"。
若是这个对象断定为有必要执行finalize()方法,那么这个对象会放置在F-Queue队列中,稍后由虚拟机自动创建、低优先级的Finalizer线程去执行finalize()方法。GC对F-Queue中的对象进行第二次小规模标记,若是对象从新与引用链上的任何一个对象创建关联,那么第二次标记时它将被移除"即将回收"的集合。不然对象就真的要被回收了。

Finalize方法
2.方法区回收断定
方法区的回收主要包括两部份内容:废弃常量和无用的类。
- 废弃常量的回收与回收Java堆中的对象相似。
- 判断无用的类的条件必须知足三个条件:
- 该类全部实例已经被回收。
- 加载该类的ClassLoader已被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,也没法经过反射访问该类。
3.垃圾收集算法
- 标记-清除算法(Mark-Sweep):
- 算法分为"标记"和"清除"两个阶段:首先标记出须要回收的对象,在标记完成后统一回收被标记的对象。它主要不足有两个:一是效率问题,标记和清除两个过程效率都不高。二是空间问题,标记清除后会产生大量不连续内存碎片,碎片太多可能致使要分配较大对象时,没法找到足够的内存空间不得不提早触发一次垃圾收集动做。

-
- 标记-清除
- 复制算法:
- 复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当一块内存用完了,将存活的对象复制到另外一块上面,而后把已使用的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。只是这种算法将内存缩小为原来的一半,代价较高。

-
- 复制算法
- 标记-整理算法(Mark-Compact):
- 标记过程与"标记-清除"算法同样,但后续不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存

-
- 标记-整理算法
4.分代收集算法
商业虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期将内存划分为几块。Java堆分为新生代和老年代,这样能够根据年代特色采用适当的收集算法。新生代中每次垃圾收集都有大批对象死去,那就选用复制算法。老年代对象存活率高,没有额外空间进行分配担保,适合使用"标记-清理"或"标记-整理"算法来回收。
4.内存分配与回收策略
- 对象优先在Eden分区:
- 大多数状况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机发起一次Minor GC。GC后对象尝试放入Survivor空间,若是Survivor空间没法放入对象时,只能经过空间分配担保机制提早转移到老年代。
- 大对象直接进入老年代:
- 大对象指须要大量连续内存空间的Java对象。虚拟机提供-XX:PretenureSizeThreshold参数,若是大于这个设置值对象则直接分配在老年代。这样能够避免新生代中的Eden区及两个Survivor区发生大量内存复制。
- 长期存活的对象进入老年代:
- 虚拟机会给每一个对象定义一个对象年龄计数器。若是对象在Eden出生而且通过一次Minor GC后任然存活,且可以被Survivor容纳,将被移动到Survivor空间中,而且对象年龄设为1.每次Minor GC后对象任然存活在Survivor区中,年龄就加一,当年龄到达-XX:MaxTenuringThreshold参数设定的值时,将会移动到老年代。
- 动态年龄判断:
- 虚拟机不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold设定的值才会将对象移动到老年代去。若是Survivor中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象能够直接进入老年代。
- 空间分配担保:
- 在Minor GC前,虚拟机会检查老年代最大可用连续空间是否大于新生代全部对象总空间,若是条件成立,那么Minor GC是成立的。若是不成立,虚拟机查看HandlePromotionFailure设置值是否容许担保失败。若是容许,那么会继续检查老年代最大可用连续空间是否大于历次移动到老年代对象的平均大小,若是大于,将尝试一次Minor GC。若是小于,或者HandlePromotionFailure设置值不容许冒险,那将进行一次Full GC。
新生代GC(Minor GC):发生在新生代的垃圾收集动做,由于Java对象大多朝生夕死,因此Minor GC很是频繁,回收速度也较快。
老年代GC(Major GC/Full GC):发生在老年代的垃圾收集动做。出现Major GC,常常会伴随至少一次Minor GC。Major GC的速度通常比Minor GC慢10倍以上。
在此我向你们推荐一个Java高级群 :725633148 里面会分享一些资深架构师录制的视频录像:(有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构、面试资料)等这些成为架构师必备的知识体系 进群立刻免费领取,目前受益良多!