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
下一篇: python实战小游戏之考验记忆力