详解JVM内存管理与垃圾回收机制 (上) 中

Heap Space (Java堆)

Java堆是JVM所管理的最大一块内存,所有线程共享这块内存区域,几乎所有的对象实例都在这里分配内存,因此,它也是垃圾收集器管理的主要区域。从内存回收的角度来看,由于现在的收集器基本都采用分代收集算法,所以Java堆又可以细分成:新生代和老年代,新生代里面有分为:Eden空间、From Survivor空间、To Survivor空间,如图1所示。有一点需要注意:Java堆空间只是在逻辑上是连续的,在物理上并不一定是连续的内存空间。

默认情况下,新生代中Eden空间与Survivor空间的比例是8:1,注意不要被示意图误导,可以使用参数-XX:SurvivorRatio对其进行配置。大多数情况下,新生对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,则触发一次Minor GC,将对象Copy到Survivor区,如果Survivor区没有足够的空间来容纳,则会通过分配担保机制提前转移到老年代去。

何为分配担保机制?在发送Minor GC前,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果是,那么可以确保Minor GC是安全的,如果不是,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于,直接进行Full GC,如果大于,将尝试着进行一次Minor GC,Minor GC失败才会触发Full GC。注:不同版本的JDK,流程略有不同

Survivor区作为Eden区和老年代的缓冲区域,常规情况下,在Survivor区的对象经过若干次垃圾回收仍然存活的话,才会被转移到老年代。JVM通过这种方式,将大部分命短的对象放在一起,将少数命长的对象放在一起,分别采取不同的回收策略。关于JVM内存分配更直观的介绍,请阅读参考资料3。


VM Stack (虚拟机栈) & Native Method Stack (本地方法栈)

虚拟机栈与本地方法栈都属于线程私有,它们的生命周期与线程相同。虚拟机栈用于描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

其中局部变量表用于存储方法参数和方法内部定义的局部变量,它只在当前函数调用中有效,当函数调用结束,随着函数栈帧的销毁,局部变量表也随之消失;

操作数栈是一个后入先出栈,用于存放方法运行过程中的各种中间变量和字节码指令 (在学习栈的时候,有一个经典的例子就是用栈来实现4则运算,其实方法执行过程中操作数栈的变化过程,与4则预算中栈中数字与符号的变化类似);

动态连接其实是指一个过程,即在程序运行过程中将符号引用解析为直接引用的过程。

如何理解动态连接?我们知道Class文件的常量池中存有大量的符号引用,在加载过程中会被原样的拷贝到内存里先放着,到真正使用的时候就会被解析为直接引用 (直接引用包含:直接指向目标的指针、相对偏移量、能间接定位到目标的句柄等)。有些符号引用会在类的加载阶段或者第一次使用的时候转化为直接引用,这种转化称为静态解析,而有的将在运行期间转化为直接引用,这部分称为动态连接。

全部静态解析不是更好,为何会存在动态连接?Java多态的实现会导致一个引用变量到底指向哪个类的实例对象,或者说该引用变量发出的方法调用到底是调用哪个类中实现方法都需要在运行期间才能确定。因此有些符号引用在类加载阶段是不知道它对应的直接引用的

每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程,下面通过一个非常简单的图例来描述这一过程

public void sayHello(String name) {
    System.out.println("hello " + name);
    greet(name);
    bye();
}

其调用过程中虚拟机栈的大致示意图如下图所示:

调用sayHello方法时,在栈中分配有一块内存用来保存该方法的局部变量等信息,①当函数执行到greet()方法时,栈中同样有一块内存用来保存greet方法的相关信息,当然第二个内存块位于第一个内存块上面,②接着从greet方法返回,③现在栈顶的内存块就是sayHello方法的,这表示你已经返回到sayHello方法,④接着继续调用bye方法,在栈顶添加了bye方法的内存块,⑤接着再从bye方法返回到sayHello方法中,由于没有别的事了,现在就从sayHello方法返回。

本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法 (也就是字节码) 服务,而本地方法栈则为虚拟机使用到的Native方法服务。

Program Counter Register (程序计数器)

程序计数器(Program Counter Register),PC寄存器,但寄存器是CPU的一个部件,用于存储CPU内部重要的数据资源,比如在汇编语言中,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。

类似的,JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。

Java虚拟机可以支持多条线程同时执行,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,JVM中的程序计数器是每个线程私有的。

1.2 堆外内存

直接内存(Direct Memory),它并不是虚拟机运行时数据区的一部分,Java虚拟机规范中也没有定义这部分内存区域,使用时由Java程序直接向系统申请,访问直接内存的速度要优于Java堆,因此,读写频繁的场景下使用直接内存,性能会有提升,比如Java NIO库,就是使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectBytedBuffer对象作为这块内存的引用进行操作。


由于直接内存在Java堆外,其大小不会直接受限于Xmx指定的堆大小,但它肯定会受到本机总内存大小以及处理器寻址空间的限制,因此我们在配置JVM参数时,特别是有大量网络通讯场景下,要特别注意,防止各个内存区域的总内存大于物理内存限制 (包括物理的和OS的限制)。

1.3 小结

花了很大篇幅来介绍Java虚拟机的内存结构,其中在讲解Java堆时,还简单的介绍了JVM的内存分配机制;在介绍虚拟机栈的同时,也对方法调用过程中栈的数据变化作了形象的说明。当然这样的篇幅肯定不足以完全理清整个内存结构以及其内存分配机制,你尽可以把它当做简单的入门,带你更好的学习。接下来会以此为背景介绍一些常用的JVM参数。



作者:CHEN川
链接:https://www.jianshu.com/p/f8d71e1e8821
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

猜你喜欢

转载自blog.csdn.net/ma15732625261/article/details/82085365