0
点赞
收藏
分享

微信扫一扫

性能调优专题(9)之从JDK源码级别解析JVM类加载机制

TiaNa_na 2024-11-12 阅读 10
java

从 JVM 的角度来看,Java 中的参数传递之所以是值传递,是因为在 JVM 执行方法调用时,参数的值(不论是基本类型还是引用类型)都被复制并压入调用栈的帧(stack frame)中。让我们从 JVM 的内存模型和调用栈的角度,详细分析为什么 Java 中是值传递。

1. JVM 的内存模型

JVM 的内存分为几个重要区域,其中与方法调用和参数传递相关的主要有:

  • 堆(Heap):用于存储对象实例。所有对象都存储在堆中,所有引用类型(如 StringList 等)变量其实是指向堆中对象的引用。
  • 栈(Stack):用于存储方法调用的局部变量和临时数据,每个线程都有一个自己的栈。当方法被调用时,JVM 会在栈中为这个方法分配一个新的栈帧(stack frame)来保存方法的参数、局部变量和一些临时数据。

2. 方法调用和栈帧

当方法被调用时,JVM 会在栈中为这个方法创建一个新的栈帧。这个栈帧包含:

  • 参数值的副本:方法的每一个参数(不管是基本类型还是引用类型)都会被复制一份,并存储在栈帧中。
  • 局部变量:方法内定义的局部变量也会存储在栈帧中。

栈帧在方法执行完毕后会被弹出栈,并释放所有数据。

3. 值传递的工作原理

当方法被调用时,JVM 会根据传入的参数创建它们的副本:

  • 基本类型参数(如 intdouble 等):直接将参数的值复制一份,并将这个值存储到栈帧中。
  • 引用类型参数(如 StringArrayList 等):传递的实际上是引用的副本。引用本身是一个指向堆中对象的地址(即对象的内存位置),这个地址值被复制并存储到栈帧中。

这意味着:

  • 基本类型:在方法内部对参数的修改只会影响副本,不会影响原始变量。
  • 引用类型:传递的是引用的副本,因此在方法内部可以通过这个副本引用操作对象本身,但如果修改引用的指向(让引用指向新的对象),不会影响原始的引用。

举例分析:基本类型的值传递

public class Test {
    public static void main(String[] args) {
        int x = 5;
        modify(x);
        System.out.println(x); // 输出:5
    }

    public static void modify(int num) {
        num = 10;
    }
}
  • modify(x) 被调用时,x 的值(即 5)被复制一份并传递给 num,此时 num 变量的值是 5
  • modify 方法中,将 num 的值改为 10,但这不会影响 main 方法中的 x,因为 num 只是 x 的副本。
  • JVM 机制:在 modify 方法的栈帧中,num 存储了 5 的副本。当方法执行完后,栈帧被销毁,num 消失,x 的值仍然是 5

举例分析:引用类型的值传递

import java.util.ArrayList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        modify(list);
        System.out.println(list); // 输出:[A, B]
    }

    public static void modify(List<String> lst) {
        lst.add("B");
    }
}
  • modify(list) 被调用时,list 引用(地址)被复制一份,并传递给 lst
  • listlst 都指向堆中同一个 ArrayList 对象。
  • JVM 机制:在 modify 方法的栈帧中,lstlist 引用的副本,因此 lstlist 指向同一个对象。对 lst 的操作(如 lst.add("B"))会影响原始对象。

为什么即使是引用类型,Java 仍是值传递?

这是因为在 JVM 中,即使是引用类型,传递的仍然是引用的副本,而不是引用本身的地址引用。

  1. 引用的副本:在方法调用中,引用类型的参数传递的是引用的值,即指向对象的内存地址。这意味着方法内部的引用与外部的原始引用是不同的变量,只是它们指向同一个对象。
  2. 修改引用指向:如果在方法内部将引用重新赋值(让它指向另一个新对象),这种修改只会影响方法内部的引用,不会影响外部的原始引用。这进一步验证了 Java 是按值传递的,因为重新赋值并不影响原始对象的引用。

例如:

public class Test {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Hello");
        modify(sb);
        System.out.println(sb); // 输出:Hello
    }

    public static void modify(StringBuilder sb) {
        sb = new StringBuilder("Goodbye"); // 重新赋值,原始对象不受影响
    }
}

modify 方法中,将 sb 重新赋值为一个新对象,这不会影响 main 方法中的 sb,因为 modify 方法中的 sb 只是原始引用的副本,重新赋值只影响副本。

总结

从 JVM 的角度,Java 是值传递,因为:

  1. 参数复制到栈帧:方法调用时,所有参数的值(无论是基本类型的值还是引用类型的地址)都被复制到新建的栈帧中。这些值的副本和原始值是独立的。
  2. 引用类型传递的是地址的副本:对于引用类型,传递的是指向对象的地址(引用)的副本,方法内部可以通过这个副本引用修改对象的内容,但不能改变原始引用本身。
  3. Java 中没有按引用传递:在 Java 中,不存在直接传递对象引用地址的情况,始终是传递的值的副本。

这种实现方式确保了 Java 的方法调用是“安全”的,不会因为方法内部的参数操作而直接改变调用方的变量。

举报

相关推荐

0 条评论