Java中类的概念就不细说了,万物皆对象。我开始也不怎么理解,时间长了慢慢也就明白了。这篇主要来聊一下java中类和虚拟机之间的一些事情,在这之前先了解下虚拟机的组成栈、堆和方法区。
先从我们的文件说起,我们平常编写的java代码都是以.java为结尾的文件,也就是源代码。以Stu类为例,Stu.java文件经过javac -Stu.java命令编译后变为Stu.class文件,接下来就需要执行java Stu.class命令把Stu.class文件放进内存的java虚拟机内。
然后再说java虚拟机,Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境,是在真机上开辟的一块内存空间,这块内存空间也就是虚拟机上主要分为栈区和堆区,此外还有运行时数据区、计数器和方法区。
栈
栈是先进后出的,对于线程和方法的执行顺序也是一样,由此也可以解释递归方法调用过程也就是不断入栈的过程,最后一个方法返回才会逐级返回递归方法调用,也就是依次出栈的过程。栈中的每段又叫栈帧,栈帧是要压入或者弹出的,每执行一个方法时都会先创建一个线程再由线程创建一个栈帧并入栈。假如有方法a,a中调用方法b,则先创建一个执行方法a的线程然后再创建方法a的栈帧并入栈,然后在方法a执行到调用方法b的时候再创建执行方法b的线程,再创建方法b的栈帧并入栈。
public void a(){
String s = "abc";
int a = 10;
b();
}
public void b(){
}

栈帧内主要存放局部变量、指向运行时常量池的引用、操作数栈、方法出口地址以及一些附加信息。Java中栈帧是线程私有的,即一个线程一个栈帧,各个线程之间不共享。
局部变量:指被执行方法中的局部变量,包括非静态变量和函数形参。
非静态变量存储情况有两种:1、基本数据类型存储值 2、引用类型存储的是指向该对象的引用(内存地址)。
堆
存储对象本身和数组的,每一块都不规则,大小也根据实际对象大小而不同,JVM只有一个堆,所以是被所有线程共享的。
注意:所谓的对象本身并不包括类内的静态变量、静态方法、类本身的代码和类内的字符串常量(这一部分在方法区),在堆内的只有普通方法和普通变量。

方法区(存在堆的内存区域内)
在虚拟机内一块所有线程共享的内存逻辑区域,JVM中只有一个方法区,存储线程可共享的内容,线程安全的,多个线程同时访问方法区中同一内容时,只能有一个线程装载数据,其他线程只能等待。
方法区主要存储class文件中的静态变量、静态方法、字符串常量和class文件的类(所有class)的代码。见下面图3。class文件进入虚拟机后,类的代码、类内静态方法、类内静态变量和代码内所有字符串常量全部存储到方法区。
对象创建过程
也就是Clazz clazz = new Clazz();这一步骤的过程。
#前提是加载完.class文件
对象的创建过程主要分下面五个步骤:
- 在main方法的栈帧内开辟clazz变量空间,并赋默认初始值null,(即Clazz clazz = null;)。
- 分配对象空间,然后对象成员变量默认初始化,数值为0,boolean为false,引用类型为null。
- 执行属性值的显式初始化 (就是给对象属性赋初始值,而不是默认的初始化值)。
- 执行构造方法,通过构造方法给对象成员变量赋值(即调用构造方法Clazz()初始化对象)。
- 返回对象的地址给相关的变量,此处是返回clazz对象在堆中的内存地址给clazz变量。
虚拟机
先上类到虚拟机整个执行过程的图(代码我放最下面):
注意:图中的Stu类的代码只是为了看着方便,不属于虚拟机的一部分

