最近在编程时,修改方法传入对象的对象引用,并没有将修改反映到调用方法中。奇怪为什么结果没有变化,原因是遗忘了Java对象引用和内存分配机制。本文介绍3个点:
① 该问题举例说明
② 简要阐述Java内存区域
③ 介绍JVM中方法调用的机制
Java中参数传递是值传递,即调用方法时,所有参数的传递都是值传递。基本类型直接将值拷贝给方法参数,引用类型将引用地址拷贝给方法参数。先看两个String类型和对象引用的实例。
public static void main(String[] args) {
String a = "123";
app(a);
System.out.println(a);
}
private static void app(String a) {
//String不可修改,只会重新创建,故main中a不变
a += "456";
}
输出:123
分析:结果并没有因为调用了app方法,而输出123456。如注释中描述,String(由字符数组实现)是不可修改的,所有的修改都会重新创建新的String对象,并且字符串拼接也会重新创建String对象(具体见下文)。也就是说app中的a字符串引用已不再指向main中a指向的内存块,即main方法中a指向的内存块中字符串的值没有发送变化。
下图展示了对象引用与内存块的关系,可以看出来main方法中的与app方法中的a没关系,只是刚调用赋值的时候指向同一个内存块。
通过下图中字节码命令可以看到,字符串拼接是通过StringBuilder实现的。
toString()的源码如下:
@Override
public String toString() {
// Create a copy, don‘t share the array
return new String(value, 0, count);
}
重新构建String对象,并且注释中说明创建拷贝,但不共享字符数组。String构造函数底层会调Arrays.copyOfRange(char[] original, int from, int to)方法,将original字符数组拷贝到一个新的字符数组中,源码如下:
public static char[] copyOfRange(char[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
char[] copy = new char[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
public static void main(String[] args) {
ListNode node = new ListNode(4);
System.out.println(node);
chg(node);
System.out.println(node.val);
}
private static void chg(ListNode node) {
node.val+=1;
System.out.println(node);
node= new ListNode(2);
System.out.println(node);
}
static class ListNode{
private int val;
private ListNode next;
ListNode(int val){
this.val = val;
}
}
输出:
test.InsertSortTest$ListNode@2a139a55
test.InsertSortTest$ListNode@2a139a55
test.InsertSortTest$ListNode@15db9742
5
分析:① test.InsertSortTest$ListNode@2a139a55地址对应的对象,在main方法调chg方法传递参数的时候,将地址拷贝给chg方法的参数node,在chg方法中修改对象的值,此时两个方法中的node仍指向统一内存块,故main方法中输出为5。
② node= new ListNode(2) 语句将chg方法中的node重新指向另一个对象地址test.InsertSortTest$ListNode@15db9742,此时main方法和chg方法中的node指向不同的对象。
具体Java虚拟机运行时数据区的划分,网上有很多相关资料,还可以看《深入理解Java虚拟机》,在此就不赘述了。但需要说明一点JDK7和JDK8稍有不同,就是JDK8中将原有的方法区(Method Area)或永久代改为元空间(MetaSpace),即将存储类信息、静态变量等元数据信息从方法区(也是堆内存)移动到本地内存(native memory)中。将不会出现java.lang.OutOfMemoryError: PermGen异常,如果该区域设置了大小,可能会出现java.lang.OutOfMemoryError: Metadata space异常,如果不设置大小,默认是自增的。
JDK7中JVM运行时数据区的划分如下图:(JDK7时,已将字符串常量池从方法区移到堆中)
JDK8中JVM运行时数据区的划分:
这两张图分别参考自选择JDK1.8的理由之JVM内存变化和深入理解系列之JDK8下JVM虚拟机(1)——JVM内存组成,其中对变化也阐述的比较清楚。
栈帧是支持虚拟机方法调用和方法执行的数据结构,每个方法调用都对应一个栈帧。栈帧中包含局部变量表、操作栈、动态连接和方法返回地址等信息,结构如下图所示(图摘自Java —— 运行时栈帧结构):
具体内容的介绍参考书《深入理解Java虚拟机》。简单总结如下:
1. 基本概念
2. 方法调用
(1)解析
(2)分派
3. 方法执行
Java虚拟机采用基于栈的字节码解释执行,过程涉及字节码指令、程序计数器、局部变量表和操作栈等,具体例子可参考书《深入理解Java虚拟机》。
《深入理解Java虚拟机》
原文:https://www.cnblogs.com/shuimuzhushui/p/11350794.html