内存分配

数据区域

1.8之前

  • 线程共享:堆、方法区
  • 线程私有:本地方法栈、虚拟机栈、程序计数器

1.8之后

  • 线程共享:堆、元空间
  • 线程私有:本地方法栈、虚拟机栈、程序计数器

程序计数器

作用

  1. 读取指令,实现流程控制(选择、循环、顺序、异常处理)
  2. 多线程情况下,记录线程当前位置
  3. 唯一不会出现OOM的地方,随线程生命周期决定

虚拟机栈

会出现两个错误。StackOverFlowErrorOutOfMemoryError

  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
  • 每次调用方法都会创建栈帧(包括局部变量表、操作数栈、动态链接、返回地址)
  • 用来保存局部变量、参加方法计算与返回

本地方法栈

  • 使用Native方法服务,也会出现上述两种错误
  • Native方法使用C语言编写

  • 内存中最大的一块。共享区域,存放对象和数组,都会在这里分配内存
    不是所有对象,有个逃逸的概念。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
  • 是垃圾回收的主要区域
  • 1.8以前,堆内存通常分为:新生代、老生代、永久代
  • 1.8之后移除永久代,加入元空间,使用的直接内存(物理内存)
  • 新生代又分为eden区和survivor区,survivor区分为from survivor 和 to survivor又叫s0,s1

OOM的几种形式
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值

方法区

  • 放常量、静态变量,类加载信息,又被称为永久代。于1.8之后移除,用元空间代替
  • 类信息包含版本、字段、方法、接口、父类等信息

元空间

  • 1.8后出现,使用本地内存

运行时常量池

方法区的一部分,存放类信息和常量池表,当无法申请到内存时会OOM。字符串常量池在堆

直接内存

也会出现OOM的问题,受到本机总内存大小和处理器寻址空间的限制

HotSpot虚拟机

Java创建对象的过程

  1. 类信息加载
  2. 分配内存
  3. 初始化零值
  4. 设置对象头
  5. 执行init方法

类信息加载

一个判断过程,看是否需要类加载。虚拟机遇到new指令,判断在是否能在常量池中定位到这个类的引用,是否被加载,解析和初始化过。都没有执行类加载过程

分配内存

在类加载完后确定内存大小,从堆中划分一块区域。

内存分配方式

由堆决定,是否规整。分为指针碰撞空闲列表两种。
规整则碰撞,有用过的放一边,没用过的放另一边。指针指向没用过的一块区域。代表GC:serial、parnew
不规整则空闲:虚拟机维护一个表,记录哪些内存可用,分配的时候,给一块足够大的。代表GC:CMS

如何保证创建对象线程安全

CAS+失败重试。保证更新操作的原子性
TLAB:首先在TLAB分配,如果不够,再采取上述的CAS分配

初始化零值

初始化一个值,保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用

设置对象头

初始值完成后,虚拟机将对象的一些信息放于对象头中(类的元数据信息、对象的哈希码、对象的 GC 分代年龄)

init

对于虚拟机来说,对象已经创建完了,从程序视角看,创建才刚开始,init会按照程序员的意愿进行初始化

垃圾回收

基本结构

堆是内存回收的主要区域,又称为GC堆
heap

-xmn:设置新生代堆大小,xms:设置最小堆空间,xmx:设置最大堆空间

一般情况下,对象从eden区分配,gc后,如果还存活,进入s0或s1,年龄+1,默认增加到15岁,会放入老年代中,可以通过参数 -XX:MaxTenuringThreshold 来设置老年代默认岁数。大对象(需要连续调用内存空间,字符串,数组)则直接进入老年代

GC分类

  • 部分收集:
  • 新生代(Minor GC / Young GC)只收集新生代
  • 老年代(Major GC / Old GC)只收集老年代
  • 混合收集(Mixed GC)整个新生代和部分老年代
  • 整堆收集 (Full GC):收集整个 Java 堆和方法区。

判断对象死亡

在gc收集的时候,首先会判断对象是否已经死亡。常见的算法有引用计数法、可达性分析算法

引用计数法

如果有一个地方引用计数器+1,反之-1,为0的时候表示对象不可在被使用。主流虚拟机不会采用此算法,难以解决对象相互循环引用的问题

可达性分析算法

以GC roots为起点,向下搜索,如果走不通,则对象不可用

判断类无用

达到下面3个条件就可以被回收了

  1. 所有类实例被回收,不存在该类的实例
  2. classloader被回收
  3. java.lang.Class对象没有在任何地方被引用,无法通过反射方式获取到该类

回收算法

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法

标记-清除

分为两阶段,标记和清除(清除不标记的)首先标记处不需要回收的对象,在标记完成后统一回收掉没有被标记的对象,最基础的算法

带来的问题

  • 效率不高
  • 空间问题(不连续的碎片)

标记-复制

解决上述效率问题,将内存分为大小相同的两块,每次使用其中的一块,这块的用完后,将存活的对象复制到另外一块内存中去,再将先前一半清空。这样每次回收都是内存的一半

标记-整理算法

标记对象后,让存活的对象向一端移动,在清理边界之外的内存

分代收集算法

主流的回收算法。根据存活周期将内存分为几块,一般分为新生代和老年代,再根据不同的代选择不同的算法

在新生代中,每次回收都有大量对象死去,可选择标记-复制算法(只需要付出少量对象的复制成本就可以完成每次垃圾收集)
老年代对象存活几率高,也没有额外空间分配担保(新生代内存不够了,放到老年代中),必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

收集器种类

上述方法,下述具体实现

  • serial
  • parnew
  • paraller scavenge
  • cms
  • G1

没有最好的收集器,只有最适合的收集器

新生代回收器:Serial、ParNew、Parallel Scavenge 一般采用的是复制算法

老年代回收器:Serial Old、Parallel Old、CMS 一般采用的是标记-整理的算法

整堆回收器:G1

serial

串行收集器,最基本的收集器,单线程,工作的时候必须停止其他的工作线程。但简单而高效(与单线程比)
新生代采用标记-复制算法,老年代采用标记-整理算法

parnew

serial的多线程版本,新生代采用标记-复制算法,老年代采用标记-整理算法

paraller scavenge

  • 同parnew,关注点是吞吐量(CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。吞吐量越高,cpu利用率越高),CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
  • 新生代采用标记-复制算法,老年代采用标记-整理算法。
  • jdk8的默认收集器 = paraller scavenge + Parallel Old(Parallel Scavenge的老年代版本)
  • 注重吞吐量可以选择该收集器

CMS

  • 注重用户体验,获取最短停顿时间为目标
  • hotspot真正意义上的并发收集器,实现了垃圾收集线程和用户线程同时工作(基本)
  • 标记-清除算法实现
  • 对CPU资源敏感
  • 无法处理浮动垃圾
  • 有空间碎片

G1

  • 面向服务器,满足停顿时间还有高吞吐量

ZGC

  • 标记-复制算法,不过做了重大改进

什么时候FGC

  • system.gc()
  • 老年代空间不足
  • 永久代空间不足