本文来自网易云社区

作者:吕宗胜

Java语言与C语言相比,最大的特点是编程人员无需过多的关心Java的内存分配和回收,因为所有这一切,Java的虚拟机都帮我们实现了。JVM的内存管理,大大降低了开发人员对内存管理的要求,也不容易出现C语言中的内存泄漏和溢出。但一旦应用内存发生问题,也会导致程序员难以定位。所以对于Java程序员来说认识和了解JVM的内存分配和回收对于代码的编写和应用的优化都有非常重要的意思。

1. JVM内存模型

Java的JVM的类型是非常多样的,不同的JVM对于内存的分配和回收机制都不尽相同。我们这里仅仅介绍的是最为流行的JVM,HotSpot VM,它是目前使用范围最广的Java虚拟机。但是JVM的更新速度也非常快,不同的版本之间也可能会存在一些区别,但总体来说,其构架还是相对稳定的。

说到内存管理,我们首先要了解的就是Java运行时的数据区域,包括线程私有数据和共享数据的分配等等方面。根据《Java虚拟机》中的描述,其运行时数据区域为:

从上图可以看出,运行时的数据区域主要分成了5个部分:方法区、堆、虚拟机栈、本地方法栈和程序计数器。下面我们分别来介绍这5个部分。

1. 方法区

方法区中存储的数据被各个线程所共享,用于存储被虚拟机加载的类信息、常亮、静态变量、编译后的代码等数据。

2. 堆

堆区域存储的所有数据被各个线程共享,也是我们程序中最为关心的内存区域。该区域的目的就是存放对象实例,所以我们程序中的几乎所有对象都是存储在这块区域的。同时,该区域也是JVM进行内存管理和回收的主要区域。

3. 虚拟机栈

虚拟机栈是除堆之外最重要的一块内存区域,虚拟机栈中的数据是线程私有的。虚拟机栈是Java方法执行的内存模型,在每个方法执行时都会创建栈帧,用于存储局部变量表,操作数栈等。

4. 本地方法栈

它与虚拟机栈的作用是十分相似的,而它们的区别是虚拟机栈是用于执行Java方法时的数据结构,而本地方法栈是Java使用的Native方法服务。

5. 程序计数器

程序计数器是非常小的一块内存,每个线程都有一个独立的程序计数器,通过程序计数器,我们可以知道当前线程的执行的字节码序号。

上面简单的了解了一下JVM运行时的数据区域和每个区域的基本功能,而在实际的使用过程中,我们最为关心的就是堆区域和虚拟机栈中的局部变量表。而对于线程私有的虚拟机栈而言,数据内存随着线程的消亡而回收,而堆数据的回收则成为JVM内存管理和回收的重点。

2. 内存管理的分代机制

JVM中,目前使用的内配管理是分代方式,即把内存分成新生代、老生代和永久代。这里我们讲的分代管理机制是针对线程共享的内存区域,主要是堆,也包括方法区。

JAVA分代机制的好处是可以根据Java的实际对象创建和销毁时机,在不同的生代中可以采用不同的垃圾回收策略,已提高垃圾回收的效率。在Java中,几乎所有对象的实例都分配与新生代,而大部分对象的存活时间都不长,新生代中的对象回收会比较频繁。而老年代中的存放是那些存活时间较长,或者对象过大导致无法在新生代中分配的对象。而永久代比较特殊,它一般是指内存区域中的方法区,HotSpot在实现方法区时作为永久代来处理,避免了额外来管理方法区。这块区域的内存回收我们一般不做考虑,因为效果不会很明显,而且回收的条件也非常苛刻。

3.垃圾清除算法

针对不同的分代以及其特性,不同分代使用的垃圾回收策略也是不一样的。

3.1标记-清除算法

标记清除算法其实非常简单,它是先标记那些已经死亡的对象,然后对这些死亡的对象进行清理。但是它的一个很大的不足在于直接清理会产生非常多得内存碎片,导致后续分配内存会因为碎片的问题而没有连续的大空间满足分配,从而触发下一次的垃圾回收。可想而知,该垃圾清除策略效率和空间上都不会是最优的。

3.2 复制算法

复制算法,其实本质上跟标记-清除算法没有区别,不过它解决了内存碎片化的问题,同时也解决了两次扫描的问题。它的实现方式是在内存分配时先预留一部分内存,当内存需要回收的时候,它会进行扫描,把没有过期的内存数据复制到预留的内存,而直接清理原先分配的内存,把原先分配的内存作为预留内存。这种方法的好处就是效率很高,缺点也非常明显,那就是要浪费一部分内存作为预留内存,而如果为了保证数据100%的不丢失,原则上我们需要预留所有可分配内存的一半,造成内存的大面积浪费。

