深入理解JAVA虚拟机2——对象的创建与访问

有了上一篇的基础深入理解JAVA虚拟机学习笔记1——内存,这一篇我们就来分析一下,代码到底时如何运行的。

以下面两段代码为例,包含两个类,一个是用来和大家打招呼的具体业务类Main.java。

import java.util.Date;

public class Main {

    private String hello = "Hello World!";

    private void greeting(){
        String preStr = "今天是";
        System.out.println(preStr + Utils.format(new Date()) + hello);
    }

    public static void main(String[] args) {
        Main main = new Main();
        main.greeting();
    }
}

另一个是用来格式化时间的工具类Utils。

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Created by pc on 2018/5/10.
 */
public class Utils {

    public final static String PATTERN = "yyyy-MM-dd";

    //日期格式化成字符串
    public static String format(Date date){
        SimpleDateFormat time=new SimpleDateFormat(PATTERN);
        return time.format(date);
    }

}

首先明确一下,当前程序是在windows系统下进行的,JDK使用的是1.8。这次我们不使用开发工具,而是直接敲命令。

首先,我们要做的是编译Java文件,因为现在文件还在硬盘上,只有通过编译,解析成.class文件,才会加载到内存中。

运行CMD,执行命令:javac D:\project\study\src\Main.java  这个时候,系统会去环境变量的Path路径中找JDK的路径。如果没有,会报“javac不是内部命令”的错误。

这里,我运行的时候报了“编码GBK的不可映射字符”错误,我们在命令中增加UTF-8编码参数。

再次执行:javac -encoding utf-8 -d .  D:\project\study\src\Main.java,会看到下面这个错误,为什么呢?

因为编译的时候,虚拟机会去检查是否能在常量池中定位Utils这个类,并且检查这个类是否被加载,解析和初始化过,而此时Utils类还没有被编译过,所以会报找不到符号的错误(如果是切换到D:\project\study\src目录下去执行就不会出现这个问题)。

于是我们执行命令,先加载Utils类:javac -encoding utf-8 -d .  D:\project\study\src\Utils.java,这个时候就会在C:\Users\pc下生成一个Utils.class文件。

再执行刚才那条语句,这时编译通过,同样会生成一个Main.class文件,这时,类就被加载到内存当中了。

然后我们再运行命令:java Main,Main.java类正确运行,出现了我们想要的结果。

假如这个时候我们运行一下命令:java Utils,(我这个时候已经直接切换到类所在目录下了)会出现什么结果呢?会因为没有main()方法而报错。

下面我们改造一下这个类,增加两条语句,

import java.util.Date;

public class Main {

    private String hello = "Hello World!";

    private void greeting(){
        String preStr = "今天是";
        System.out.println(preStr + Utils.format(new Date())+ "," + hello);
    }

    public static void main(String[] args) {
        Main main = new Main();
        System.out.println(main);
        System.out.println(new Main());
        main.greeting();
    }
}

再次运行,会看到如下图所示的结果,@后面对应的就是对应的内存地址。

加载类的时候虚拟机会为新创建的对象分配内存,有两种分配方式。

1. 指针碰撞:即有一个指针当作空闲内存与已用内存的分界线,分配内存的时候把指针像空闲内存处移动与对象大小相同的距离。(Java堆是否规整取决于垃圾收集器是否带有压缩整理功能)

2. 空闲列表:即空闲内存和已用内存是不连续的,就需要从列表中找到一块足够大的空间分配给对象。

另外,要注意到一点,虚拟机会将分配到内存空间都初始化为零值。这样,new的对象可以不赋初始值就能使用。这个我们可以简单测试一下,运行如下代码。

package test;

/**
 * Created by pc on 2018/5/12.
 */
public class TypeTest {

    private Integer initInteger;
    private int initInt;

    public static void main(String[] args) {
        TypeTest typeTest = new TypeTest();
        System.out.println("initInt:"+typeTest.getInitInt());
        System.out.println("initInteger:"+typeTest.getInitInteger());
    }

    public Integer getInitInteger() {
        return initInteger;
    }

    public void setInitInteger(Integer initInteger) {
        this.initInteger = initInteger;
    }

    public int getInitInt() {
        return initInt;
    }

    public void setInitInt(int initInt) {
        this.initInt = initInt;
    }
}

运行结果如下,这里也可以看出,基本数据类型int和数据的包装类型Integer的初始值是不一样的。

执行完new 命令后会执行<init>方法,可以理解为执行类的构造方法,处理我们对初始值的一些设置。

对象的内存布局分三块区域:对象头,实例数据,对齐填充。

对象的访问定位:

通过句柄访问对象:栈中reference存储的是对象的句柄地址,句柄中包含对象实例数据与类型数据各自的具体地址信息。优点:对象移动时只改变句柄中的实例数据指针,本身不修改。

通过直接指针访问对象:栈中reference存储的是对象实例数据,然后在对象实例数据中包含指向对象类型数据。

优点:节省了一次指针定位的时间,访问速度更快(使用较多)。

喜欢文章或想一起学习的朋友可以关注我,我将会持续更新,有什么疑问或文中有不当之处请给我留言,真诚地希望能与大家一起交流探讨,学习进步。


猜你喜欢

转载自blog.csdn.net/wuyuwuqiu/article/details/80414428