JVM 内存模型-图文

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lwj_zeal/article/details/88858851

一、JVM 内存模型

本节来分析 Java 对象如何进行分配回收

JVM 运行时数据区主要由线程私有区域线程共享区域组成。

  1. 线程私有区域:
  • 虚拟机栈
  • 本地方法栈
  • 程序计数器

2.线程共享区域:

  • 方法区

下面绘制一个草图来描述 JVM 运行数据区的组成:

JVM 运行数据区

1.1、线程私有区域

线程私有区域组成为:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

1.1.1、程序计数器

什么是程序计数器呢?

因为 Java 本身就是一个多线程的,每一个线程都有一个程序计数器, CPU 在切换线程时,会使用程序计数器记录下当前线程正在执行的字节码指令的地址(行号),这样线程再次回来工作时,就知道执行到哪个位置了。

为了更加深入的理解程序计数器,下面来看这样一段代码:

demo

通过 javap -c -l MMDemo.class 得到对应字节码:

扫描二维码关注公众号,回复: 5840590 查看本文章

程序计数器

这个 Code 对应的这些数就是程序计数器了。

1.1.2、虚拟机栈

虚拟栈属于线程私有部分,在线程内部中一般会调用很多方法,而每一个方法使用一个栈帧来描述。

下面用一个草图来描述一下栈帧虚拟机栈的关系:

虚拟机栈是由多个栈帧组成,每调用一个方法就相当于有一个栈帧入栈到虚拟机栈中。

栈帧

1.1.3、栈帧的组成

在前面描述过,在线程中,一个方法被调用就会一个栈帧被压入虚拟机栈中。栈帧就是用来描述这个方法,一个栈帧是由局部变量表操作数栈返回值地址动态链接组成。

下面还是回到上面示例,结合草图,看它们之间的关系:

虚拟机栈

局部变量表:

方法内部声明的变量存放表

32位地址,寻址空间为 4G 。如果需要存放64位的数据,需要使用高位和地位表示。

局部变量表

下面是 pay() 生成的局部变量表:

  • this 表示当前对象
  • i
  • obj

操作数栈:

对局部变量表中的变量进行出栈入栈的操作。

返回值地址:

一个方法被执行之后,有一个返回值,返回给对应的调用处。

动态链接:

主要对应的多态,只有代码执行时才知道具体的实现类是那个对象。

1.1.4、StackOverflowError

这个异常想必很多人都遇过,字面意思就是栈溢出。我们通过上面的分析我们知道,虚拟机栈如果不断出现栈帧入栈,当虚拟机栈空间达到上限,那么就会出现 StackOverflowError

下面来模拟这个错误的产生:

public class StackOverflowError {
    private static int count = 0;

    public static void main(String[] args) {
        try {
            recursion();
        } catch (Throwable e) {
            System.out.println("deep of calling = " + count);
            e.printStackTrace();
        }
    }

    public static void recursion() {
        count++;
        recursion();
    }
}

StackOverflowError

如果是死循环出现这样的错误StackOverflowError,那么通过 -Xss 参数的设置也是没有用。

当然,如果是因为虚拟机栈空间比较少到导致频繁出现这个错误,那么是可以合理的调节这个参数的。

例如设置参数:-Xss164K

1.1.5、本地方法栈

虚拟机栈对应的方法是 Java 方法,而本地方法栈对应的是 native 方法。其他方面应该和虚拟机栈差不多。

1.2、线程共享区域

1.2.1、方法区

方法区所存放的数据为类信息,常量静态变量,即时编译后的代码

方法区

在 JDK1.8 以下的 JVM 中,方法区就是对应的永久代,而 JDK1.8 之后方法区就变成了元空间(MetaSpace)

1.2.2、堆空间

在堆空间中主要存放的是通过 new 创建出来的对象或者数组。

##1.3、JVM 内存模型-堆

在 JVM 内存模型中,将线程共享部分分为了堆空间和方法区,下面主要来看堆空间是如何进一步划分的。

在下面这张草图中,JVM 堆空间按照分代思想划分为新生代老年代两部分,它们两者空间分别为堆空间的1/3和2/3。

JVM 内存模型

对于新生代这个区域又进一步进行划分为 Eden区from区to区这三个区域。

  • 对象或者数组的创建优先在 Eden 区分配内存空间。
  • 如果新创建的大对象在新生代放不下,那么会直接移入老年代空间。
  • 在每一次进行 Minor GC 之后,Eden 区的垃圾对象就会被回收,并且存活的对象会进入 from区 或者 to区。在每次 Minor GC 之后存活的对象的年龄会累加,当多次 GC 之后,对象的年龄达到 15 ,那么将进入老年代。
  • 如果 Eden 区存活的对象太大,放不进 from 或者 to 中,那么将进入老年代。

注意:在新生代中发生的 GC 为 Minor GC 而老年代发生的 GC 为 Full GC, Full GC 比 Minor GC 的效率要低。

1.4、垃圾回收算法

GC 是如何判断一个对象是否需要被回收呢?

在 JVM 中主要有两种判断方法:

  • 对象引用记数法

当一个对象被一个变量引用时,那么引用计数累加。如果一个对象没有被其他变量引用,那么就是需要被 GC 回收的。但是这里有一个弊端,那就是对象间相互引用导致无法被回收的问题。

  • 可达性分析算法

从一个 GCRoot 开始遍历,当一个对象到 GCRoot 没有直达路径时,就标记为不可达,是需要被 GC 回收的。

那什么对象可以作为 GCRoot 呢?

  • 局部变量表所引用的对象
  • 静态变量/常量引用的对象
  • 本地方法引用的对象

JVM垃圾收集算法

  • 标记清除法

标记清除算法分为两个阶段:

标记阶段负责将将可回收的对象进行标记出来,清除阶段负责将这些标记出来的对象进行回收。从下面的图可以看出,这种算法会造成内存碎片。

标记清除算法

  • 复制算法:

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

复制算法

  • 标记整理法:

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

标记整理算法

  • 分代收集算法:

根据分代思想对堆区划分位新生代和老年代,针对这个两个区只用不同的回收算法。
新生代:使用复制算法
老年代:使用标记清理算法或者标记整理算法

总结

记录于2019年3月27日

猜你喜欢

转载自blog.csdn.net/lwj_zeal/article/details/88858851