JVM之内存

1、JVM内存区域

  • 方法区
  • 虚拟机栈
  • 本地方法栈
  • 程序计数器
image.png

方法区

  所有线程共享
所有对象实例及数组都需要在堆上分配(随着栈上分配、标量替换等,也不那么绝对了)
  可以处于物理上不连续的内存空间,当堆中没有内存用于实例分配,堆也无法再扩展时,抛出OutOfMemoryError异常。

虚拟机栈

  为虚拟机执行Java方法(也就是字节码)服务
  线程私有

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

本地方法栈

  为虚拟机执行Native方法服务
  线程私有

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

程序计数器

  当前线程所执行的字节码行号指示器

  • 若线程正在执行的是Java方法,则为虚拟机字节码指令地址
  • 若线程正在执行的是Native方法,则为空
是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

2、对象的创建

  • 指针碰撞
    所有用过的内存放在一边,没有用过的内存放在另一边。仅需要移动中间的临界点指针
  • 空闲列表
    通过列表记录哪些内存块是可用的,在分配时从列表中找到足够大的内存
采用哪种方式由Java堆是否规整决定
Java堆是否规整由垃圾收集器是否带压缩整理功能决定

对象创建非常频繁,存在并发问题:

  • 方案一:对分配内存空间的动作进行同步处理:虚拟机采用了CAS配上失败重试的方式保证更新操作的原子性
  • 方案二:按照线程划分不同的块,每个线程在Java堆中预先分配一小块内存,成为本地线程分配缓冲(TLAB)

2、对象的访问

  • 句柄
  • 直接指针

内存溢出:(out of memory)通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。

