欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JVM内存数据区域分配和使用详解

程序员文章站 2022-07-09 19:17:43
1.运行时数据区a.程序计数器–作用为什么需要程序计数器?b.栈(stack) 先进后出Java虚拟机为什么用stack?Android studio 如何配置JVM的参数信息?...

一、运行时数据区

JVM运行时会将它所管理的内存划分为多个不同的数据区域,按线程私有和线程共享的可分为:
线程私有:虚拟机栈、本地方法栈、程序计数器
线程共享:方法区(包含运行时常量池)、堆

先借助这张图大概连接下数据结构:
JVM内存数据区域分配和使用详解

用代码来熟悉

package com.zzq.cilent; public class TestJVM { /*    public static class User{
            public int id = 0;
            public String name = "";
        }*/ //常量 final String chagnliang ="常量";//存放在方法区的静态常量池 //静态变量 static String jtbl ="静态变量";//存放在方法区的静态常量池 //次数 int count =0 ;//存放在堆 public void buy(int money){ money = money -100; //money 存放在局部变量表 count++; if(money < 0) return; buy(money); } public static void main(String[] args)throws Throwable { TestJVM testJVM = new TestJVM();//对象放再对里面,testJVM引用放再局部变量表里面 try { testJVM.buy(10000); }catch (Throwable e){ System.out.println("栈异常!调用方法(buy)的次数():"+testJVM.count); throw e; } } } 

1.程序计数器

指向当前线程正在执行的字节码指令的地址(行号)

为什么需要程序计数器?
Java是多线程的,意味着CPU会进行线程切换,确保线程切换回来时,能接上之前执行的代码执行程序。

2.虚拟机栈

一个个方法的执行,是以栈帧为载体携带局部变量变,动态连接,操作数栈,返回地址进虚拟机栈,出虚拟机栈的形式进行的。
b.栈(stack) 先进后出

JVM为什么用stack?

java中的方法调用机制跟栈的数据结构吻合,比如说在A方法中调用B方法,再在B方法中调用C方法,则C方法最先结束,其次是B,再次是C。方法的调用过程就是入栈,方法的结束就是出栈。
这里详细介绍下虚拟机栈:

  • 局部变量表:里面存放方法的局部变量,第0个元素为this,是该类的对象引用
  • 操作数栈:存放在方法里面执行程序过程中的一些变量,比如上面的buy方法中,执行将money = money -100的操作,先将money 从局部变量表中复制出来,放到操作数栈,然后将100的值带符号扩展成int值继续放入操作数栈。然后将100和money 出栈交给CPU做相减的操作,然后将相减的结果入操作数栈,在将结果出操作数栈,放到局部变量表给money赋值。
  • 返回地址:记录正常返回的地址(异常的话,是通过异常处理器表处理)
  • 动态连接:用于支持java的多态。记录对象运行时到底属于哪个类型。

3.本地方法栈

本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法

4.方法区

方法区存放类信息,常量,静态变量,即时编译期编译后的代码

5.堆

存放对象实例(几乎所有的对象),数组
java堆大小参数设置:
-Xmx 堆区内存可被分配的最大上限
-Xms 堆区内存初始化内存分配大小

虚拟机中的对象
(1)对象初始化流程

检查加载——>内存分配——>内存空间初始化——>设置——>对象初始化

  • 内存分配 :按内存的是否规整可分为 指针碰撞分配和空闲列表方式分配。
    内存分配的安全问题
    CAS(比较和交换):
    问题背景:线程1和线程2同时需要创建对象,此时堆里面有块空间A能分配给线程1或者是线程2
    线程1在分配内存之前会先去堆里面查看该块内存是否为null,如果为null,并记住null(old),然后去准备给该内存分配对象时,会再次获取该内存空间A是否为null,如果为null,则分配,如果不为null,说明该内存地址已经被其他线程抢占,则会另外重新找新的合适内存地址。
    本地线程缓冲:每个线程都会在java堆中有一块空间留给一部分小对象分配。该区域由于是线程独享的,所以不会存在线程安全问题,分配效率比较高。

  • 内存空间的初始化:(注意不是构造方法)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  • 设置属性:接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

  • 对象初始化:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,一般来说,执行new指令之后会接着把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

(2)对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。

(3)对象的访问定位

建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。

  • 句柄
    如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

  • 直接指针
    如果使用直接指针访问, reference中存储的直接就是对象地址。
    这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
    使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
    对Sun HotSpot而言,它是使用直接指针访问方式进行对象访问的。

(4)对象的访问定位逃逸分析
  • 栈上分配
    虚拟机提供的一种优化技术,基本思想是,对于线程私有的对象,将它打散分配在栈上,而不分配在堆上。好处是对象跟着方法调用自行销毁,不需要进行垃圾回收,可以提高性能。
    栈上分配需要的技术基础,逃逸分析。逃逸分析的目的是判断对象的作用域是否会逃逸出方法体。注意,任何可以在多个线程之间共享的对象,一定都属于逃逸对象。

本文地址:https://blog.csdn.net/zzq2006/article/details/104742684