JAVA虚拟机内存区域介绍

作者: 小疯子 分类: Java,JVM虚拟机 发布时间: 2019-05-08 21:40

参考链接:java虚拟机内存区域的划分以及作用详解

一、JVM内存结构

也叫作运行时数据区,如下图所示,都在内存中

1. 程序计数器

程序计数器在JVM中所起的作用就是用于存放当前线程接下来将要执行的字节码指令、分支、循环、跳转、异常处理等信息。(上面这句话可能有些歧义,它存放的是指令的执行地址(也就是数字格式的),每次执行的时候程序计数器中的执行地址就会跟着更新,更新到下一步指令的执行地址,这也说明在我们程序运行过程中计数器中改变的只是值,而不会随着程序的运行需要更大的空间,也就不会发生溢出情况,所以也就有了此特点:此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域)。在任何时候,一个处理器只执行其中一个线程中的指令,为了能够在cpu时间片轮转切换上下文之后顺利回到正确的执行位置,每个线程都需要具有一个独立的程序计数器,各个线程之间互相不影响,因此JVM将此块内存区域设计成了线程私有的。
首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。
如果线程正在执行的是Java 方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址。如果正在执行的是Native 方法,则这个技术器值为空(Undefined)。
程序计数器中只存储当前线程执行程序的行号,一个类指针的数据结构。
我们熟悉的分支操作、循环操作、跳转、异常处理和线程恢复等基础模型都需要依赖这个计数器来完成。

2. 本地方法栈

很多的算法或者一个功能的实现,都被java封装到了本地方法中,程序直接通过调用本地的方法就行了,本地方法栈就是用来存放这种方法的,实现该功能的代码可能是C也可能是C++,反正不一定就是java实现的。
Java中提供了调用本地方法的接口(Java Native Interface) JNI,比如网络通信、文件操作的底层,甚至是String的intern等都是JNI方法,JVM为本地方法所划分的内存区域便是本地方法栈。

3. 虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期与线程相同,是在JVM运行时创建的,在线程中,方法在执行的时候都会创建一个名为栈帧的数据结构,主要用于存放局部变量表、操作栈、动态链接、方法出口等信息。如下图所示,方法的调用对应着栈帧在虚拟机中的压栈和弹栈过程。
每一个线程在创建的时候,JVM都会为其创建对应的虚拟机栈,虚拟机栈的大小可以通过-xss来配置,方法的调用是栈帧被压入和弹出的过程。通过上图可以看出,同等的虚拟基站如果局部变量表等占用内存越小则可被压入的栈帧就会越多,反之则可被压入的栈帧就会越少,一般将栈帧内存的大小称为宽度,而栈帧的数量则称为虚拟机栈的深度。
上面讲的可能有些太抽象,按照参考链接中的一个例子
比如执行一个类(类中有main方法)时,执行到main方法,就会把为main方法创建一个栈帧,然后在加到虚拟机栈中,栈帧中会存放这main方法中的各种局部变量,对象引用等东西。如图
当在main方法中调用别的方法时,就会有另一个方法的栈帧入虚拟机栈,当该方法调用完了之后,弹栈,然后main方法处于栈顶,就继续执行,直到结束,然后main方法栈帧也弹栈,程序就结束了。总之虚拟机栈中就是有很多个栈帧的入栈出栈,栈帧中存放的都是一些变量名等东西,所以我们平常说栈中存放的是一些局部变量,因为局部变量就是在方法中。也就是在栈帧中,就是这样说过来的。
以上说的三个都是线程不共享的,也就是这部分内存,每个线程独有,不会让别的线程访问到,接下来的两个就是线程共享了,也就会出现线程安全问题。

4、堆内存(堆)

堆内存是JVM中最大的一块内存区域,被所有的线程所共享,Java在运行期间创建的所有对象几乎都存放在该内存区域,也就是通常我们说的new对象,该对象就会在堆中开辟一块内存来存放对象中的一些信息,比如属性呀什么的。该内存区域也是垃圾回收器重点照顾的区域,因此有些时候堆内存被称为“GC堆”。

5、方法区和其中的运行时常量池