在新生代中,JVM采用了复制算法,因为新生代中的对象基本都是朝生夕死的,所以每次垃圾回收效果会比较明显,我们也称之为MinorGC。这里新生代划分成3块区域,Eden区,From Survivor区和To Survivor区。两块Survivor互为备份,垃圾回收时,对象会集中复制到空闲的Survivor区中去。为了提高内存的利用率,这个Eden区会占用较大的比例,默认比例是8:1。这样新生代只有10%的内存被浪费掉,但是毕竟很是有大量对象不能被回收而导致Survivor区空间不足的问题。这里就涉及到分配担保问题,当Survivor区不够的时候,对象会直接进入老年代。

3.3 标记-整理算法

复制算法除了空间的浪费外,还有一个问题就是如果对象是长期存活的,将会导致内存回收的效率降低,因为复制的内存将会变大。所以复制算法比较适合那些对象存活期较短的内存区域回收。所以在复制和标记-清除算法的基础上,提出了标记-整理算法。标记-整理算法也是先对对象进行标记,而后该算法将存活的对象往内存的一个方向移动,最终的内存将是占用的内存和空闲的内存有明显的分界,它主要是解决了内存碎片化的问题。

与新生代的朝生夕死相比,老生代的对象存活时间会比较长,所以采用了标记-整理算法。如果发生了老生代的垃圾回收,我们称之为FullGC。老生代的回收效率较低,会导致系统暂停较长的时间,所以我们要尽量减少FullGC的发生。

4. 分配回收策略

上面我们看到了JVM分代的垃圾回收算法,下面我们来看看JVM在内存分配和回收中的一些最常见的几个点。

4.1 对象优化分配在Eden区

Java的对象优先分配在Eden区中,当Eden区中没有足够的内存分配时,JVM会进行一次MinorGC。所以JVM中MinorGC会是比较频繁的垃圾回收动作,一般回收速度也比较快。对象分配在Eden区也不是绝对的,有一种例外是大对象会直接进入老年代。这里的大对象是指需要连续内存空间的Java对象,比如说很长的字符串和数组等。大对象直接进入老年代非常不适合垃圾回收策略,特别是这些大对象也是那些朝生夕死的对象,这会造成比较频繁的FullGC,导致系统性能降低。

4.2 长期存活的对象进行老年代

每一个对象都有一个对象年龄,对象在新生代中每经过一次垃圾回收,对象年龄增长1,当对象年龄超过某个阈值时,该对象会进入老年代。所以这里就有一个问题,如果我们在非常频繁的进行垃圾回收时,对象的对象年龄就会快速增长,一个对象会非常容易的进行老年代,造成FullGC的次数增长。

5. 对象死亡的判断算法

上面我们介绍了垃圾回收,却一直没有介绍JVM中使用的判断对象死亡的算法。最简单的对象判断的算法是采用计数法。当对象被引用时,计数加1,当一个对象的引用计数为0时,表示该对象已经死亡,可以进行回收。计数的方法虽然简单,易实现,但是却不能解决相互引用的问题,比如说对象A引用B,B也引用A,而A和B不再被其他对象引用,这种情况下,如果AB对象是可以被回收的,但是计数确不为0。

目前,通用的判断对象死亡的方法是可达性分析算法。可达性分析是指从对象起点开始,如果该对象可以被引用到,则该对象是活着的,否则,该对象则死亡了。那么该算法中最基本的对象起点是哪些呢?这些对象是指虚拟机栈中引用的对象、方法区中引用的对象和本地方法栈中引用的对象。

本文来自网易云社区,经作者吕宗胜授权发布

相关文章:
【推荐】 流式断言器AssertJ介绍
【推荐】 浅析Kubernetes的工作原理
【推荐】 四六级成绩查询,你的『验证码』刷出来了吗?

