为了账号安全,请及时绑定邮箱和手机立即绑定

《手册》详解 第2节学员提问之断点 i=255问题解析

标签:
Java

一、背景

码出规范:《阿里巴巴Java开发手册》详解 专栏刚发布, 其中 “第二节 02 Integer缓存问题分析”讲到:

Integer var = ? 形式声明整型,最终会通过 java.lang.Integer#valueOf(int) 来构造整数对象。

验证方法有两种:一种是直接在该源码处断点;另外一种是通过javap反汇编的方式。

有一位同学提出了一个非常有价值的问题:


那么为什么断点后 i=255 呢?接下来带着大家来研究这个问题。


示例代码:

public class IntegerDemo {
    
    public static void main(String[] args) {
        Integer var = 12;
        System.out.println(var);
    }
}

执行后的表现:

https://img1.sycdn.imooc.com//5db987fa0001a9f906800137.jpg

这个问题非常有意思,让我们继续分析。

二、问题分析

研究这个问题我们主要从 3 个角度,第 1 个角度是思考角度; 第 2 个角度是调试’ 第 3 个角度是 加虚拟机启动参数。

2.1 思考

专栏的前面也讲到了,专栏的主要价值之一就是教会大家思考问题,而不仅仅是解决某个具体的问题。

很多同学很少在JDK里打断点,通常是在自己代码中打断点,通常调用入口只有自己的代码,因此会潜意识认为我打断点之后,一定是自己写的代码调用进来的。

但是这里显然并不是自己写的代码。

因此我们推测这里的 java.lang.Integer#valueOf(int)  调用来源并不是我们自己写的代码。

注意这里很多同学就开始怀疑自己写的 12 这里变成了 255 ,其实并不是,请继续执行。

在 JDK 8的环境中,我们会发现多次断点中断执行。

我们放过这次调用,后面还有几此断点断住,一次 i = 255, 0, 30,0,127,最后一次是我们写的12。

通过观察我们可以推测,除了我们自己写的main函数中声明的  var = 12 外,肯定有一些源代码有类似

Integer var = ? 写法。


2.2 断点调试

很多新手,甚至工作两三年的人调试用总是断点,单步这些基本用法,很少去关注调用栈的信息(专栏后续代码调试的正确姿势章节将讲述一些高级用法)。

我们调试时要注意观察这里。

红色区域,就是当前的调用栈,双击可以走到源码中。

https://img1.sycdn.imooc.com//5db98851000163e110600480.jpg

双击 java.lang.invoke.MethodHandleImpl类看其源码:

static {
    final Object[] values = { 255 };
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        @Override
        public Void run() {
            values[0] = Integer.getInteger(MethodHandleImpl.class.getName()+".MAX_ARITY", 255);
            return null;
        }
    });
    MAX_ARITY = (Integer) values[0];
}

其实,第一个 255 和第二个 255 分别是这个静态代码块中 

final Object[] values = { 255 };

values[0] = Integer.getInteger(MethodHandleImpl.class.getName()+".MAX_ARITY", 255);

前两次断点中断,是这两处构造整数对象时调用的,不信大家可以在这里断点,单步执行。

java.lang.Integer#getInteger(java.lang.String, int),底层也会通过java.lang.Integer#getInteger(java.lang.String, java.lang.Integer)

如果获取不到值是,则会通过Integer.valueOf(val) 来获取默认的整数值,即 255.

public static Integer getInteger(String nm, int val) {
    Integer result = getInteger(nm, null);
    return (result == null) ? Integer.valueOf(val) : result;
}

继续放过,又会在 i = 0 处断住。

我们发现上层是通过 java.lang.invoke.MethodHandleStatics 的静态代码块调用过来的。

https://img1.sycdn.imooc.com//5db98a4700018e6411810734.jpg

和上面一样,分别调用到了 java.lang.Integer#valueOf(int) ,构造 0 30 0 127 四个整数对象。

然后继续运行,此时 i= 12,注意观察调用栈!此时才是从我们自己编写的源代码调用过来的。

https://img1.sycdn.imooc.com//5db98a950001ae4208860666.jpg

双击main 函数 进入源码

https://img1.sycdn.imooc.com//5db98af500012e3b07880489.jpg

通过调试,可以证明 我们自己写的 Integer var = 12; 最终通过调用 java.lang.Integer#valueOf(int)构造整数对象的。

那么到这里,我们证明了专栏中的表述,并且证明了第 1 步的猜测。


另外我们还可以在打印语句处断点,看memory选项卡,搜索Integer查看加载的整数对象的个数。

https://img1.sycdn.imooc.com//5db98d1c000141f726041765.jpg