内存泄漏:(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果

如果是建立过多线程导致的内存溢出,在不能减少线程数或更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

3、 GC算法

程序计数器、虚拟机栈、本地方法栈不需要过多考虑回收的问题

仅需关注Java堆和方法区

3.1对象存活还是死亡

  • 引用计数法
    很难解决对象间相互循环引用的问题
  • 可达性分析算法
    使用“GCRoots对象”作为起点。
    可作为GC Roots对象:
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的变量

3.1.1引用类型

https://www.jianshu.com/p/825cca41d962

  • 强引用
    只要强引用存在,被引用对象永远不会被回收
  • 软引用 SoftReference
    系统在发生OOM之前,将这些对象列进回收范围,如果回收后依然没有足够内存,才抛出OOM
  • 弱引用 WeakReference
    对象仅会存活到下一次垃圾收集发生之前。get方法可以访问被引用实例。
  • 虚引用
    对象仅会存活到下一次垃圾收集发生之前。get方法返回null。为一个对象设置虚引用的唯一目的就是能在对象被回收时受到通知。

3.1.2对象死亡

对象真正死亡至少要经历两次标记过程

  1. 对象在可达性分析后没有与GC Roots存在引用链,会被第一次标记。
  • 若对象没有覆盖finalize()方法或finalize()方法已被虚拟机调用过,被认为没有必要执行finalize()方法。
  • 其余均被认为有必要执行finalize()方法

2.有必要执行finalize()方法的对象会被放入F-Queue队列中,稍后被虚拟机建立的低优先级的Finalize线程执行。(虚拟机仅保证调用finalize方法,但不保证等待它执行结束,避免执行缓慢、死循环等)。
虚拟机会对F-Queue队列中的对象进行第二次标记,如果对象在finalize方法中与引用链上的任何一个对象建立了关联,则被移出待回收集合。

如果这之后对象仍然在待回收集合,则对象已死亡,等待垃圾回收。

任何对象的finalize方法仅会被调用一次,之后再也不会被调用

方法区(永久代)的垃圾回收主要回收:废弃常量+无用的类
废弃常量与堆中的对象类似,没有被引用则清出常量池
无用的类必须满足:

  • 该类所有的实例都已被回收,java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
    满足以上3个条件仅可以被回收,而不是必然被回收

4、 GC算法

4.1标记-清除算法

标记全部需回收对象,在标记完成后统一回收所有被标记的对象。

  • 效率问题:
    标记和清除效率都不高
  • 空间问题:
    产生大量不连续内存碎片,导致分配大对象时不得不重新触发GC

4.2复制算法(用于新生代)

将可用内存分为大小相等的两块,每次只使用一块,GC时将存活的对象复制到另一块。全部清除原来那半。
*空间问题:
内存浪费,代价太高

由于新生代中98%的对象都是“朝生夕死”,并不需要1:1划分内存空间。
采用1块较大的Eden空间和2块较小的Survivor空间。
每次使用1块Eden和1块Survivor空间,清理时将存活对象复制到1块Survivor空间。全部清除原来的1块Eden和1块Survivor空间。

当Survivor空间不够用时,需要老年代进行分配担保。

4.3标记-整理算法(用于老年代)

标记全部需回收对象,将所有存活对象向一端移动。然后直接清理掉端边界以外的内存。

4.4分代收集算法

新生代采用复制算法,老年代采用标记-清除或标记-整理算法。

5、 垃圾收集器

5.1Serial收集器

  • 仅用单线程完成垃圾收集工作
  • 进行收集时,必须暂停其他所有工作线程

新生代单线程复制算法,暂停所有工作线程
老年代单线程标记-整理算法,暂停所有工作线程

单CPU环境下,简单而高效

5.2 ParNew收集器

  • Serial收集器的多线程版本

新生代多线程复制算法,暂停所有工作线程
老年代单线程标记-整理算法,暂停所有工作线程

仅Serial和ParNew收集器可以和CMS配合工作
因为Parallel Scavenge和G1都没有采用传统的GC收集器代码框架

5.3 Parallel Scavenge收集器(新生代收集器)

新生代多线程复制算法

Parallel Scavenge收集器的目的是达到一个可控制的吞吐量。(CPU运行用户代码时间/总时间)

  • 停顿时间短适合与用户交互的程序
  • 高吞吐量适合高效利用CPU,适合后台运算而不需要太多交互的任务

可设置两个参数控制吞吐量:

  • 最大垃圾收集停顿时间
  • 直接设置吞吐量大小

需注意,GC停顿时间牺牲了吞吐量新生代空间
收集300M的新生代肯定比500M要快,但之前10秒收集一次每次100ms,现在5秒一次,一次70ms,吞吐量也相应下降。

5.4 Serial Old收集器(老年代收集器)

  • Serial 收集器的老年代版本
    老年代采用单线程标记-整理算法,暂停所有工作线程

  • 给Client模式下的虚拟机使用

  • 给Server模式下的虚拟机使用

    • 与Parallel Scavenge收集器配合使用
    • 作为CMS收集器的后备预案,在并发发生Concurrent Mode Failure时使用。

5.5 Parallel Old收集器(老年代收集器)

  • Parallel Scavenge收集器的老年代版本
    老年代采用多线程标记-整理算法,暂停所有工作线程

在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+ Parallel Old的组合。

5.6 CMS收集器

*是一种以获取最短回收停顿时间为目标的收集器

1.初始标记,stop the world
2.并发标记
3.重新标记,stop the world
4.并发清除

  • 对CPU资源非常敏感。
    并发时导致用户进程变慢。CMS默认启动线程数是(CPU数量+3)/4
  • 无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。
    因为垃圾收集阶段用户线程还是继续运行。
    CMS不能等到老年代几乎完全被填满再开始收集,若收集过程中预留的内存无法满足程序需要,则会出现Current Mode Failure失败。虚拟机临时启动后备预案Serial Old
  • CMS基于标记-清除算法,会有大量空间碎片产生。

5.7 G1收集器

1.初始标记
2.并发标记
3.最终标记
4.筛选回收

G1将Java堆划分为多个大小相等的区域。新生代老年代不再物理隔离。
在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测的停顿

6、 内存分配

6.1 对象优先在Eden区分配

Eden区没有足够空间时,出发Minor GC
(新生代Minor GC
老年代Major GC / Full GC,一般比Minor GC慢10倍以上)

6.2 大对象直接进入老年代

常见的很长的字符串和数组

6.3 长期存活的对象进入老年代

对象在Eden区出生并经历一次Minor GC后仍能被Survivor容纳的话,记为1岁,移动代Survivor中。
每经历一次Minor GC就增加1岁。
当年龄增加到一定程度(默认15岁),就会被晋升到老年代中。

6.4 动态对象年龄判断

如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象就直接进入老年代,无需等到要求的年龄。

6.5 空间分配担保

在Minor GC前,检查老年代最大可用连续空间是否大于新生代空间总和。若大于则Minor GC可以安全进行。否则,则查看是否允许担保失败。若允许担保失败,则查看老年代最大可用连续空间是否大于历次新生代晋升到老年代的平均大小。如果大于则尝试进行Minor GC(虽然有风险),否则进行Full GC。

SafePoint

由于 Full GC(或Minor GC) 会影响性能,所以我们要在一个合适的时间点发起 GC,这个时间点被称为 Safe Point,这个时间点的选定既不能太少以让 GC 时间太长导致程序过长时间卡顿,也不能过于频繁以至于过分增大运行时的负荷。一般当线程在这个时间点上状态是可以确定的,如确定 GC Root 的信息等,可以使 JVM 开始安全地 GC。Safe Point 主要指的是以下特定位置:

循环的末尾
方法返回前
调用方法的 call 之后
抛出异常的位置 另外需要注意的是由于新生代的特点(大部分对象经过 Minor GC后会消亡), Minor GC 用的是复制算法,而在老生代由于对象比较多,占用的空间较大,使用复制算法会有较大开销(复制算法在对象存活率较高时要进行多次复制操作,同时浪费一半空间)所以根据老生代特点,在老年代进行的 GC 一般采用的是标记整理法来进行回收。

image.png

你可能感兴趣的:(JVM之内存)