文档章节

JVM理解-内存管理

后海
 后海
发布于 2019/08/01 14:58
字数 3843
阅读 27
收藏 0

JVM理解 —— 内存管理

Java不是由开发人员来显示分配内存和回收内存,而是由JVM来 自动管理内存的分配和回收(又称为:垃圾回收Garbage Collection:GC)。这降低了开发的难度,但是实际使用中遇到的问题就是由于不清楚JVM的 内存分配 和 回收机制,造成内存泄漏,最终导致JVM内存不够用。

1. 内存空间

JDK遵照 JVM规范进行内存区域的划分,如下:

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • 方法区(非堆)

1.1 程序计数器

数据活动范围:

  • 当前线程私有

说明:

  • 占用一块较小的内存空间。
  • 可以看做是当前线程所执行的 字节码 的 行号指示器。

抛出异常:

Java虚拟机栈

数据活动范围:

  • 当前线程私有;生命周期与线程相同

说明:

  • Java方法执行的内存模型:每个方法在执行的同时创建一个栈帧(Stack Frame),用以存储:

    • 局部变量表
    • 操作数栈
    • 动态连接
    • 返回方法地址

    每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中执行 入栈 到 出栈 的过程。

抛出异常:

  • 线程请求的栈深度大于虚拟机所允许的深度,抛出:StackOverflowError。
  • 无法申请到足够内存时,抛出:OutOfMemoryError。

1.2 本地方法栈

数据活动范围:

  • 当前线程私有

说明:

  • 与 虚拟机栈发挥的作用类似,不同点:
    • 虚拟机栈为虚拟机执行Java方法(字节码)服务
    • 本地方法栈为虚拟机执行的 Native方法服务

抛出异常:

  • StackOverflowError。
  • OutOfMemoryError。

1.3 方法区(非堆)

数据活动范围:

  • 所有线程公有

说明:

  • 存储已被 虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。
  • 永久代
  • 运行时常量池,存放编译期生成的各种
    • 字面量
    • 符号引用
    • 直接引用
    • 具有动态性(相对于常量池)

抛出异常:

  • OutOfMemoryError。

1.4 堆

数据活动范围:

  • 所有线程公有

说明:

  • 是Java虚拟机所管理的内存中最大的一块,
  • 唯一目的:存放所有对象实例、数组
  • 是垃圾收集器管理的主要区域,实行:分代收集算法(JDK 1.2开始):
    • 新生代:Eden Space,Survivor Space(From Space)+ Survivor Space (To Space)(From:To = 1:1)
    • 旧生代(新:旧 = 1:2)
  • TLAB(Thread Local Allocation Buffer) 线程私有缓冲区

抛出异常:

  • OutOfMemoryError。

1.4.1 新生代(New Generation)

1.4.1.1 新生代内存由:

  • Eden Space
  • 2块相同大小的 Survivor Space 构成,也被称为:From Space 和 To Space
  • Eden Space 与 From Space 与 To Space 所占新生代内存空间的比值= 8:1:1
  • 新建对象只会存在于 Eden Space 和 From Space 中, To Space 是空的 —— 此种分配方式与 复制回收算法 有关。

不同的GC方式会以不同的方式来划分或者根据运行状态调整 Eden Space 和 Survivor Space 的大小。

大多数情况下Java程序中所有新建的对象都是从 新生代 的内存中分配的,当 Eden Space 不足时,会把存活对象转移动 Survivor Space 中。

新创建的对象一般情况下都会分配到 Eden Space 中(大对象、大数组分配到 旧生代 中),当对象经过第一次 Minor GC 后(对象年龄+1),如果仍然存活则将存活对象移动至 Survivor Space 中,当对象的年龄达到一定的岁数时,将被移动至 旧生代中。

1.4.1.2 为什么 新生代中会有 Survivor Space?

  • 如果没有 Survivor Space , Eden Space 中每进行一次 Minor GC后,存活的对象就会被送到 旧生代 中,这样就会导致 旧生代 很快被填满,发生 Major GC或 Full GC。

    但由于 旧生代 内存空间远大于 新生代 空间,所以进行一次 Full GC 会消耗大量的时间。而且若是在无 Survivor Space的情况下,旧生代 被填满的速度大大加快,结果就是导致 频繁 的发生 Full GC,这将会影响程序的执行和响应速度,更有甚至会因为 超时 产生其他问题。

    在无 Survivor Space 的情况下,如何解决?

