3.Java虚拟机内存管理

参考:《深入理解java虚拟机》一书

一、Java虚拟机内存区域简介
在这里插入图片描述
分析:运行时数据区包括线程共享区和线程独占区。线程共享区包括方法区和堆,被所有线程所共享;线程独占区包括虚拟机栈、本地方法栈和程序计数器,为单个线程所独有。

二、Java虚拟机内存区域详解
1.程序计数器(线程独占区)
概述:程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

特点:程序计数器出于线程独占区;如果线程执行的是java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址,如果线程执行的是native方法,则这个计数器的值为undefined;此区域是Java虚拟机规范中唯一没有规定任何OutOfMemoryError(内存溢出)情况的区域。
2.Java虚拟机栈(线程独占区)
概述:描述Java方法执行的动态内存模型,位于线程独占区。

栈帧:每个方法执行时,都会创建一个栈帧,伴随着方法从调用到执行完成;用于存储局部变量表、操作数栈、动态链接、方法出口等。栈帧图解如下:
在这里插入图片描述
局部变量表:存放各种基本数据类型、引用类型、returnAddress类型,其内存空间在编译器完成分配;当进入一个方法时,这个方法需要在栈帧中分配多少内存是固定的,在方法运行期间不会改变局部变量表的大小。

出现的异常
  StackOverflowError:虚拟机栈满了,但还需继续入栈,比如无限制地递归调用方法;
  OutOfMemory:当虚拟机栈帧还能继续入栈,但这时内存不够了,会出现内存溢出异常。
3.本地方法栈(线程独占区)
概述:与Java虚拟机栈类似。
与Java虚拟机栈区别(唯一的):Java虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机执行native方法服务。

4.堆内存(线程共享区)
概述:基本存放所有的对象实例,同样也是垃圾回收器管理的主要区域(堆中划分的新生代、老年代、Eden空间等是为了便于垃圾回收管理);当堆内存溢出时,也会抛出OutOfMemory异常。

堆内存修改:-Xmx-Xms
5.方法区(线程共享区)
概述:存储虚拟机加载的类信息(包括:类的版本、字段、方法、接口等)、常量、静态变量、即时编译器编译后的代码数据等。
方法区和永久代(元空间):方法区是《Java虚拟机规范》中的一个概念,也可以说是定义的一个规范,而永久代(元空间)是HotSpot虚拟机中对于方法区的一种实现,而其他虚拟机并没有永久代(元空间)这一说法。注:从Java8开始,使用元空间取代“永久代”
运行时常量池:
常量池(Class文件常量池):编译生成的Class字节码文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用。
概述:运行时常量池是方法区的一部分,一个类加载到 JVM 中后对应一个运行时常量池,在类加载后,常量池中的内容进入方法区中的运行常量池中存放。
举例

	public static void main(String[] args){
        String s1 = "abc";
        String s2 = "abc";
        String s3 = new String("abc");
        
        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
        System.out.println(s1 == s3.intern());
	}	

运行结果
在这里插入图片描述
分析:在方法中创建的常量(s1和s2,这里被称为字节码常量)一般会存放于常量池中,在类被加载后,这些常量池中的常量会存放于运行时常量池中,运行时常量池中有一个字符串表,类似于HashSet(无重复、无序),所以程序中的s1和s2均指向运行时常量池中的"abc",由于"==“比较的是地址值,所以这里(s1 == s2)为true,而s3为创建String类的实例对象,存放于堆内存中,所以 (s1 == s3)为false;使用s3.intern()相当于让s3指向的String类对象放到运行时常量池中,之后s3也指向运行时常量池中的"abc”,故(s1 == s3.intern())返回true。
在这里插入图片描述
6.直接内存
概述:直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

注意:本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

直接内存溢出:DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。

三、对象在内存中的布局(HotSpot虚拟机)
1.对象的创建
对象创建步骤如下:
在这里插入图片描述
给对象分配内存(两种方法)
  指针碰撞:适用于内存区域比较规整,空闲内存区和已用内存区泾渭分明;
  空闲列表:空闲内存和已用内存混在一起,这时可以创建空闲列表来记录空闲内存区,需要用的时候在空闲列表里找即可。
线程安全性问题:比如在空闲列表中,一块内存区域创建的对象使用,还没来得及更新空闲列表,又一个对象过来申请该内存区域,就有可能占用该已被其他对象使用的内存区域。
解决方法:(1)线程同步,当又一个线程过来申请到了该对象的内存区域,则该内存区域加锁封闭,不能再被其他线程对象所占用,这种方法执行效率较低;(2)为了解决低效率,可以先对每个线程分配一块内存区域,当某一线程的内存区域满了后,再使用线程同步策略,再将其他内存区域分配给该线程。
初始化对象:将int初始化为0,引用类型初始化为null,Boolean类型初始化为false。
执行构造方法:创建对象的最后执行构造方法。

2.对象的结构(内存布局)
在HotSpot中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头(Header):分为两部分。第一部分为自身的运行时数据,包括:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”,其结构图下图;另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在这里插入图片描述
实例数据(Instance Data):实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。
对齐填充(Padding):对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3.对象的访问定位
两种访问方式:实用句柄、直接指针。
在这里插入图片描述
分析:栈内存中的方法对对象访问时包含两个指针,即:指向句柄和直接指向对象。直接指向对象的指针,其包含两部分,一部分为指向对象实例数据的指针,另一部分为指向方法区中的对象类型数据的指针;指向句柄的指针指向句柄池,句柄池中同样包含两类指针,一类是指向实例数据的指针,另一类是指向方法区中的对象类型数据区域。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值