美文网首页Java
Java 虚拟机—内存模型

Java 虚拟机—内存模型

作者: 未见哥哥 | 来源:发表于2019-03-28 00:03 被阅读24次

    一、JVM 内存模型

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

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

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

    2.线程共享区域:

    • 方法区

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

    JVM 运行数据区

    1.1、线程私有区域

    线程私有区域组成为:

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

    1.1.1、程序计数器

    什么是程序计数器呢?

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

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

    demo

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

    程序计数器

    这个 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日

    相关文章

      网友评论

        本文标题:Java 虚拟机—内存模型

        本文链接:https://www.haomeiwen.com/subject/jlyxbqtx.html