方案 优点 缺点
增加老年代空间 更多存活对象才能填满老年代。降低Full GC频率 随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长
减少老年代空间 Full GC所需时间减少 老年代很快被存活对象填满,Full GC频率增加

上述两种解决方案都不能从根本上解决问题。

结论一:

Survivor Space 存在的意义就是 减少被移动至 旧生代 中的对象,进而减少 Full GC 发生。

1.4.1.3 为什么 Survivor Space 会有 From Space 与 To Space 2块空间?

由上文已经说明了为什么没有 Survivor Space 不行,这次假设只有 1块 Survivor Space

当只有 1块 Survivor Space 时,新建对象填满了 Eden Space ,此时就会触发 Minor GC ,然后 Eden Space 中的存活对象被复制至 Survivor Space 中。

这样的操作一直循环下去,虽然存活对象得到了转移,但是这2个区域中对象所占用的内存地址并不是连续的,这就导致了在只有 1块Survivor Space 的条件下产生的问题:

1
内存地址碎片化

内存地址碎片化导致的问题就是:

严重影响Java程序的性能,堆空间中被散布的对象占据着不连续的内存,最直接的结果就是:没有足够大的连续空间,内存碎片

1.4.1.4 建立 2块Survivor Space 的意义

当建立 2块Survivor Space 时:

  • 对已满的 Eden Space 进行 Minor GC 时,存活对象就会被复制至 From Space 中,Eden Space 被清空。

  • 当对已满的 Eden Space 再次进行 Minor GC 时,再次执行 Minor GC,Eden Space 和 From Space 中的存活对象将被复制至 To Space 中( ==这种 复制算法 保证了 To Space 中来自 Eden 和 From Space 2部分中存活对象所占用的内存空间地址是连续的,避免了碎片化的产生 == )。

  • 之后对 Eden Space 和 From Space 内存空间进行清空,将 From Space 和 To Space 进行交换即:将 To Space 中存活的对象复制至 From Space 中,保证 To Space 是空的

所以,建立 2块Survivor Space 的意义就是:

1
永远有一个 Survivor Space 是空的,另一个非空的 Survivor Space 无碎片。

参考:https://blog.csdn.net/u012799221/article/details/73180509

1.5 旧生代(Old Generation/Tenuring Generation)

  • 是用于存放 新生代 内存中经过多次垃圾回收后仍然存活的对象,例如 缓存对象
  • 新建的对象也有可能在 旧生代 中直接分配内存。主要由2种情况决定:

    • 新建对象为大对象
    • 新建对象为大数组对象,且数组中无引用外部对象

2. 内存分配

2.1 堆分配

Java对象所占用的内存主要是从  上进行分配, 是 所有线程共享 的,因此在堆上分配内存时是 需要加锁 的。这导致了创建对象开销比较大。当  上的内存空间不足时,会触发 GC ,如过 GC 后内存空间仍然不足,则抛出 OOM

2.2 TLAB(Thread Local Allocation Buffer)分配

JDK 为了提升内存分配效率,会为 每个新创建的线程 在 新生代的 Eden Space 上分配一块独立空间。这块空间称为 TLAB(Thread Local Allocation Buffer),其大小是由JVM根据运行情况而得出的。

在 TLAB 中分配内存是 不需要加锁 的,因此JVM在给线程中的对象分配内存时会尽量在 TLAB 上分配。如果对象过大或者 TLAB 空间已经用完了,则仍然在  上分配内存

因此在编写Java程序时,通常多个 小的对象比大的对象在内存分配上 更高效。 


3. 垃圾回收

JVM 通过 GC 来回收  和  中的内存。

**GC 的基本原理是**:

  • 首先找到程序中不在被使用的对象
  • 然后回收这些对象所占用的内存

3.1 GC 分类:

Java 中的堆也是 GC 收集垃圾的主要区域。

  • Minor GC - 新生代 - 复制算法
  • Full GC(又称 Major GC)- 旧生代 - 标记-清除算法

