[关闭]
@TryLoveCatch 2022-04-15T03:54:20.000000Z 字数 8237 阅读 10296

Java知识体系之内存模型

Java知识体系


什么是JVM内存

Java源代码文件(.java)会被Java编译器编译为字节码文件(.class),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。

JVM在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。


了解清楚JVM的内存结构会更有助于我们理解Java的内存模型。

我们可以把上图的运行时数据区分为线程私有共享数据区两大类。

本地区(native area)

栈帧

每一个栈帧都包括了局部变量表操作数栈动态连接方法返回地址和一些额外的附加信息。
在编译代码的时候,栈帧中需要多大的局部变量表多深的操作数栈都已经完全确定了,并且写入到了方法表Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。

一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧的概念结构如下图所示:

方法区(method area)

方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

可以这样理解,方法区存的是类的模版。比如说方法,其实就是行为,一个类的行为都是一致的,所以存在方法区;而变量,就是数据,每一个对象的数据都是不一样的,所以存在堆里

符合引用和直接引用

虚拟机栈的动态链接就是将符号引用(这些符号引用的集合就是常量池)转换为直接引用(符号引用对应的具体信息,这些具体信息的集合就是运行时常量池,存在方法区中)的过程。

常量池

常量池表(Constant Pool Table)

我们写的每一个Java类被编译后,就会形成一份class文件(每个class文件都有一个class常量池)。 class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。

常量池表会在类加载后存放到方法区的运行时常量池中。

运行时常量池(Runtime Constant Pool)

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将 class常量池 中的内容存放到 运行时常量池 中,由此可知,运行时常量池 也是每个类都有一个。

在上面我也说了,class常量池 中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询 字符串常量池 ,以保证 运行时常量池所 引用的字符串与 字符串常量池 中所引用的是一致的。

字符串常量池(String Pool)

字符串常量池存的是 引用值,而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。

是在类加载完成,经过验证,准备阶段之后 在 堆 中生成字符串对象实例,然后 将该字符串对象实例的 引用值 存到 String Pool 中

小结

Jdk1.6、1.7和1.8区别



版本 方法区位置 静态变量 字符串常量池
1.6 JVM内存 方法区 方法区
1.7 JVM内存 堆区 堆区
1.8 本地内存 堆区 堆区

堆(heap space)

所有的对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:年轻代(Young Generation Space)老年代(Old Generation Space)
年轻代又可以划分为:伊甸区(Eden)幸存者区域(Survivor Sapce)

总结

整体如下图所示:

例子

  1. import java.text.SimpleDateFormat;
  2. import java.util.Date;
  3. import org.apache.log4j.Logger;
  4. public class HelloWorld {
  5. private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName());
  6. public void sayHello(String message) {
  7. SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
  8. String today = formatter.format(new Date());
  9. LOGGER.info(today + ": " + message);
  10. }
  11. }

这段程序的数据在内存中的存放如下:

垃圾回收

是在 JVM 启动时创建的,主要用来维护运行时数据,如运行过程中创建的对象和数组都是基于这块内存空间。Java 堆是非常重要的元素,如果我们动态创建的对象没有得到及时回收,持续堆积,最后会导致堆空间被占满,内存溢出。

因此,Java 提供了一种垃圾回收机制,在后台创建一个守护进程。该进程会在内存紧张的时候自动跳出来,把堆空间的垃圾全部进行回收,从而保证程序的正常运行。

也就是说,我们负责创建对象,GC负责来回收,那么如何认定是否是垃圾呢?

哪些是垃圾呢?

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

可达性分析算法

这种方案是目前主流语言里采用的对象存活性判断方案。基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。

上图中,object5, object6object7 便是不可达对象,视为“死亡状态”,应该被垃圾回收器回收。

在Java语言里,可作为GC Roots对象的包括如下几种:

关于GC Root

【证】:那些可作为GC Roots的对象

如何来回收这些垃圾呢?

参考下图,黑色的表示垃圾,灰色表示存活对象,绿色表示空白空间。

标记-清理

结果如下:

这便是 标记-清理 方案,简单方便 ,但是容易产生内存碎片

标记-整理

既然上面的方法会产生内存碎片,那好,我在清理的时候,把所有 存活 对象扎堆到同一个地方,让它们待在一起,这样就没有内存碎片了。

结果如下:

这两种方案适合 存活对象多,垃圾少 的情况,它只需要清理掉少量的垃圾,然后挪动下存活对象就可以了。

复制

这种方法比较粗暴,直接把堆内存分成两部分,一段时间内只允许在其中一块内存上进行分配,当这块内存被分配完后,则执行垃圾回收,把所有 存活 对象全部复制到另一块内存上,当前内存则直接全部清空。

起初时只使用上面部分的内存,直到内存使用完毕,才进行垃圾回收,把所有存活对象搬到下半部分,并把上半部分进行清空。

这种做法不容易产生碎片,也简单粗暴;但是,它意味着你在一段时间内只能使用一部分的内存,超过这部分内存的话就意味着堆内存里频繁的 复制清空。

这种方案适合存活对象少,垃圾多 的情况,这样在复制时就不需要复制多少对象过去,多数垃圾直接被清空处理。

Java中采用那种方法呢?

我们先来回忆一下,一块 Java 堆空间一般分成三部分,这三部分用来存储三类数据:

