Java内存区域

运行时数据区域

  • 进程中线程共享:
    • 方法区
    • 直接内存
  • 进程中线程共享:
    • 程序计数器
    • 虚拟机栈
    • 本地方法栈

程序计数器(一块较小的内存空间)

  • 字节码解释器通过改变程序计数器的值,来选取下一个需要执行的字节码指令,实现代码的流程控制。如顺序执行、选择、循环、异常处理等。
  • 多线程情况下,程序计数器用于记录当前线程的执行位置,当线程切换回来可知道线程上次运行的位置。

各线程之间程序计数器互不影响,独立存储,这一块内存是线程私有的内存。

虚拟机栈

与程序计数器一样,Java虚拟机栈是线程私有的,用于描述Java方法执行的内存模型,每次方法调用数据都是通过栈传递。

Java内存可大致分为堆内存和栈内存,栈指的是虚拟机栈,或者是虚拟机栈的局部变量表部分。

局部变量表存放了编译期可知的8种基础数据类型,以及引用数据类型。

本地方法栈

作用与虚拟机栈相似,虚拟机栈为JVM使用的Java方法服务,本地方法栈为JVM使用的Native方法服务。

本地方法被执行时,在本地方法栈创建一个栈帧,用于存放本地方法的局部变量表、操作数栈、动态链接、出口信息。

  • 新生代内存
  • 老年代内存
  • 永久代内存(jdk1.8后改为元空间:直接内存)

JVM内存管理中最大的一块,堆是所有线程共享的内存区域,在虚拟机启动时创建。该区域的唯一目的就是存放对象实例,几乎所有对象实例及数组都在这分配内存。

Java堆是垃圾收集器的主要区域,也被称作GC堆

方法区(1.7永久代–>1.8元空间)

与堆一样,方法区是线程共享的内存区域,它用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

jdk1.8前HotSpot虚拟机把方法区当作永久代进行垃圾回收,难以确定大小,大小时常变化难以管理。jdk1.8后方法区移至元空间,位于本地内存中,而不是虚拟机内存。

运行时常量池

运行时常量池是方法区的一部分,Class文件中的常量池在类加载后放入该区域

直接内存

直接内存并非是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。

HotSpot

JVM相当于一个虚拟机规范,而HotSpot就是规范的具体实现。

对象的创建

  • 类加载检查

    虚拟机遇到一条new指令时,会检查指令的参数是否能在常量池中定位到这个类的符号引用,并检查该符号引用的类是否已加载、解析、初始化。若没有,则执行相应的类加载过程。

  • 分配内存

    在类加载检查后,虚拟机会为新对象分配内存。类加载完成后可确定对象所需的内存大小,此时只需要将确定大小的内存从Java堆中划分出来。Java堆有两种分配方式:指针碰撞,空闲列表。一般采用CAS来保证线程安全。

  • 初始化零值

    内存分配后,虚拟机及那个分配的内存空间都初始化为零值(不包括对象头),该操作是为了保证对象的实例字段可不赋初始值就直接使用,程序则会访问该实例字段对应类型的初始值。

  • 设置对象头

    初始化零值后,虚拟机会将对象的相关信息存放到对象头中,如实例属于哪个类、对象的哈希码、类的元数据信息等。

  • 执行init()方法

    虚拟机完成了对象创建,但对于程序来说,对象创建才刚开始,此时执行init()方法,初始化对象。

对象内存布局

对象在内存中布局可分为3快区域:对象头、实例数据、对齐填充。

  • 对象头:包含两部分信息,一部分用于存储对象自身运行时数据,另一部分是类型指针(对象指向其类元数据的指针),虚拟机通过该指针确定对象是某个类的实例。

  • 示例数据:对象真正存储的有效信息。

  • 对齐填充:不是必要的,仅仅起占位作用。由于HotSpot虚拟机要求对象大小必须是8字节的整数倍,通过对齐填充补齐。

对象的访问定位

通过句柄直接指针两种方式实现访问定位。

JVM垃圾回收

堆空间分配与垃圾回收

基本结构

35.png

新生代内存:eden、from survivor0(s0)、to survivor1(s1)

老年代内存:old memory

对象会优先在eden区域分别,若垃圾回收后还存活,就进入s0或s1,且对象年龄加1,当年龄达到一定程度后(默认是大于15),会晋升到老年代,也就是长期存活进入老年代。而大对象则会直接进入老年代。

  • Minor GC:新生代垃圾回收
  • Major GC:老年代垃圾回收
  • Mixed GC:对整个新生代与老年代进行垃圾回收
  • Full GC:清理整个堆和方法区

如何判断对象是否可回收

引用计数法

给对象添加引用计数器,每次引用+1,引用失败-1,计数器=0即对象可回收。

但由于对象之间会产生循环引用问题,主流虚拟机并没有使用该算法进行内存管理。

可达性分析算法

以GC Roots对象作为起点,可达到的对象是存活的,不可达的对象可被回收。

36.png