源代码文件经过编译最后通过java -文件名.class命令加载到虚拟机中,然后所有代码和静态变量、静态方法、字符串常量都存储在方法区,从main方法进入。(图中红箭头为调用步骤的指向)
以下为详细讲解:
main方法入口:创建执行main方法的线程,创建栈帧并入栈到栈区。
第一句代码(创建Stu对象):创建Stu类型的变量jack,并放入到栈帧的局部变量表,然后通过new关键字调用Stu类的构造函数,此时创建Stu()构造函数的执行线程并入栈,构造函数去堆开辟一块内存空间,用来存储一个Stu类对象的数据,并把这块内存的首地址赋值给栈内jack变量。也就是完成了Stu jack = 29c2fff0;这句代码,并进行变量的初始化,如下:
//jack对象在堆上的实时变量及值
id = 0;
age = 0;
name = null;
mydog = null;
play();
study();
最后Stu()构造函数出栈,相应线程销毁,继续main方法的线程。
后面三句:通过栈帧内jack这个对象变量的内存地址找到对象在堆内的位置,并将jack对象的id、age、name重新赋值。
//jack对象在堆上的实时变量及值
id = 1001;
age = 18;
name = "杰克";//这里的"杰克"字符串是提前存储在方法区的字符串常量
mydog = null;
play();
study();
第五句代码(创建Dog对象): 创建Dog类型的变量hsq,并放入到栈帧的局部变量表,然后通过new关键字调用Dog类的构造函数,此时创建Dog()构造函数的执行线程并入栈,构造函数堆开辟一块内存空间,用来存储一个Dog类对象的数据,并把这块内存的首地址赋值给栈内hsq变量。也就是完成了Dog hsq = 29c2fff5;并进行变量的初始化,如下:
name = null;
最后Dog()构造函数出栈,相应线程销毁,继续main方法的线程。
后面一句:通过栈帧内hsq这个对象变量的内存地址找到对象在堆内的位置,并将hsq对象的name重新赋值。
name = "哈士奇";//字符串"哈士奇"也是提前存储在方法区的字符串常量
第七句代码: 获取hsq这个栈帧变量的值(hsq对象在堆内存的地址),通过jack变量的值(jack对象的堆内存地址)找到jack对象,将其中的mydog变量重新赋值,为栈帧变量hsq的值(hsq对象在堆内存的地址)。
//jack对象在堆上的实时变量及值
id = 1001;
age = 18;
name = "杰克";//这里的"杰克"字符串是提前存储在方法区的字符串常量
mydog = 29c2fff5;
play();
study();
第八句代码:根据jack栈帧变量的地址获取到jack对象在堆上的内存地址,再找到jack对象的play()方法并调用,此时创建执行play()函数的线程并将play()函数入栈,此时执行play()函数的内容,输出一句话,同时拿到方法区静态变量并按当前jack对象的mydog属性所存储的内存地址找到hsq对象的属性name,然后将两者打印出来,最后play()方法出栈,相应线程销毁,继续main方法的线程。
第九句代码:根据jack栈帧变量的地址获取到jack对象在堆上的内存地址,再找到jack对象的study()方法并调用,此时创建执行study()函数的线程并将study()函数入栈,此时执行study()函数的内容,输出一句话,并调用方法区的字符串常量"Hello World!",最后study()方法出栈,相应线程销毁,继续main方法的线程。
第十句代码: 输出栈帧jack变量的值(即jack对象在堆的内存地址)。
最后到达Main方法的右括号,到此程序执行完成。main()方法执行完成,出栈,相应线程销毁,这便是一个完整的Java程序在虚拟机内的执行过程了。
下面为相应源代码,可以按照上面流程自己独立画一遍图3的内容,对于帮助理解Java虚拟机与Java类和方法的执行过程有很大帮助。
温馨提示:一个class文件里可以有多个class类,但最多只能有一个public标识的类(private和protected不行),有的话类名要与.class文件名一致;也可以没有。
public class Stu {
int id;
int age;
String name;
Dog mydog;
//类内默认构造函数,只是不显示
Stu(){}
void play(){
System.out.println("我有一只小狗,名字是:"+mydog.name);
}
void study() {
System.out.println("Hello World!");
}
public static void main(String[] args) {
Stu jack = new Stu();
jack.id = 1001;
jack.age = 18;
jack.name = "杰克";
Dog hsq = new Dog();
hsq.name = "哈士奇";
jack.mydog = hsq;
jack.play();
jack.study();
System.out.println(jack);
}
}
class Dog{
String name;
}