JVM运行内存分配和回收的更多相关文章

  1. 最简单例子图解JVM内存分配和回收

    一.简介 JVM采用分代垃圾回收.在JVM的内存空间中把堆空间分为年老代和年轻代.将大量(据说是90%以上)创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象.年轻代中又被分 ...

  2. 最简单例子图解JVM内存分配和回收(转)

    本文转自http://ifeve.com/a-simple-example-demo-jvm-allocation-and-gc/ http://www.idouba.net/a-simple-exa ...

  3. JVM内存分配和回收

    本文内容来自<Java编程思想(第四版)>第二章<一切都是对象>和第五章<初始化与清理>.作为一个使用了好几年的Javaer,再次看编程思想的前面章节(不要问我为什 ...

  4. 图解JVM内存分配和回收

    一.简介 JVM采用分代垃圾回收.在JVM的内存空间中把堆空间分为年老代和年轻代.将大量(据说是90%以上)创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象.年轻代中又被分 ...

  5. JVM内存分配与回收

    1.内存分配与回收策略 内存自动管理:自动化的解决了对象内存分配和回收对象内存的问题. 一般在堆上分配对象,也可能经过JTI编译后间接在栈上分配. 主要分配在新生代的Eden区,如果启动了本地线程分配 ...

  6. 窥探JVM内存分配和回收的过程

    一.环境 JDK 垃圾收集器 是否启用TLAB 通用JVM参数(堆内存分配见下图) 1.6.0_65 Serial + Serial Old 否 -Xms20m -Xmx20m -Xmn10m -XX ...

  7. jvm内存分配和回收策略

    在上一篇中,已经介绍了内存结构是什么样的. 这篇来介绍一下 内存是怎么分配的,和怎么回收的.(基本取自<深入理解Java虚拟机>一书) java技术体系中所提倡的自动内存管理最终可以归结为 ...

  8. JVM 内存分配和回收策略

    对象的内存分配,主要是在java堆上分配(有可能经过JIT编译后被拆为标量类型并间接地在栈上分配),如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配.少数情况下也是直接分配到老年代,分配规则不 ...

  9. JVM内存分配与回收策略

    对象优先在Eden分配 大多数情况下,对象在新生代Eden区中分配. 当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC. Minor GC:新生代GC,指发生在新生代的垃圾收集动作 ...

随机推荐

  1. nutz 结合QueryResult,Record 自定义分页查询,不构建pojo 整合

    public QueryResult getHistoryIncome(int d, int curPage) throws Exception { /**sql**/ Sql sql = Sqls. ...

  2. ACM-ICPC (10/19)

    这两天在看虚树,的确很难理解. 不过大致的思路就是说删掉一些没有用的点,但是仍然保持树的相对结构,树上只有两种点,一个是集合点,和一些LCA,这些LCA是为了保持树的相对结构,才留下的. 具体做法网上 ...

  3. Uva 11600 期望DP

    题意:n个城市,相互可达(有n(n-1)/2条边),其中有一些道路上面有妖怪,现在,从1号城市出发,随机挑取一个城市走去,这个道路上的妖怪就会被消灭,求: 在平均情况下,需要走多少步,使得任意两个城市 ...

  4. 大整数乘法(POJ2389)

    题目链接:http://poj.org/problem?id=2389 #include <stdio.h> #include <string.h> #define Max 1 ...

  5. Batch Normalization:Accelerating Deep Network Training by Reducing Internal Covariate Shift(BN)

    internal covariate shift(ics):训练深度神经网络是复杂的,因为在训练过程中,每层的输入分布会随着之前层的参数变化而发生变化.所以训练需要更小的学习速度和careful参数初 ...

  6. 【洛谷P2168】[NOI2015]荷马史诗

    荷马史诗 建一个k叉哈夫曼树,用堆维护一下 // luogu-judger-enable-o2 #include<iostream> #include<cstdio> #inc ...

  7. 安装 centos7

    一.安装 省略前面安装操作,直接进入设置界面: 日期.键盘等设置按默认即可,主要是系统安装位置需要设置,点击 系统安装位置,进入设置 最终完成分区: 点击左上角完成按钮 接受更改 开始安装 设置roo ...

  8. 解决:fontawesome-webfont.woff2?v=4.6.3 404 (Not Found)

    用Bootstrap里面的字体,你项目中会报一个错,一个字体找不到,但我们的项目中却是存在这个字体的. 解决方法: 修改我们的Web.Config文件

  9. CALayer创建图层(转)

    一.添加一个图层  添加图层的步骤:  1.创建layer  2.设置layer的属性(设置了颜色,bounds才能显示出来)  3.将layer添加到界面上(控制器view的layer上) @int ...

  10. 使用Win32DiskImager后重置SD卡

    再1.Windows diskpart命令 diskpart 2.列出所有的磁盘 lisk disk 3.选择U盘所在的磁盘 4.清除磁盘 clean 5.创建主分区 create primary p ...