3.1.1 Minor GC

 中 新生代 中对象的生命周期(80%)一般为 朝生夕死。所以在 新生代 中使用的垃圾回收算法是 复制算发

  • 在GC开始前,对象只会存在于 Eden Space 和 From Space 区,此时 To Space 是空的。

  • 当GC运行时,Eden Space 中存活的对象被复制到 To Space 中;同时 From Space 中的对象根据其年龄值(年龄阈值可以通过-XX:MaxTenuringThreshold来设置)来决定去向:

    • 到达一定值,则对象将移动至 旧生代 ;
    • 未达到一定值,则对象被复制到 To Space 中。
  • 之后 Eden Space 和 From Space 区域已经被清空,此时,将 From Space 和 To Space 的角色进行交换(保证命名为 To Space 是空的)。

  • Minor GC 会一直重复这样的过程,直到 To Space 被填满,将所有对象移动至 旧生代 中。

3.1.2 Major GC

Major GC 是发生在 旧生代 的垃圾收集动作,所采用的是 标记-清除 算法。

旧生代 中的对象有部分是从 新生代 中移动过来的。所以不会轻易被回收掉,因此 Major GC 不会像 Minor GC 那样发生的频繁,并且做一次 Major GC 要比 Minor GC 消耗的时间长。

由于使用 标记-清除 算法时,会导致产生内存碎片(即:不连续的内存空间),当此后要为较大的对象分配内存空间且没有较大内存空间时,会提前触发一次GC操作。

3.2 回收算法

回收算法分类:

  • 引用计数 收集器:判断对象的 引用数量
  • 可达性 收集器:判断对象的 引用链是否可达

可达性收集器 所涉及的内存区域:

  • JVM栈中引用的对象
  • 方法区中类静态引用的对象
  • 方法区常量引用的对象
  • JNI引用的对象

3.3 引用计数 收集器

引用计数 收集器采用的是 分散式 的管理方式,通过 计数器 记录是否对 对象进行引用。当 一个对象的引用计数器为0 时,说明此对象已经不在被使用,于是进行回收

不足

  • 引用计数器 需要在每次对对象赋值时进行 计数器的增减,它有一定的消耗

  • 引用计数器 对于 循环引用 的场景是 无法实现回收 的。如:对象B 和 对象C 互相引用时,

    对于Java这种面向对象的会形成复杂引用关系的语言而言,引用计数器 是非常不合适的,在 JDK 的 GC 实现中 未采用此种方式。

3.4 可达性 收集器

可达性收集器采用的是 集中式 管理方式。全局记录 数据的 引用状态

基于一定条件(定时、空间不足时)的触发 ,使用跟踪收集器,执行时需要从 根集合 来扫描 对象的引用关系 ,这可能会造成 应用程序暂停

实现算法有3种

  • 复制(Copying)
  • 标记-删除(Mark-Sweep)
  • 标记-压缩(Mark-Compact)

3.4.1 复制(Copying)

复制采用的方式是:

  • 从 根集合 扫描出 存活的对象
  • 将 找到的存活对象 复制到 一块新的完全未使用的空间中

复制特点:

当要回收的空间中 存活的对象较少时,复制算法会比较高效,但是其带来的成本是:要 增加一块 全新的内存空间 及进行 对象的 移动

3.4.2 标记-清除(Mark-Sweep)

此种方式采用的是方式:

  • 从 根集合 开始扫描, 对 存活对象进行标记
  • 标记完成之后,再次扫描 整个空间中 未标记的对象,并对其进行回收。

标记-清除 特点:

  • 此种方式 不需要进行对象的移动, 仅对 不存活的对象 进行清除。因此在空间中 存活对象较多 的情况下,较为高效。
  • 由于此方式采用的是 直接回收不存活对象所占用的内存空间,因此会造成 内存碎片

3.4.3 标记-压缩(Mark-Compact)

  • 此种方式采用和 标记-清除 一样的方式对 存活对象进行标记
  • 在 回收不存活对象所占用的内存空间 后,会将 其他所有存活的对象 都往 左端空闲的空间进行移动,并 更新引用其对象的指针

    标记-压缩 特点:

  • 需要 对存活对象进行移动,成本相对较高

  • 不产生碎片

参考:https://www.cnblogs.com/sunniest/p/4575144.html

3.5 对象引用强弱问题

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 Unknown 标记的对象被回收后发送一条通知 Unknown

