Java 中的基本类型

Java 的基本类型有整数类型 byte、short、char、int 和 long,以及浮点类型 float 和 double,还有 boolean。

每个基本类型都有对应的取值范围和默认值。boolean、byte、short、int、long、float 以及 double 的值域依次扩大。所以前面的类型转后面的类型无需强转。并且,所有类型在内存中的默认值都是 0。

Java 中的基本类型的存取

Java 虚拟机每调用一个非本地方法 (native) 都会在虚拟机栈中创建一个栈帧,每个栈帧中有局部变量表(包括方法内的、方法入参的和实例方法的 this 引用中的所有局部变量)、操作栈、动态链接、返回地址…..。

为了更好的通过下标来计算地址从而直接访问,boolean、byte、char、short 这四种类型,在栈上 (栈帧中的局部变量表中) 占用的空间和 int 或引用类型是一样的(在 32 位的 HotSpot 中,占用 4 个字节;而在 64 位的 HotSpot 中,占用 8 个字节。)。在操作数栈进行运算时,也是都当成 int 类型的值进行运算。

boolean、byte、char、short 这四种类型,当成 int 类型的另外一个原因是字节码指令包含操作码和操作数(不一定有),操作码仅用1个字节表示,如果每一种与数据类型相关的指令都支持 JVM 所有运行时数据类型则指令的数量可能超出一个字节所能表示的数量范围。因为 JVM 中存在一些单独的指令在必要的时候用来代替将一些不支持类型转换为可被支持的类型。所以,这四种类型的数据在编译期或运行期会进行符号扩展为 int 类型的数据,当遇到这四种类型的数组时转换类似。

而在堆中占用的空间就仅对应于值域的范围。byte、char 以及 short 这三种类型的字段或者数组,在堆上占用的空间分别为 1 个字节、2 个字节,以及 2 个字节。如果将一个 int 类型值存储在堆中,仅会保留低位的两个字节,其余将被截取调。

在 HotSpot 中,堆中 boolean 字段占用 1 个字节,如果存入的数据超过 1 个字节,将会进行截取,截取到只剩低位的 1 个字节。然后取出最低位,0 表示 false,1 表示 true。如下代码:

public class TestBoolean {

  boolean boolValue;

  public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
    TestBoolean test = new TestBoolean();
    Unsafe unsafe = getUnsafe();
    long objFieldOffset = unsafe.objectFieldOffset(TestBoolean.class.getDeclaredField("boolValue"));
    System.out.println("实例变量相对于实例内存地址的偏移量 = " + objFieldOffset);
    // 设置为: 00000000,取最低位,结果为 false
    unsafe.putByte(test, objFieldOffset, (byte) 0);
    System.out.println(unsafe.getByte(test, objFieldOffset));
    System.out.println(test.boolValue);
    // 设置为: 00000101,取最低位,结果为 true
    unsafe.putByte(test, objFieldOffset, (byte) 5);
    System.out.println(unsafe.getByte(test, objFieldOffset));
    System.out.println(test.boolValue);
  }

  // 无法用过 Unsafe 自带的获取方式,只能通过反射获取 Unsafe 类中 theUnsafe 变量下存储的 unsafe 实例
  static private sun.misc.Unsafe getUnsafe() throws IllegalArgumentException, IllegalAccessException {
    Class<?> cls = sun.misc.Unsafe.class;
    Field[] fields = cls.getDeclaredFields();
    for(Field f : fields) {
      if("theUnsafe".equals(f.getName())) {
        f.setAccessible(true);
        return (sun.misc.Unsafe) f.get(null);
      }
    }
    throw new IllegalAccessException("no declared field: theUnsafe");
  }

}
实例变量相对于实例内存地址的偏移量 = 12
0
false
5
true

final 与非 final 的初始化顺序

Java 虚拟机的类加载机制分为 5 个步骤:加载,验证,准备,解析,初始化。如下图:

上图中,连接中的准备阶段是在方法区中对类变量(静态变量)分配内存空间初始化值的阶段。

如下代码:

public static int v = 8080;

对于非 final的变量 v 在准备阶段过后的初始值为 0 而不是 8080,赋值为 8080 的语句存放于类构造器<clinit>中(client 中存放静态代码块和静态变量的初始化语句)。而类构造器<clinit>将会在初始化阶段执行。

而如果该变量被声明为 final,如下:

public static final int v = 8080;

如果变量被声明为final,则在编译阶段,会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v
赋值为 8080。

所以,final 类型变量相比非 fianl 类型变量,省略了一步初始化为 0 的过程,在准备阶段直接赋值为 class 中定义的值。

参考

最后修改日期: 2019年12月22日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。