文章采集调用( 一图带你看Java虚拟机运行时数据区内存)

优采云 发布时间: 2021-09-01 14:14

  文章采集调用(

一图带你看Java虚拟机运行时数据区内存)

  

  一张图带你看这篇文章

  一、运行时数据区

  首先我们来看看Java虚拟机管理的内存包括哪些区域。正如我们要了解一座房子,我们首先要了解房子的大致结构。根据《Java虚拟机规范(Java SE 7版)》,请看下图:

  

  Java 虚拟机运行时数据区

  1.1 程序计数器

  程序计数器是一个很小的内存空间,可以看作是当前线程执行的字节码的行号的指示器。

  1.2 Java 虚拟机栈

  和程序计数器一样,Java虚拟机栈也是线程私有的,其生命周期与线程相同。虚拟机栈描述了Java方法执行的内存模型:每个方法执行时,都会创建一个栈帧来存储局部变量表、操作数栈、动态链接、方法退出等信息。每个方法从调用到执行完成的过程,对应着虚拟机栈中一个栈帧入栈到出栈的过程。请看下图:

  

  Java 虚拟机栈

  1.2.1 虚拟机堆栈溢出

  如果线程请求的栈深度大于虚拟机允许的最大深度,会抛出StackOverflowError异常。如果虚拟机在扩展堆栈时无法申请足够的内存空间,则会抛出 OutOfMemoryError 异常。

  1.3 本地方法栈

  1.4 Java 堆

  Java 堆是所有线程共享的内存区域,在虚拟机启动时创建。此内存区域的唯一目的是存储对象实例。几乎所有的对象实例都在这里分配内存(但随着技术的发展,所有的对象都分配在堆上,逐渐变得不那么“绝对”了)。请看下图:

  

  生成堆内存模型

  1.4.1 Java 堆溢出

  1.5 方法区

  方法区和Java堆一样,是每个线程共享的内存区。用于存储虚拟机已加载的类信息、常量、静态变量、即时编译器编译的代码等数据。

  1.5.1 运行时常量池

  1.6 直接内存

  二、内存分配策略

  对象的内存分配,大体上是在堆上分配(但也有可能在JIT编译后分解为标量类型,间接在栈上分配)。对象主要分配在新生代的Eden区。如果启动了本地线程分配缓冲区,则会根据线程优先级在TLAB上进行分配。少数情况下,也可能直接分配到老年代。分配规则不固定。具体取决于当前使用的是哪种垃圾采集器组合,以及虚拟机中内存相关参数的设置。

  2.1 对象先在 Eden 中分配

  大多数情况下,对象分配在新一代的Eden区。当 Eden 区域没有足够的空间进行分配时,虚拟机会发起一次 Minor GC。例如看下面的代码:

  private static final int _1MB = 1024 * 1024;

/**

* VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

*/

