有了上一篇的基础深入理解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存储的是对象实例数据,然后在对象实例数据中包含指向对象类型数据。
优点:节省了一次指针定位的时间,访问速度更快(使用较多)。
喜欢文章或想一起学习的朋友可以关注我,我将会持续更新,有什么疑问或文中有不当之处请给我留言,真诚地希望能与大家一起交流探讨,学习进步。