转载

虚拟机执行内存区域

虚拟机执行内存区域

)

  • 程序计数器(线程私有区域)

    • 当前线程所执行的字节码行号指示器
    • 字节码解释器工作依赖计数器控制完成
    • 通过执行线程行号记录,让线程轮流切换各条线程之间计数器互不影响
    • 线程私有,生命周期与线程相同,随JVM启动而生,JVM关闭而死
    • 线程执行Java方法时,记录其正在执行的虚拟机字节码指令地址
    • 线程执行Native方法时,计数器记录为空(Undefined)
    • 唯一在Java虚拟机规范中没有规定任何OutOfMemoryError情况区域

    ? 在这其中,很多不理解的没关系,我们学过多线程,有两个线程,其中一个线程可以暂停使用,让其他线程运行,然后等自己获得cpu资源时,又能从暂停的地方开始运行,那么为什么能够记住暂停的位置的,这就依靠了程序计数器, 通过这个例子,大概了解一下程序计数器的功能。

  • Java虚拟机栈(线程私有区域)

    ? 用于存储栈帧。每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈、动态链接以及方法出口等信息。每一个方法从调用直至完成的过程中,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这个话怎么理解呢?比如执行一个类(类中有main方法)时,执行到main方法,就会为main方法创建一个栈帧,然后在加到虚拟机栈中,栈帧中会存放main方法中的各种局部变量,对象引用等东西。如图

    当在main方法中调用别的方法时,就会有另一个方法的栈帧入虚拟机栈,当该方法调用完了之后,弹栈,然后main方法处于栈顶,就继续执行,直到结束,然后main方法栈帧也弹栈,程序就结束了。总之虚拟机栈中就是有很多个栈帧的入栈出栈,栈帧中存放的都是一些变量名等东西,所以我们平常说栈中存放的是一些局部变量,因为局部变量就是在方法中,也就是在栈帧中。

  • 本地方法栈(线程私有区域)

    与虚拟机栈相似。虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

  • Java堆(线程共享区域):

    所有线程共享的一块内存区域。Java虚拟机所管理的内存中最大的一块,因为该内存区域的唯一目的就是存放对象实例。几乎所有的对象实例度在这里分配内存,也就是通常我们说的new对象,该对象就会在堆中开辟一块内存来存放对象中的一些信息,比如属性呀什么的。同时堆也是垃圾收集器管理的主要区域。因此很多时候被称为"GC堆",虚拟机的垃圾回收机制等下一篇文章来讲解。 在上一点讲的栈中存放的局部引用变量所指向的大多数度会在堆中存放。

  • 方法区(线程共享区域)和运行时常量池:

    和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、和编译器编译后的代码(也就是存储字节码文件.class)等数据,这里可以看到常量也会在方法区中,是因为方法区中有一个运行时常量池,为什么叫运行时常量池,因为在编译后期生成的是各种字面量(字面量的意思就是值,比如int i=3,这个3就是字面量的意思)和符号引用,这些是存放在一个叫做常量池(这个常量池是在字节码文件中)的地方,当类加载进入方法区时,就会把该常量池中的内容放入运行时常量池中。这里要注意,运行时常量池和常量池,不要搞混淆了,字节码文件中也有常量池,在后面的章节会详细讲解这个东西。现在只需要知道方法区中有一个运行时常量池,就是用来存放常量的。还有一点,运行时常量池不一定就一定要从字节码常量池中拿取常量,可能在程序运行期间将新的常量放入池中,比如String.intern()方法,这个方法的作用就是:先从方法区的运行时常量池中查找看是否有该值,如果有,则返回该值的引用,如果没有,那么就会将该值加入运行时常量池中。

  • 直接内存:

    并非虚拟机运行时数据区的一部分,也被称为堆外内存。

  • 实例详讲:

    class Demo1_Car{
        public static void main(String[] args) {
            Car c1 = new Car();
            //调用属性并赋值
            c1.color = "red";
            c1.num = 8;
            //调用行为
            c1.run();
            Car c2 = new Car();
            c2.color = "black";
            c2.num = 4;
            c2.run();
        }
    }
    Class Car{
        String color;
        int num;
        public void run() {
        	System.out.println(color + ".." + num);
    	}
    }
    

    • 首先运行程序,Demo1_car.java就会变为Demo1_car.class,Demo1_car.class加入方法区,检查是否字节码文件常量池中是否有常量值,如果有,那么就加入运行时常量池。
    • 遇到main方法,创建一个栈帧,入虚拟机栈,然后开始运行main方法中的程序。
    • Car c1 = new Car(); 第一次遇到Car这个类,所以将Car.java编译为Car.class文件,然后加入方法区,跟第一步一样。然后new Car()。就在堆中创建一块区域,用于存放创建出来的实例对象,地址为0X001.其中有两个属性值color和num。默认值是null 和 0
    • 然后通过c1这个引用变量去设置color和num的值,调用run方法,然后会创建一个栈帧,用来装run方法中的局部变量的,入虚拟机栈,run 方法中就打印了一句话,结束之后,该栈帧出虚拟机栈。又只剩下main方法这个栈帧了
    • 接着又创建了一个Car对象,所以又在堆中开辟了一块内存,之后就是跟之前的步骤一样了。
  • 创建对象

    虚拟机在遇到一条new指令时,会首先检查这个指令的参数是否可以在方法区中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已经被加载,解析和初始化过。如果没有,则必须先执行类加载过程.

    在虚拟机中,执行完new指令后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来

    • 创建对象时的内存分配方法:

      • 指针碰撞法(要求堆内存规整)

        Java堆中空闲内存和已使用内存分别存放在堆的两边,中间存放一个指针作为分界点的指示器,在为对象分配内存时只需要将指针向空闲区域移动创建对象所需要的内存大小即可。

      • 空闲列表法

        如果堆内存中已使用内存区域和空闲区域相互交错,此时虚拟机需要维护一个列表,记录哪些内存块是可用的,在分配时从列表中找到一块足够大的内存区域划分给对象实例并更新列表上的记录。

    • 保证为对象实例分配内存空间的安全性:

      • 对分配操作进行同步处理

        实际上虚拟机采用CAS加上失败重试的方式保证更新操作的原子性。

        • 采用本地线程分配缓冲(TLAB)

          预先为线程分配一部分的堆内存空间用于对象实例的内存分配。 在当TLAB用完并分配新的TLAB时进行同步锁定。

    • 为对象分配内存的方法

      • 采用CAS配上失败重试的方法保证更新操作的原子性。
      • 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。只有当线程的TLAB使用完,才同步锁定分配TLAB.
  • 对象在内存中的布局(HotSpot虚拟机为例)

    • 对象头

      “Mark Word”, 用于存储对象自身的运行时数据,如哈希码、GC分代年龄以及锁状态标志等。 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    • 实例数据

      对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。

    • 对齐填充

      并非必然存在,仅仅起着占位符的作用。

  • 对象的访问定位

    Java程序需要通过栈上的reference数据来操作堆上的具体对象。

    • 句柄访问

      Java堆中划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。(两次寻址)

    • 直接指针访问

      Java堆中对象的布局中需要考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。

      • 使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中实例数据指针,而reference本身不需要修改。
正文到此结束
本文目录