private static void testAllocation() {

byte[] allocation1, allocation2, allocation3, allocation4;

allocation1 = new byte[2 * _1MB];

allocation2 = new byte[2 * _1MB];

allocation3 = new byte[2 * _1MB];

allocation4 = new byte[4 * _1MB];//出现一次 Minor GC

}

  执行上面的 testAllocation() 代码。分配allocation4对象语句时,会发生Minor GC。这次GC的结果是新生代6651KB变成了148KB,总的内存占用几乎没有减少(因为allocation1、allocation2、allocation3三个对象都活着,虚拟机几乎找不到可回收的对象)。这次GC的原因是在给allocation4分配内存的时候,发现Eden已经占用了6MB,剩余空间不足以分配allocation4需要的4MB内存,所以发生了Minor GC。在GC的时候,虚拟机发现现有的3个2MB的对象都无法放入Survivor空间(从上图可以看出Survivor空间只有1MB大小),只好转移到老年代通过分配保证机制提前。

  2.2 大物件直接进入老年

  2.3 长寿对象会进入老年

  由于虚拟机采用分代回收的思想来管理内存,所以在内存回收的时候必须能够识别出哪些对象应该放在年轻代,哪些对象应该放在老年代。为了做到这一点,虚拟机为每个对象定义了一个对象年龄计数器。如果对象出生在 Eden 并且在第一次 Minor GC 中幸存下来,并且可以被 Survivor 容纳,它就会被移到 Survivor 空间,并且对象的年龄设置为 1。 每次一个对象“通过”一个Survivor区的Minor GC,年龄增加1年。当它的年龄增加到一定级别(默认为15岁)时,将被提升到老年。可以通过参数-XX:MaxTenuringThreshold设置提升对象的年龄阈值。

  2.4 动态对象年龄确定

  为了更好地适应不同程序的内存情况,虚拟机并不总是要求对象的年龄达到MaxTenuringThreshold才能提升到老年。如果Survivor空间中所有同龄对象的大小之和大于Survivor空间Half,则年龄大于等于该年龄的对象可以直接进入老年,无需等待MaxTenuringThreshold中的所需年龄.

  2.5 空间分配保障机制

  三、内存恢复策略

  3.1 内存恢复的关注区域

  3.2 判断对象存活

  3.2.1 引用计数算法

  3.2.2 可达性分析算法

  虚拟机栈中引用的对象(栈帧中的局部变量表) 方法区类静态属性引用的对象 方法区常量引用的对象 本地方法栈中JNI引用的对象

  请看下图:

  

  可访问性分析算法

  3.3 方法区的恢复

  这个类的所有实例都被回收了,即Java堆中没有这个类的实例。加载此类的 ClassLoader 已被回收。该类对应的java.lang.Class对象在任何地方都没有被引用,该类的方法在任何地方都无法通过反射访问。

  3.4 垃圾采集算法

  3.4.1 标签——清除算法

  

  “mark-clear”算法*敏*感*词*

  3.4.2 复制算法

  

  复制算法*敏*感*词*

  一个优化例子:新生代98%的对象都是“生死存亡”,所以不需要按照1:1的比例来划分内存空间,而是把内存划分成更大的Eden space 和两个较小的 Survivor 空间,每次使用 Eden 和 Survivor 之一。回收时,将Eden和Survivor中的幸存对象一次性复制到另一个Survivor空间,最后将刚刚使用的Eden和Survivor空间清理干净。

  另一个优化例子:将Eden和Survivor的大小比例设置为8:1,即每一代的可用内存空间是整个新一代容器的90%,只使用了10%的内存作为保留区。当然,98%可以回收的对象只是一般场景下的数据。我们无法保证每次回收时不超过 10% 的对象存活。当 Survivor 空间不够用时,需要依靠其他内存(这里指的是老年代)进行分配。保证(空间分配保证机制在最上面,看看)。

  3.4.3 标签——组织算法

  复制集合算法在对象存活率高的时候会执行更多的复制操作,效率会更低。因此,在老年代,一般不能直接使用副本采集算法。

  

  “标记-组织”算法*敏*感*词*

  3.4.4 分代采集算法

  四、编程中的内存优化

  相信大家都会注意到编程中的内存使用问题。下面我就简单罗列一下在实际操作中需要注意的几点。

  4.1 减少对象的内存使用

  我们可以考虑使用ArrayMap/SparseArray代替HashMap等传统数据结构。 (在老项目中,按照Lint的提示用ArrayMap/SparseArray替换HashMap后,在老项目中,Android Profiler显示运行时内存比之前少了几M,相当可观。)

  inSampleSize:缩放比例。在将图像加载到内存之前,我们需要计算一个合适的缩放比例,以避免不必要地加载大图像。 decode format:解码格式,选择ARGB_8888 / RBG_565 / ARGB_4444 / ALPHA_8,有很大区别。

  4.2 内存对象的复用

  4.3 避免对象内存泄漏

  内部类引用导致Activity Context泄露给其他实例,可能导致自身被引用而泄露。

  4.4 内存使用策略优化

  五、内存检测工具

  最后推荐几个内存检测工具。具体使用方法可以自行搜索。当然,除了下面这些工具,应该还有更多更有用的工具,只是我还没有找到。如果您有任何建议,可以在文章下方评论并留言。让我们一起学习和分享。

  

  需要本书的请私信

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线