GC Roots的选取一般为以下几种:

  • 虚拟机栈中引用的对象
  • 本地方法栈(Native方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

四种引用类型

  • 强引用

    被强引用关联的对象不会回收

    使用 new 关键字来创建强引用。

  • 软引用

    软引用关联对象只有在内存不足时才会被回收

    使用 SoftReference类 来创建软引用。

  • 弱引用

    弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前。

    使用 WeakReference类 来创建软引用。

  • 虚引用

    一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到对象。

    为一个对象设置虚引用的唯一作用,就是该对象被回收时会收到一个系统通知。

    使用 PhantomReference类 来创建虚引用。

垃圾回收算法

标记–清除

37.png

先标记处所有不需要回收的对象,标记后统一回收没有被标记的对象,该算法是最基础的回收算法,其余算法都是对它的改进。

缺点:

  • 标记清除效率低
  • 会产生大量不连续的内存碎片,导致后续无法给大对象分配内存

标记–整理

38.png

让所有的存活对象向一端移动,随后清理除这一端边界以外的内存。

优点:不会产生内存碎片

缺点:移动大量对象,效率更低

标记–复制

39.png

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存使用完后,先将存活对象全部复制到另一块上,然后再将这一块内存清理。保证有一半的区域充当备用。

多数虚拟机采用该算法回收新生代,但不是划分2个相等内存块,而是一块较大Eden空间与两块较小的Survivor空间,每次都指使用Eden与其中一块Survivor,回收时将存活对象复制存放到另一块Survivor上,最后清理Eden与刚刚使用的这一块Survivor。

Eden与Suvivor大致分配8:1:1,这样保证了内存利用率可达到90%。若回收有多于10%的对象存活,那么一块Survivor会不够用,此时需要使用老年代的内存空间进行分配担保,借用老年代存储对象。

缺点:内存只能使用部分。

分代收集

一般虚拟机都采用该算法,它根据对象存活周期将内存划分成好几块,为每一块采用适当的算法。

比如说新生代每次都会有大量对象死亡进行回收,可以选择复制算法,只需要少利用部分内存空间九年完成垃圾回收。而老年代的对象几乎都是存活率高的,没有额外空间进行分配担保,需要清除或整理。

  • 新生代:标记–复制
  • 老年代:标记–清除 或 标记–整理

七种垃圾收集器

blog13.png

  • Serial收集器(Client)

    单线程收集器,可串行执行,相比其他单线程收集器更加简单高效,因为没有线程交互的开销。

  • ParNew收集器(Server)

    Serial收集器的多线程版本,除了支持多线程外和Serial一致,除了Serial外,只有它可以和CMS收集器配合使用。(并发)

  • Parallel Scavenge收集器

    多线程收集器,其他收集器在意的是用户线程的停顿时间,而它的关注点是吞吐量优先,即高效利用CPU时间。适合用于后台运算且无需太多交互的任务。

  • Serial Old收集器(Client)

    Serial老年代版本,单线程。主要有两大作用:一是JDK1.5以前与 Parallel Scavenge 搭配使用,而是作为CMS的后备方案。

  • Parallel Old收集器

    Parallel Scavenge的老年代版本,注重吞吐量或CPU资源敏感时,可考虑使用Parallel Scavenge + Parallel Old。

  • CMS收集器(Concurrent Mark Sweep)

    CMS的MS即Mark Sweep标记清除算法,此处实现步骤分4步:

    • 初始标记:标记能与GC Roots关联的对象,速度快,需要停顿。
    • 并发标记:进行GC Roots Tracing的过程,在整个回收过程中耗时最长,无需停顿。
    • 重新标记:为了修正并发标记期间用户程序继续运作,进而导致标记产生变动的部分对象的标记记录,需要停顿。
    • 并发清除:对未标记区域进行清理,无需停顿。
    • 优点:并发收集,低停顿。
    • 缺点:
      • 吞吐量低,CPU利用率不高。
      • 无法处理浮动垃圾,可启用Serial Old进行替代。
      • 标记–清除算法会产生空间碎片,使得没有大空间来分配对象,进而导致提前使用Full GC。
  • G1收集器

    一款面向服务端的垃圾收集器,在多CPU和大内存场景下有较好性能。

    运作步骤:

    • 初始标记
    • 并发标记
    • 最终标记:类似CMS的重新标记,需暂停线程,但可并行执行。
    • 筛选回收:对回收价值与成本进行排序,然后进行回收。

    特点:

    • 空间整合:整体基于 整理算法 实现,局部基于 复制算法 实现,即运行时不会产生空间碎片。
    • 可预测停顿:指明使用者在某长度时间片段内,GC不得超过相应的规定时间。

类加载机制

类的生命周期

40.png

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

类加载过程

包括了加载、验证、准备、解析、初始化5个阶段。

加载

通过类的完全限定名称获取定义类的二进制字节流。

将字节流表示的静态存储结构转换成方法区的运行时存储结构。

在内存中生成一个代表该类的Class对象,作为方法区中该类各种数据的访问入口。

验证

保证Class文件的字节流中包含的信息符合虚拟机的要求。

准备

类变量被static修饰,在准备阶段为类变量分配内存并设置初始值,使用的是方法区内存。

解析

将常量池符号引用替换成直接引用的过程。

初始化

初始化阶段才是真正开始执行类中定义的Java代码,初始化即虚拟机执行类构造器<clinit\>()方法的过程。准备阶段已经赋过以此初始值,而在初始化阶段会根据程序具体内容初始化类变量与资源。

双亲委派模型

41.png

该模型要求除顶层的启动类加载器外,其他类加载器都要有自己的父类加载器。这里父子关系通过组合关系实现,而非继承关系。

  • 工作流程

    一个类加载器首先将类加载请求转发给父类加载器,只有当父类加载器无法完成时才会尝试自行加载。

  • 优点

    双亲委派让类加载器之间具有优先级的层级关系,使得Java基础类稳定运行,避免类重复加载。