一、Java的内存模型
关于Java运行时的内存模型,布局,大部分人了解熟悉的就是堆和栈(这也是我们最关心的俩个区域),然而实际上,JVM的内存模型其实远远不止这两块。实际上,JVM讲内存划分为了5大模块:1、方法区。2、堆。3、JVM栈。4、本地方法栈。5、程序计数器(这5大区域里面,线程私有的是3,4,5这三个模块(即线程自己拥有的空间,其他线程不能访问到的),而线程共享(就是所有线程都能够访问的数据)的则是1和2)。下面笔者就会详细讲解每个区域到底存放那些数据,又有那些特性。
目录
1、方法区
这块区域存放的数据就是已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等(其实方法区保存了很多关于类的信息,包括这个类的类变量、字段、方法、方法的参数、类与方法的修饰符等,而Java中的反射,通过Class对象获取类信息的时候,就是在方法区找到数据的;Class对象就是一个暴露给我们的一种接口,提供了获取类的各种信息的功能。而关于Class对象,JVM并没有强制规定必须在方法区分配,有些JVM分配在方法区,有些分配在堆中)。GC收集器(垃圾收集器)很少光顾这里(关于GC收集器,笔者会在下一篇文章中提及)。
2、堆
堆,可以说是非常重要的一个区域,它存放了几乎所有程序中new出来的对象实例(new出来的对象,都在堆中分配内存,当然,在最新的jdk版本中,这已经不是绝对正确的了)。因为几乎所有的对象实例都在这里分配,所以GC收集器回收的内存大部分都是从这里回收。
可以说,我们new出来的对象的各种数据都是存放在堆里面的,比如创建的Person类里面的name属性的值,你都可以在这里找到。
3、JVM栈
JVM栈,描述的是Java方法执行时的内存模型。每一个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表等各种数据,方法中所有你用到的变量都会在这里找到(基本数据类型,如int,long,double这种,就是保存的变量的值,而像对象,String这种引用类型的变量,它保存的并不是值,而是一个地址,这个地址就指向真实的数据)。
在这里,可能有读者会想到,上面说了,所有new出来的对象都在堆中分配内存,而栈又是用于存储方法的局部变量等数据的,那如果在方法里面,new了一个局部变量出来,这个变量到底是在堆中还是栈中?
就如上面括号中的内容所说,如果在方法里面new了一个局部变量出来,这个变量其实还是在堆中分配的内存,但是,会在栈中存放这个变量的地址,该地址就指向堆中这个变量分配的内存地址。讲到这里,笔者就想说一下关于java参数传递中经常会出错的情况,看如下代码:
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestUnitl
{
//@Autowired
//private IUserLoginServiceImpl userLoginService;
//
//@Test
//public void testForConfig()
//{
// UserAccountEntity test = userLoginService.verifyLoginUser("123456","123456");
// System.out.println("搜索的结果:" + test);
//}
public static void main(String[] args)
{
TestUnitl testUnitl = new TestUnitl();
String originData = "我是原始数据";
System.out.println("原始数据originData的值是:" + originData);
testUnitl.changeValue(originData);
System.out.println("原始数据originData的值是:" + originData);
}
public void changeValue(String data)
{
System.out.println("传递给形参的值是:" + data);
data = "我改变了";
System.out.println("改变之后的值:" + data);
}
}
在看答案之前,读者可以先自己阅读代码,想一下你得出的控制台输出是哪些数据?
下面是这段代码运行之后的结果:
看到答案,可能会有读者感到疑惑,我不是已经把originData传到方法里面,对他进行重新赋值,修改了值了吗,为什么最后输出的结果还是原来的值?
这里,就要考虑上面笔者所说的堆和栈,以及栈中保存的数据了。其实,每个方法的形参,也都相当于是一个局部变量,它也会在栈中分配一个内存用来保存它的值。当你将方法外部的实参传递给形参的时候,对于基本数据类型,就相当于是给形参这个变量赋值了,将他内存的数据修改为你实参一样的数据了,然后在方法内部对形参进行操作时,其实操作的都是这个形参的内存,并没有操作到实参的内存,如下图所示:
而对于引用类型,比如对象、数组,或者String这种类型,实参和形参其实存放的是对象的内存地址(对象在堆中分配,所以这个地址指向堆中的某个位置),并没有保存实参的数据的值。这个内存地址指向了对象的首地址。所以,当在方法体内部用“=”给形参赋值时,其实真正的操作是,修改了形参存放的内存地址,所以形参就指向了另外一个对象,但是实参,它还是指向的原来的对象,所以最后你打印出来的实参变量,他并没有发生改变。(读者可以参考一下上面的图,就是把数值换成了一个内存地址,比如5,改成0x12345678这总内存地址,然后0x12345678这个内存地址的位置是在堆里面,这个地址就是一个对象数据的首地址,存放了对象的数据,这个地址之后就是对象的其他数据,因为笔者比较懒,不想画图了,画图工具太伤。。。。)
在最后,补充一下,访问数组的具体某个值时,以及访问对象的某个属性的时候,其实修改的就是堆中的值,因为当你用a[0]或者object.name这种方法操作时,如,假设最开始,实参a[0]=0,然后当做参数传到一个方法里面,方法体进行了a[0]=10这个操作,如果读者没有完全理解上面的描述,你会认为实参a[0]还是等于0,不会改变值,其实是错误的,这个时候实参的值变了。
“=”是一个赋值语句,左边是内存地址,右边是要赋的值,你用a[0]或者object.name=xxx时,左边的内存地址其实就是真实数据的地址,而像开始说的,str = xxxx时,或者a = {1,2,3}这种直接操作形参的方式,str和a其实是形参的内存的地址,所以最后不会改变实参的值。如还不明白,可以留言,笔者会进行解答
4、本地方法栈
其实这个区域和JVM栈功能差不多,区别就是,JVM栈存放java方法的各种数据,本地方法栈存放native方法各种数据。
5、程序计数器
这个区域唯一的作用就是,存放下一条将要运行的指令的地址。
大概就这么多,其实最主要的就是平常说的最多的堆和栈,另外稍微解释了一下java的参数传递和一个经常看到的错误,如果有什么不懂的,大家可以留言,如果有什么不对的,也希望大神指正。