这里 256个整数对象,我们断点没构造那么多啊?

别忘了,除了上述方法外,通过 new 来构造整数对象更常见。

如果感兴趣可以断点尝试一下,其他的对象大多数应该是通过 new Integer方式构造的。

2.3 虚拟机参数法

为了更好的证明我们的猜想,我们加入虚拟机参数:-XX:+TraceClassLoading,来打印加载的类。

https://img1.sycdn.imooc.com//5db98beb0001db3e09190329.jpg

发现,的确加载并初始化了  MethodHandleImpl 和 MethodHandleStatics 两个类。

因此执行 java main函数之前会加载到了这两个类,这两个类初始化时,静态代码块的执行调用了调试的源码( java.lang.Integer#valueOf(int)),造成了上述问题。

2.4 拓展

那么为什么会加载这两个类呢?

简单而言,编译执行我们写的函数需要通过其他 java 对象来表示函数调用(还记得吗 class 文件都需要用Class 对象来表示),这里的MethodHandle 就是函数调用的表示形式之一。

按照我们专栏的惯例,找《Java 虚拟机规范》, 我们发现里面由有一段描述:

The result of successful method handle resolution is a reference to an instance of java.lang.invoke.MethodHandle which represents the method handle MH.

The type descriptor of this java.lang.invoke.MethodHandle instance is thejava.lang.invoke.MethodType instance produced in the third step of method handle resolution above.

显然证实了 java.lang.invoke.MethodHandle 就代表着函数调用(类似于 Class 表示 class文件的对象表示形式)。

我找到另外一段关键的描述:

In Java SE 8, the only signature polymorphic methods are the invoke and invokeExactmethods of the class java.lang.invoke.MethodHandle.

Java 8 中,只有 java.lang.invoke.MethodHandle的 invoke 和 invokeExact 是签名多态性方法。


那么什么是多态性方法?

有两种含义,一种是静态多态性也称编译期多态,实现方法的重载;一种是动态多态性即运行期多态,实现方法的重写。

那么是不是因为我们的 Integer.valueOf 重载才用到了那个类呢?

https://img1.sycdn.imooc.com//5db992cb00014b0804390388.jpg

我们可以写一个代码(没调用含有重载和重写)证明一下:

public class IntegerDemo {

    public static void main(String[] args) {
        Runtime.getRuntime().gc();
    }
}

继续在 java.lang.Integer#valueOf(int) 代码断点,发现依然会执行到断点,如果加上面的打印加载类信息的虚拟机参数依然可以看到加载了MethodHandle类。

举这个例子,希望大家能够自主编写代码来验证问题,而不是看到博客就相信,而不是只看到书上的知识点就背诵和别人讨论。猜想并验证比单纯记忆知识点本身更有价值。

因此可以理解为,java 8 使用到了 MethodHandle来表示方法调用,因此会加载并初始化,执行其动态代码块,从而调用到了 java.lang.Integer#valueOf(int) 。

到此整个问题研究结束。

关于该问题,想学习更多细节,请参考《Java 虚拟机规范》、《深入理解 Java 虚拟机》的相关章节。

3、总结

首先,提问的同学很细心,值得点赞。

其次,本文沿袭专栏的核心思路,利用《Java虚拟机规范》等来作为权威的工具书解决问题。

学习的态度很重要,很多人遇到奇怪的问题都会放过,这恰恰是你深入掌握某些知识的最好机会之一。

学习的方法和解决问题的能力很重要,本专栏有大量的类似讲解,希望能够带着大家养成好的学习和解决问题的习惯,提高学习的能力。

另外再次强调方法、思考才是最重要的,懂得方法之后才能脱离《手册》,快速而自主研究学习其他内容。

希望能够有更多地朋友注意到这一点,能够转变思维,提高进阶的步伐,这也是本专栏的核心目的。

通过这个问题希望大家明白,很多看似简单的问题其实有很多学问;很多和自己想法不一致的问题背后一定是有自己没掌握的技术。

通过这个问题希望大家明白,很多看似简单的问题其实有很多学问;很多和自己想法不一致的问题背后一定是有自己没掌握的技术。


掌握方法是我们的学习目的,具体知识点只是我们练习方法的途径,希望最终大家可以脱离《手册》,脱离专栏,掌握学习和分析问题的方法,将其运用到其他知识的学习中。


另外如果有疑问欢迎在专栏下留言,下班之后会尽可能为大家解答问题。

专栏购买链接:https://s.imooc.com/WP71m6k


参考文章

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9

https://www.sitepoint.com/quick-guide-to-polymorphism-in-java/


点击查看更多内容
19人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消