JVM 运行时内存分配

Stella981
• 阅读 694

Java内存分配

在解释这个问题之前,我想简单的记录一下Java虚拟机对内存的分配管理。

JVM 运行时内存分配

简单的说,Java运行时内存区域,就由上面几部分构成。青绿色标记的,是每个线程私有的内存区域,其他的为线程共享的内存区域。我们先简单的依次说明每个部分是用来存什么的,最后再用一个简单的例子,将各个部分结合起来简单介绍其内存分配的基本过程。

首先,程序计数器(pc)。这个东西对于很多开发者来说,再熟悉不过了,尽管不同领域的pc,具体用法上存在一些小小的差异,但总的来说,pc是用来记录程序运行到哪里了,下一步又该执行哪一步操作。pc占据的内存是线程级的,即随线程的创建而产生,随线程的销毁而销毁(被回收)。

其次JVM栈和本地方法栈。这两个栈在存储结构上,基本相同,以至于很多的JVM产商,将二者合而为一。JVM栈,顾名思义,是用来存储Java方法运行过程中使用的栈数据,本地方法栈就是用来存储本地方法执行过程中的栈数据。栈中存储的数据,是一种被称为“栈帧”的东西。栈帧主要包括:局部变量表和操作数栈。栈帧的入栈和出栈,分别意味着一个方法的执行与结束。

接着,我们来看看方法区。方法区主要是用来存类型数据的,与类型相关的东西,比如常量,静态变量,编译后的代码等,基本都存储在这一区域。而因为“无用类”的判断条件非常苛刻(有三点,第一,该类无可达对象,第二,该类的ClassLoader已被回收,第三,该类的Class对象无引用),这个区域存储的内容很难会被回收,所以你可能会在很多地方看到“永久代”一词,其实说的主要也就是这个方法区。方法区中,有个特殊的区域,被划分(逻辑划分,不一定为物理划分)出来,即“运行时常量池”。运行时常量池,保存着字面量,符号引用等。方法区是线程共享的,随JVM启动而创建,JVM退出而销毁。

最后,是这个堆。堆,在很多领域也有用到。在Java中,堆,是用来存储对象的相关内容,包括对象的对象头和实例数据(数组对象还有一个数组的长度)。不同的JVM实现,对象可能还包括类型指针(指向对象所属的类型信息,存在方法区中)和占位符(虚拟机实现可能需要内存对齐)等。

一个简单的例子

public void test (int result, int num) { TestClassB classB = new TestClassB; classB.methodB; } public class TestClassB { public void methodB(result, num) { int finalResutl = result + num; ...... } }

现在假设线程A在执行test方法,并已经执行到TestClassB classB = new TestClassB。首先,会去判断类TestClassB有没有被加载到方法区中,如果没有,先加载类(类的加载过程不详细说明,有空可以写篇Java类加载过程的博客)入方法区;然后因为执行的是new操作,需要创建一个对象,这时候需要在堆上申请内存(内存分配有很多方案,需要考虑多线程下的线程安全问题等诸多因素,不详细阐述),用于存放对象的相关数据(对象头,实例数据,类型指针,占位符等);再然后为TestClassB的成员赋“零值”(不同类型的数据,零值不同,基本数据类型int的零值为0,引用类型的零值为null,等);最后,设置对象头。这样对于JVM来说,对象就创建成功了(后面就是执行类的构造方法了,那是属于Java语言层面的创建对象的过程)。

上面总提及一个叫做“对象头”的东西,这个东西跟对象本身没有什么关系,存储的是对象的运行时数据,包括对象的hashcode,对象的锁状态,对象持有的锁等等。比如对象的hashcode,用于指定对象的唯一性,在GC和对象定位等过程中都会用到。

接着,pc加一(此处加一,表示的是加上一个JVM指令的位数,表示的是下一个指令的内存地址),执行下一步:classB.methodB;这是一个方法调用。正如上面所说,方法的执行和结束,意味着方法栈中,栈帧的进栈和出栈。

JVM 运行时内存分配

对象在堆中存放,然而,对象的操作,方法的执行,就进入了“栈”。调用methodB时,methodB栈帧进栈,栈帧包含局部变量表和操作数栈。因为这个地方的methodB不是类方法,所以,局部变量表的第一个变量为调用该方法的类,即classB(this)。操作数栈用于进行当前数据操作,操作结果出操作数栈,并保存进局部变量表。

例子就这样简单的结束了,总的来说,就是类进入方法区,创建的对象在堆中,方法执行的时候,在方法栈中。

下面,我们来看这个有意思的Java面试题。

当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

网上的标配答案:是值传递。Java语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。

其实这确实是一个很无聊的问题,本来也没有太当回事,但是一来,这个问题下面的追问者很多,我查了下知乎,对这个问题的提问者和回答人也很多;二来,答案不够准确,或者说是,没讲到点子上,有人甚至拿《Java核心卷》里的三句话作为答案。

《Java核心卷》对这种问题有如下三句话的描述:

1.一个方法不能修改一个基本数据类型的参数

2.一个方法可以改变一个对象参数的状态

3.一个方法不能让对象参数引用一个新的对象

无可厚非,这三句话总结的很经典,但是这只是简单的说出了结论,原因呢?就用这三句话解释这个问题,给初学者带来的感觉,只是,哦,原来Java还有这么一个定理(限制)。那么一个个由JVM规范导致的结果,都成了需要死记硬背的“定理”。

public class Program { public static void swap(String x, String y) { String temp = x; x = y; y = temp; } public static void main (String[] args) { String a = "testa"; String b = "testb"; swap (a, b); } }

我们接着看这段代码,将它还原到内存中。

JVM 运行时内存分配

图中“0x”开头的是十六进制的内存地址,随便举的例子。在main方法调用swap方法的时候,只是将main的局部变量表中的a和b的值(指向运行时常量池的地址)拷贝到swap的局部变量表中的x和y,在swap的局部变量表中进行的换值操作,并未对main局部变量表起作用,所以,在swap退出前,x的值是“testb”, y的值是“testa”,x与y的值互换了,但a与b的值并没有因此而改变。当然,swap退出之后,相应的局部变量表会被回收,也就没有所谓的x和y了。

这是这个问题所真正涉及的知识点,我很认同知乎上那位朋友的话,没有必要非得分出个所谓的“值传递”和“引用传递”。

这边我顺便提一点在C中,是怎么做到交换上面例子中a和b这两个值的。

在C中有一个很神奇的东西,名字叫“指针”。可以很简单的认为,它就是地址。那么“指针的指针”,就是“地址的地址”。上面以“0x”开头的数据,就是内存地址,如果将这个地址赋值给一个C中的变量,那么这个变量就称为指针变量。那么我们完全可以通过指针,透过中间变量,直接操作a和b中存储的内容(此处说的是地址),甚至是直接操作到“testa”和“testb”。

评论亮了:

传值还是传引用,深入理解java虚拟机中有这个解释,方法的参数是局部变量,存储在栈中,包括8种基本类型和引用类型,是方法调用之前调用者按参数逆序入栈,然后拷贝给被调用者。可见基本类型和引用类型是等价的,都是传值,只不过引用类型代表的含义不是自身,而是堆中的对象类型。也可以用指针来理解,但没有指针也不至于含糊吧!

备注:

Java虚拟机规范没有强制性约束在什么时候开始类加载过程
类什么时候被加载/类加载时机:
第一:生成该类对象的时候,会加载该类及该类的所有父类;
第二:访问该类的静态成员的时候;
第三:class.forName("类名");

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这