天天看点

浅谈 Jvm 内存区域1、前述2、运行时数据区域图3、程序计数器4、Java 虚拟机栈5、本地方法栈6、Java 堆7、方法区8、运行时常量池

1、前述

在大学学习 C++ 的时候,有一个最烦人的操作,就是对内存的管理,全部交给了开发者自己管理,开发者在写代码的时候,经常需要写代码去释放内存,否则就很容易造成内存泄露或者内存泄漏,导致服务崩溃,但是在 Java 语言里,Jvm 剥夺了开发者这个权力,由 java 虚拟机自己来管理,这样在一定程度上释放 java 语言开发者,不再需要为每一个 new 出来的对象做

delete/free

的操作,但是如果一旦 jvm 自己没有处理好,出现了内存泄漏以及内存溢出的问题,如果对虚拟机是怎么使用管理内存的话,排查起来问题就会变得比较艰难了

本文参考 《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》,本人自己做笔记加深理解和记忆

2、运行时数据区域图

浅谈 Jvm 内存区域1、前述2、运行时数据区域图3、程序计数器4、Java 虚拟机栈5、本地方法栈6、Java 堆7、方法区8、运行时常量池

其中,堆以及方法区是线程共享的数据区域,程序计数器、本地方法栈、虚拟机栈是线程私有的数据区域,线程间相互独立隔离的

3、程序计数器

3.1、程序计数器概述

(1)程序计数器是内存中很小很小的一块内存区域,甚至在 Jvm 规范中,这块区域都不会发生 oom ,也是唯一一个,程序计数器是每个线程自己独有的,可以把它看作是每个线程的一个执行行号计数器,也就是说标识当前所属的线程执行到了字节码的哪一行,防止线程执行到一般休眠再唤醒的时候,忘记自己执行到了哪里

(2)字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支(switch)、循环(for)、跳转(goto)、异常处理(exception handle)、线程恢复(thread wake up)等基础功能都需要依赖这个计数器来完成

(3)如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native 方法,这个计数器值则为空(Undefined)。

3.2、程序计数器大概工作示意图

public void testProgramCounterRegister(){
        int a = 0;	                       //(1)
        int b = 1;                         //(2)
        System.out.println(a + b);         //(3)
        System.out.println(b - a);         //(4)
    }
           

如图:

浅谈 Jvm 内存区域1、前述2、运行时数据区域图3、程序计数器4、Java 虚拟机栈5、本地方法栈6、Java 堆7、方法区8、运行时常量池
T1 时刻,线程 A 进入执行代码,执行到 (2)处代码,线程 A 的计数器记录字节码解释器所执行到的行号位置
T2 时刻,线程 A 阻塞或者休眠,线程 B 抢占 cpu 执行到 (2)处代码,线程 B 的计数器记录当前字节码解释器执行到的行号位置
T3 时刻,线程 A 、B 阻塞或者休眠,线程 C 抢占 cpu 执行到 (1)处代码,线程 C 的计数器记录当前字节码解释器所执行到的的行号位置 
T4 时刻,线程 A 唤醒,根据自身计数器之前记录的位置开始继续执行代码直到完成,线程 B、C 阻塞或者休眠
T5 时刻,线程 B 唤醒,根据自身计数器之前记录的位置开始继续执行代码直到完成,线程 C 继续阻塞或者休眠
T6 时刻,线程 C 唤醒,根据自身计数器之前记录的位置开始继续执行代码直到完成
           

在上面的例子中,如果每个线程没有自身的计数器,在被唤醒以后,就不知道自己上次执行到了哪里,从而出现错误

4、Java 虚拟机栈

4.1、简述

跟程序计数器一样,虚拟机栈也是每个线程私有的,java 虚拟机栈的生命周期跟线程的生命周期相同,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

4.2、局部变量表

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址),这里涉及到两种访问对象的方式,直接访问与句柄访问

4.2.1、对象的访问

在 java 程序里,都是通过栈帧上的 reference 数据来操作堆上的对象的

4.2.1.1 句柄池访问

如图所示:

浅谈 Jvm 内存区域1、前述2、运行时数据区域图3、程序计数器4、Java 虚拟机栈5、本地方法栈6、Java 堆7、方法区8、运行时常量池

句柄访问的方式中,java 栈帧中的 reference 数据存储的就是对象的句柄地址,在 Java 堆上,划分出了一块内存作为对象的句柄池,reference 指向的就是这一块地址,句柄池中包含了对象的实例数据以及类型数据,用句柄方式访问对象的优点就是如果对象在堆上位置改变了,只需要修改句柄池中这个对象对应的句柄,不需要改变 Java 栈帧中的 reference 数据

4.2.1.2 直接地址访问

如图所示:

浅谈 Jvm 内存区域1、前述2、运行时数据区域图3、程序计数器4、Java 虚拟机栈5、本地方法栈6、Java 堆7、方法区8、运行时常量池

指针访问方式直接访问对象的方式相比句柄访问少了一次指针定位的方式,更加快速,减少了一次指针定位开销,当对象访问的非常频繁的时候,开销就会变得很大,现金使用的主流的虚拟机是 Sun HotSpot,它是使用的直接指针访问的方式

4.2.2 局部变量表的内部分配

局部变量表的所需空间在编译期间就已经分配好了,当线程调用方法进入方法的时候,这个方法在栈中分配多大的空间来存放局部变量是确定的,不会在运行时动态的修改大小

4.2.3 OutOfMemmoryError 与 StackOverflowError

(1)当线程调用方法的时候,请求不到足够的栈空间(主要是申请局部变量的空间)时,也就是大于当前 Java 虚拟机所允许的最大栈大小,就会抛出 StackOverflowError

(2)如果虚拟机栈允许动态扩展,但是申请不到足够的内存,将会抛出 OutOfMemmoryError 异常

5、本地方法栈

本地方法栈跟虚拟机栈很像,只是虚拟机栈是为字节码服务的,但是本地方法栈是为虚拟机使用到的 native 方法使用

6、Java 堆

(1)目前所有的 Java 服务中,堆区算是很大的一块空间,它是所有线程共享的内存空间,在虚拟机启动的时候就创建了,它的唯一作用就是存放实例对象,以及数组,也是在堆区开辟空间

(2)文中开题就说到了内存管理,主要就是在这一块,当创建的对象使用完以后,如果一直不处理,很快就会占满整个堆区,没有新的内存空间可以创建实例对象或者数组,就会抛出 oom ,可以通过 -Xmx,Xms 两个参数来设置大小,以便于动态扩展

(3)Java 堆区又叫 GC 堆,也就是垃圾回收的地方,现在主流的垃圾收集器都使用了分代回收算法,分为新生代以及老年代,再细分可以分为 Eden、From Survivor、To Survivor,

(4)在 Java 虚拟机规范中,内存不是物理上连续的,就跟电脑磁盘一样,逻辑上是连续就好了

7、方法区

方法区与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来,在方法区的垃圾回收主要是针对常量池的回收以及类型的卸载,当方法区无法满足内存分配需求的时候,也会抛出 oom

8、运行时常量池

(1)运行时常量池其实属于方法区的一部分,类文件中有一块是常量池,用于存放编译期间生成的字面量以及符号引用,这些在类加载完成以后都会放入到方法区的运行时常量池里面去,在运行时常量池同样也会因为分配不到足够的内存同样也会抛出来 OOM

(2)运行时常量池里的并不一定是事先放到 class 文件里被加载放到方法区的,可能在运行时,生成的一些新的常量,也可能放到里面去

继续阅读