JVM(1):虚拟机内存区域与内存管理

本文详细介绍了JVM中的内存区域,包括程序计数器、虚拟机栈、本地方法栈、堆和方法区的功能及特点。讨论了运行时常量池、对象的创建与访问定位,并分析了内存分配过程中的线程安全问题与解决方案。
摘要由CSDN通过智能技术生成

1.虚拟机中的内存区域及其功能

虚拟机中的内存区域示意图:
在这里插入图片描述
(1)程序计数器(Program Counter Register):存储的是当前线程所执行到的程序的地址码。也是虚拟机中唯一一个没有规定OutOfMemoryError(内存溢出)情况的区域。
为什么每个线程都有一个独有的程序计数器?
因为线程并不是从头执行到尾的,而是线程之间不停的进行切换的,因此就需要每个线程都有一个属于自己的程序计数器来记录当前线程执行到的程序的位置,才能保证各个线程有序的执行下去。

(2)虚拟机栈(VM Stack):虚拟机栈其实可以理解为我们通俗所说的内存中的栈区。其中有几个比较重要的概念
a.栈帧:一个栈帧其实就存储了java程序中一个方法所需要的最基本的信息。每调用一个方法,就把这个方法对应的栈帧压入虚拟机栈。
b.局部变量表:局部变量表存储了编译期可知的各种Java虚拟机基本数据类型,对象引用等基本信息。
c.大小:虚拟机栈的大小是有限的而不是无限大的,这就意味着我们不能无限调用方法(无限递归),如果出现调用的方法数量大于了虚拟机栈的最大容量,就会出现StackOverflowError。

(3)本地方法栈(Native Method Stacks):本地方法栈其实与虚拟机栈十分的类似。其主要的区别就是虚拟机栈是为虚拟机执行Java方法服务的,而本地方法栈是为虚拟机执行native方法服务的。其他的地方都是一样的。

(4)堆(Heap):堆的主要作用就是存储对象的实例。是虚拟机所管理的最大的一块区域。是垃收集器管理的主要区域。
这里就涉及了两个概念,即对象域对象的实例。对象是存储在虚拟机栈中的,由局部变量表表示,可以将对象看作一个抽象的东西,就是一个概念。因此存储在虚拟机栈中的局部变量表的大小都是一样的,因为都只是一个一个的概念。而对象的实例就是把一个对象给具体化之后的产物,他是存储在堆中的,对象的实例就是一个实际可以操作的东西,而因为具体化的方式的不同,因此每个对象的实例产生的时候所占据的空间的大小都是不一样的。
“几乎”所有的对象实例以及数组都应该在堆上分配!
同样的,堆内存也存在OutOfMemory错误。
堆区域在物理上可以是不连续的内存片段,但是在逻辑上必须是连续的。

(5)方法区(Method Area):存储虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。

2.运行时常量池

运行时常量池是方法区的一部分,常量池存放的主要是编译期生成的各种字面量与符号的引用。
虚拟机对于Class文件的每一个部分都有明确的规范和定义,如类的版本,字段,方法等。但是对常量池并没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需求来实现这个内存区域。
这里结合前面所讲的虚拟机的区域划分,我们举一个例子:

public void main(String[] args){
	String s1 = "abc";
	String s2 = "abc";

	System.out.println(s1 == s2);
	String s3 = new String("abc");
	System.out.println(s1 == s3);
}

首先,搞清楚s1,s2,s3在虚拟机中各个区域中是怎样存放的。
在这里插入图片描述
可以看出s1和s2都是通过直接赋值的形式创建的变量,因此他们的存储区域是在方法区的常量池中,而s3是通过new创建的对象,任何通过new创建的对象都是在堆中开辟空间进行存储的。

然后就是println语句的输出是true还是false?
第一个println的输出是true。因为其实这个语句实际上比较的就是s1和s2的地址是否是相同的。通过图中的分布可知,s1和s2的都是指向方法区中的常量池的,而常量池又是一个HashSet,其特点就是无序不重复。因此s1和s2内容都是abc,在常量池中只能指向同一个位置,因此地址是一样的。
第二个println的输出是false。显而易见的就是s1和s3的存储空间一个在堆中一个在常量池中。

这时,如果在上述代码的后面再加一行输出:

System.out.println(s1 == s3.intern());

会是什么结果?
这个时候的输出是true。因为s3.intern()的作用就是将s3的位置搬到运行时常量池中,这个时候s3和s1的地址就是一样的了,输出就是true。(这其实也给了我们比较字符串是否相同的一种启发,因为直接s1==s2比较的并不是字符串的内容,而是地址,如果此时使用了intern()方法,就可以利用HashSet的特性对字符串的内容进行比较。)

3.虚拟机中的对象

对象创建时的内存分配:
假设java堆中的内存是绝对规整的,所有被使用过的内存都放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲方向移动,这种分配方式成为“指针碰撞”。但是如果堆中的内存分配是不规整的,已使用的内存和未使用的内存交错的放在一起,就没有办法使用这种方式,必须设置一个表,分别记录空闲的块和已经使用的块。
选择哪种分配方式取决于使用的垃圾收集方式是否带有空间压缩整理的能力。(Serial,ParNew收集器带有压缩整理功能,CMS不带有压缩整理功能)

内存分配时的安全问题:
因为线程是不断切换的,因此就存在当一个线程还未来得及移动分配内存的指针时就被切换,导致内存分配出现问题等一系列线程不安全的情况。
针对这种情况可以有两种解决的方案:
a.对分配内存空间的动作进行同步性处理,即“加锁”,保证操作完成的完整性。但是这种方式对于资源的耗费比较严正。
b.使用本地线程分配缓冲。即每个线程在堆中预先分配一小块内存,当这个线程需要使用堆内存的时候就先使用它的本地线程分配缓冲,只有当本地线程分配缓冲使用完了之后再进行内存分配。

执行了上述内存分配的流程之后,在虚拟机看来,一个对象已经形成了,但是在java程序看来,我们还需要执行这个对象的构造函数,即Class文件中的init()方法,为所有的字段都默认赋零值。这也就是为什么java对象在没有赋值的时候也可以输出。完成构造函数的调用之后,一个对象才算完全被构造出来了。

4.对象的访问定位

成功创建一个对象之后,我们是怎么找到对象的呢?
有两种总方法:使用句柄和直接指针。
使用句柄:
java堆中可能会划分出一块内存作为句柄池,虚拟机栈中的引用中的reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据格子具体的地址信息。
直接指针:
java虚拟机栈中的reference存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。

使用句柄:
在这里插入图片描述

直接指针:
在这里插入图片描述
这两种对象访问方式各有优劣。使用句柄最大好处就是reference中存储的是稳定句柄的地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
使用直接指针最大的好处就是速度更快,节省了一次指针定位的时间开销。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值