也就是说,常规的 Java 堆至少包括了 新生代老年代 两块内存区域,而且这两块区域有很明显的特征:

新生代-复制 回收机制

对于新生代区域,由于每次 GC 都会有大量新对象死去,只有少量存活。因此采用 复制 回收算法,GC时把少量的存活对象复制过去即可。

新生代区域分成8:1:1,依次取名为 EdenSurvivor ASurvivor B 区,其中 Eden 意为伊甸园,形容有很多新生对象在里面创建;Survivor区则为幸存者,即经历 GC 后仍然存活下来的对象。

工作原理如下:

  1. 首先,Eden区最大,对外提供堆内存。当 Eden 区快要满了,则进行 Minor GC,把存活对象放入 Survivor A 区,清空 Eden 区;
  2. Eden区被清空后,继续对外提供堆内存;
  3. Eden 区再次被填满,此时对 Eden 区和 Survivor A 区同时进行 Minor GC,把存活对象放入 Survivor B 区,同时清空 Eden 区和Survivor A 区;
  4. Eden区继续对外提供堆内存,并重复上述过程,即在 Eden区填满后,把 Eden 区和某个 Survivor 区的存活对象放到另一个 Survivor 区;
  5. 当某个 Survivor 区被填满,且仍有对象未被复制完毕时,或者某些对象在反复 Survive 15 次左右时,则把这部分剩余对象放到Old 区;
  6. Old 区也被填满时,进行 Major GC,对 Old 区进行垃圾回收。

那么,所谓的 Old 区垃圾回收,或称Major GC,应该如何执行呢?

老年代-标记整理 回收机制

根据上面我们知道,老年代一般存放的是存活时间较久的对象,所以每一次 GC 时,存活对象比较较大,也就是说每次只有少部分对象被回收。

因此,根据不同回收机制的特点,这里选择 存活对象多,垃圾少标记整理 回收机制,仅仅通过少量地移动对象就能清理垃圾,而且不存在内存碎片化。

总结

至此,我们已经了解了 Java 堆内存分代原理,并了解了不同代根据各自特点采用了不同的回收机制,即 新生代 采用 复制回收 机制,老年代采用 标记整理机制。

内存

Android基础之Java内存模型

jvm在执行java程序的时候,会把它所管理的内存,分为若干个不同的数据区域,这些区域可以分为线程私有和线程共享两部分。

线程私有

线程私有的主要是程序计数器、虚拟机栈和本地方法栈:

线程共享

线程共享主要是堆和方法区:

  1. // 分配了一个又一个对象
  2. 放到Eden
  3. // 不好,Eden区满了,只能GC(新生代GC:Minor GC)了
  4. Eden区的存活对象copySurvivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)
  5. // 又分配了一个又一个对象
  6. 放到Eden
  7. // 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
  8. Eden区和Survivor A区的存活对象copySurvivor B区,然后清空Eden区和Survivor A
  9. // 又分配了一个又一个对象
  10. 放到Eden
  11. // 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
  12. Eden区和Survivor B区的存活对象copySurvivor A区,然后清空Eden区和Survivor B
  13. // ...
  14. // 有的对象来回在Survivor A区或者B区呆了比如15次,就被分配到老年代Old区
  15. // 有的对象太大,超过了Eden区,直接被分配在Old区
  16. // 有的存活对象,放不下Survivor区,也被分配到Old区
  17. // ...
  18. // 在某次Minor GC的过程中突然发现:
  19. // 不好,老年代Old区也满了,这是一次大GC(老年代GC:full GC)
  20. Old区慢慢的整理一番,空间又够了
  21. // 继续Minor GC
  22. // ...
  23. // ...

StackOverflowError & OutOfMemoryError

StackOverflowError

栈溢出抛出java.lang.StackOverflowError错误,出现此种情况是因为方法调用层次太深,请求的栈深度大于虚拟机所允许的深度

  1. public class SOFTest {
  2. public void stackOverFlowMethod(){
  3. stackOverFlowMethod();
  4. }
  5. public static void main(String[] args) {
  6. SOFTest sof = new SOFTest();
  7. sof.stackOverFlowMethod();
  8. }
  9. }

通过递归调用方法,不停的产生栈帧,一直把栈空间堆满,直到抛出异常 :

OutOfMemoryError

OOM怎么排查

https://www.jianshu.com/p/60ab4f63b59d

OOM怎么避免

为什么局部变量是线程安全的?

为什么局部变量是线程安全的?

参考

https://blog.csdn.net/xiaojin21cen/article/details/105300521
https://javaguide.cn/java/jvm/memory-area.html#%E5%89%8D%E8%A8%80
https://www.cnblogs.com/code-duck/p/13577103.html
JAVA的内存模型及结构
Android面试一天一题(Day 44:实战美团--Java内存模型)
Android中高效的显示图片 - Bitmap的内存模型
Java虚拟机的堆、栈、堆栈如何去理解?
Java-技术之垃圾回收机制
JVM内存模型解析
深入理解Java虚拟机笔记---运行时栈帧结构
深入探究JVM | Java的内存区域解析
Java 虚拟机面试题全面解析
深入理解JVM03--判断对象是否存活(引用计数算法、可达性分析算法,最终判定)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注