3.6 对象创建

  1. 虚拟机接到 New 指令后,先检查这个 指令的参数 是否能在 常量池 中定位到一个 符号引用,并检查此符号引用是否经过了加载验证等一系列步骤。

    若未经过此步骤,那么 先执行类加载步骤

  2. 类加载检查通过后,JVM在堆中为新生的对象分配内存空间,空间的大小在类加载完成之后就已经得知。分配方式:

    • 若堆中的 内存足够规整,那么进行 指针碰撞 内存分配;
    • 若堆中 内存不够规整,那么进行 空闲列表内存分配。

    分配内存空间 结束后,将对对象的进行 实际创建,创建方式:

    • 对分配内存空间的动作进行 同步处理
    • 对分配内存空间的操作按照 线程 划分在 不同的空间 TLAB 中进行。
  3. 内存分配 结束后,JVM要将内存空间 初始化为0值

  4. JVM对 对象进行必要的设置。如:此对象是哪个类的实例、如何找打类的元数据信息、对象的hash值、对象的GC年龄等信息。

  5. 到此为止,在JVM中一个新的对象产生了,但是 Java中一个对象才刚开始创建,接下来要执行: <init>(),所有字段都归0.

本文转载自:http://yannischeng.com/JVM%E7%90%86%E8%A7%A3-%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/

后海

后海

粉丝 33
博文 48
码字总数 25623
作品 2
闵行
后端工程师
私信 提问
mina中的allocate和directAllocate区别

allocate和directAllocate 区别在于内存的类型,allocate分配的内存在jvm管理范围内,directAllocate分配的内存则不是由jvm管理,可以理解成是类似C++那种分配的内存,大一定会说那由directA...

JavaGG
2009/07/08
377
0
《成神之路-基础篇》JVM——JVM内存结构(已完结)

Java内存模型,Java内存管理,Java堆和栈,垃圾回收 本文是《成神之路系列文章》的第一篇,主要是关于JVM的一些介绍。 持续更新中 参考文章: Java虚拟机的内存组成以及堆内存介绍 Java堆和栈...

2018/05/05
0
0
My java——JVM(内存域)三

续 My java——JVM(内存)二 写了一点JVM内存的一些操作的方法,和引出内存的分类。 是呀,java内存是我们在java编程中很少考虑到的,也没用真正的管理过。也许都知道JVM有自己的垃圾回收机...

tngou
2013/03/18
281
1
在 JNI 编程中避免内存泄漏

此文转自 IBM developerWorks JNI 编程简介 JNI,Java Native Interface,是 native code 的编程接口。JNI 使 Java 代码程序可以与 native code 交互——在 Java 程序中调用 native code;在...

IBMdW
2011/04/26
1K
1
【JVM】 java内存区域与内存溢出异常

前言 此系列博客是读《深入理解java虚拟机》所做的笔记整理。 No1. JVM内存管理这堵墙? 对C和C++的开发人员来说,在内存管理领域,他们既拥有每一个对象的“所有权”,也担负着每一个对象生...

binggetong
2018/05/07
0
0

没有更多内容

加载失败,请刷新页面

加载更多

How to Round a Number to N Decimal Places in Java

1. Overview In this short article, we're going to look at how to round a number to n decimal places in Java. 2. Decimal Numbers in Java Java provides two primitive types that ca......

Ciet
37分钟前
36
0
SpringCloud 基础教程(六)-负载均衡Ribbon

 我的博客:兰陵笑笑生,欢迎浏览博客!  上一章 SpringCloud基础教程(五)-配置中心热生效和高可用当中,我们对配置中心进行进行了深入的了解,本章将继续微服务架构的深入学习,了解在微服务...

_兰陵笑笑生
52分钟前
49
0
java面向对象(3)匿名对象与构造方法详解

一.匿名对象 1. 说明 有名字的对象:普通对象 没有名字的对象:匿名对象 匿名对象,用到链式编程,简单方便快捷。 如果,追求速度:匿名对象 如果,追求效率,内存占用低:普通对象 2.小案例...

煌sir
59分钟前
51
0
【自用】 Flutter Stack & Align 布局

Dart: Stack( children: <Widget>[ Align( child: Icon(Icons.home), alignment: Alignment(0,0), ), Align( child: Icon(Icons.se......

Tensor丨思悟
59分钟前
33
0
计算机实现原理专题--自动化(七)

在目前电路中,假设需要在原来的结果中再加两个数,较好的做法是从0020h处存放新的指令,并从0030h处存放新的数据: 现在两部分的指令起始地址分别为:0000h和0020h,两部分数据的起始地址分...

FAT_mt
今天
50
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部