和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、和编译器编译后的代码(也就是存储字节码文件。.class)等数据,这里可以看到常量也会在方法区中,是因为方法区中有一个运行时常量池,为什么叫运行时常量池,因为在编译后期生成的是各种字面量(字面量的意思就是值,比如int i=3,这个3就是字面量的意思)和符号引用,这些是存放在一个叫做常量池(这个常量池是在字节码文件中)的地方,当类加载进入方法区时,就会把该常量池中的内容放入运行时常量池中。这里要注意,运行时常量池和常量池,不要搞混淆了,字节码文件中也有常量池,在后面的章节会详细讲解这个东西。现在只需要知道方法区中有一个运行时常量池,就是用来存放常量的。还有一点,运行时常量池不一定就一定要从字节码常量池中拿取常量,可能在程序运行期间将新的常量放入池中,比如String.intern()方法,这个方法的作用就是:先从方法区的运行时常量池中查找看是否有该值,如果有,则返回该值的引用,如果没有,那么就会将该值加入运行时常量池中。
在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
先看classLoader是如何加载class文件和存储文件信息的
当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader。那么方法区中的字节码内存块,除了记录一个class自己的class对象引用和一个加载自己的ClassLoader引用之外,还记录了什么信息呢??见下图:

二、画运行内存图

平常分析中用到的最多还是堆、虚拟机栈和方法区。
例如:看下面这段程序,然后画出内存分析图
最主要是看我的分析过程,这个图由于要显示出动态弹栈画不了,所以只能够那样画一下了。
    1、首先运行程序,Demo1_car.java就会变为Demo1_car.class,将Demo1_car.class加入方法区,检查是否字节码文件常量池中是否有常量值,如果有,那么就加入运行时常量池
    2、遇到main方法,创建一个栈帧,入虚拟机栈,然后开始运行main方法中的程序
    3、Car c1 = new Car(); 第一次遇到Car这个类,所以将Car.java编译为Car.class文件,然后加入方法区,跟第一步一样。然后new Car()。就在堆中创建一块区域,用于存放创建出来的实例对象,地址为0X001.其中有两个属性值 color和num。默认值是null 和 0
    4、然后通过c1这个引用变量去设置color和num的值,
    5、调用run方法,然后会创建一个栈帧,用来装run方法中的局部变量的,入虚拟机栈,run方法中就打印了一句话,结束之后,该栈帧出虚拟机栈。又只剩下main方法这个栈帧了
    6、接着又创建了一个Car对象,所以又在堆中开辟了一块内存,之后就是跟之前的步骤一样了。
这样就分析结束了,在脑袋中就应该有一个大概的认识对堆、虚拟机栈、和方法区。注意这个方法区的名字,并不是就单单装方法的,能装很多东西。
这个只是一个简单的分析,可以再讲具体一点,1、创建对象,在堆中开辟内存时是如何分配内存的?2、对象引用是如何找到我们在堆中的对象实例的?通过这两个问题来加深我们的理解。
 1、创建对象,在堆中开辟内存时是如何分配内存的?
       两种方式:指针碰撞和空闲列表。我们具体使用的哪一种,就要看我们虚拟机中使用的是什么了。
       指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存度放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,所分配内存就仅仅是把哪个指针向空闲空间那边挪动一段与对象大小相等的举例,这种分配方案就叫指针碰撞
       空闲列表:有一个列表,其中记录中哪些内存块有用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,然后更新列表中的记录。这就叫做空闲列表
 2、对象引用是如何找到我们在堆中的对象实例的?
       这个问题也可以称为对象的访问定位问题,也有两种方式。句柄访问和直接指针访问。 画两张图就明白了。
       句柄访问:Java堆中会划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息
        解释图:在栈中有一个引用变量指向句柄池中一个句柄的地址,这个句柄又包含了两个地址,一个对象实例数据,一个是对象类型数据(这个在方法区中,因为类字节码文件就放在方法区中),
      直接指针访问:引用变量中存储的就直接是对象地址了,如图所示
        解释:在堆中就不会分句柄池了,直接指向了对象的地址,对象中包含了对象类型数据的地址。
      区别:这两种各有各的优势,
          使用句柄来访问的最大好处就是引用变量中存储的是稳定的句柄地址,对象被移动(在垃圾收集时移动对象是很普通的行为)时就会改变句柄中实力数据指针,但是引用变量所指向的地址不用改变。
          而使用直接指针访问方式最大的好处就是速度更快,节省了一次指针定位的时间开销,但是在对象被移动时,又需要改变引用变量的地址。在我们上面分析的例子中,就是使用的直接指针访问的方式。
0

发表评论

电子邮件地址不会被公开。 必填项已用*标注