学习笔记之JavaSE(9)--Java基础语法8

时间:2023-02-16 07:44:55

第三个遗留问题,就是引用和对象是什么?这个问题会在“面向对象编程”详细讲解,在这里提出这个问题主要是借机学习一下JVM运行原理个知识非常重要!!!)。


JVM在执行Java程序的过程中会把本进程所管理的内存划分为五个部分,分别是程序计数器虚拟机栈本地方法栈方法区


第一部分:程序计数器

类似于PC寄存器,是一块较小的内存空间,它可以看作是当前线程所执行的字节码指令的行号指示器为了让线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。如果线程正在执行一个Java方法,这个计数器记录的是正在执行的字节码指令的地址;如果正在执行的是Native方法,计数器值为Undefined。


第二部分:虚拟机栈

与程序计数器一样,虚拟机栈也是线程私有的,JVM创建新线程的同时会为这个线程创建一个虚拟机栈,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个帧栈,用于储存局部变量表、操作数和方法出口等信息,当前被执行的帧栈会被放在栈顶,方法调用结束帧栈弹出。帧栈分为局部变量区操作数栈帧数据区三个部分,方法中的局部变量就是存储在局部变量区的。注意虚拟机栈中存储的变量没有默认初始化,所以局部变量必须要初始化


第三部分:本地方法栈

类似虚拟机栈,区别在于虚拟机栈是为执行Java方法服务的,本地方法栈是为执行Native方法服务的。


第四部分:方法区

被各个线程共享的内存区域,用于存储已被JVM加载的类型信息常量池(上篇文章的两座大山之一)、静态变量、即时编译器编译后的代码等数据。其中类型信息包括类及其父类的全限定名、类的类型(Class or Interface)、访问修饰符(public、abstract、final)、实现的接口的全限定名列表、字段信息(仅包括字段名。类型和修饰符)和方法信息(包括方法的字节码);常量池用于存放编译器生成的各种字面量符号引用(暂时不知道这是啥)。方法区还有一部分是运行时常量池,它不仅包括常量池的所有内容,也有可能加入运行期间产生的新的常量(比如String的intern()方法)。方法区中存储的静态变量是默认初始化的,所以静态变量不必须初始化。

注:java7字符串常量池从方法区移到堆中。java8 整个常量池从方法区中移除。方法区使用元空间(MetaSpace)实现


第五部分:堆

堆也是被各个线程共享的内存区域,是JVM所管理内存中最大的一块,此区域存在的唯一目的就是存放对象实例(其实就是存储对象的实例变量。按照类型信息创建对象实例时,会为这个实例分配内存。堆中存储的变量是默认初始化的所以实例变量不必须初始化。

注:栈空间不足会报*Error,堆和常量池空间不足会报OutOfMemoryError

下面通过程序示例说明JVM的工作原理(以单线程为例):

public class Test22 {

private static int a=1;//静态变量a存储在方法区,字面量1存储在运行时常量池
private final int b=2;//字面量2存储在运行时常量池
private int c=3;//字面量3存储在运行时常量池
private String str_1="abc";//字面量"abc"存储在运行时常量池
private String str_2=new String("abc");//字符串没有被创建

public static void main(String[] args){
A obj=new A(25,"k");
obj.toMyPrint(1);
System.out.println(a);//静态变量可以直接在静态方法中调用
//!System.out.println(b); 非静态变量不可以直接在静态方法中调用
//!System.out.println(c); 非静态变量不可以直接在静态方法中调用
//!System.out.println(str_1); 非静态变量不可以直接在静态方法中调用
//!System.out.println(str_2); 非静态变量不可以直接在静态方法中调用
//aa(); 静态方法可以直接在静态方法中调用
//!bb(); 非静态方法不可以直接在静态方法中调用
}

public static int getA() {
return a;
}

public static void setA(int a) {
Test22.a = a;
}

public int getC() {
return c;
}

public void setC(int c) {
this.c = c;
}

public String getStr_1() {
return str_1;
}

public void setStr_1(String str_1) {
this.str_1 = str_1;
}

public String getStr_2() {
return str_2;
}

public void setStr_2(String str_2) {
this.str_2 = str_2;
}

public int getB() {
return b;
}

public static void aa(){}//静态方法aa()的字节码被存储在方法区,可以使用类.方法调用
public void bb(){}//方法bb()的字节码被存储在方法区,由于没有创建Test22对象所以无法调用该方法
}
public class A {		 private String name="abc";//此时常量池中已经存在"abc",不用再创建	 private int age=20;	 //构造函数的重载版本	 public A(){}	 public A(String name){this();		 this.name=name;	 }	 public A(int age,String name){		 this(name);		 this.age=age;	 }	 	public void toMyPrint(int x){		int y=x;		myPrint(y);	}		public void myPrint(int z){		System.out.println(z);	}	public String getName() {		return name;	}	public void setName(String name) {		this.name = name;	}	public int getAge() {		return age;	}	public void setAge(int age) {		this.age = age;	}}
 
 


内存图例:

学习笔记之JavaSE(9)--Java基础语法8

下面结合程序和内存图例说一下JVM的运行原理

  1. 启动程序后,操作系统会为其启动一个JVM进程,并为它分配一片内存空间
  2. JVM将这片内存空间分为上文说的五个部分
  3. JVM从classpath找到Test22.class,读取文件中的二进制数据,将Test22类的类型信息、静态变量a存储进方法区,并将字面量存储进方法区中的运行时常量池。这个过程称为类的加载
  4. JVM先将静态变量a默认初始化为0,然后再将运行时常量池中的字面量1赋给它
  5. JVM读取方法区中Test22类的main()方法的字节码,然后在虚拟机栈创建main()帧栈并执行main()方法
  6. JVM执行语句A obj=new A(25,"k");  首先加载A类,然后声明A类引用obj并存储进main()帧栈,再然后在堆上创建A类对象并给成员变量默认初始化,同时调用构造函数A(age, name),在虚拟机栈创建A(age,name)帧栈并放到栈顶,再然后创建局部变量age和name,赋值并存储到帧栈,再然后继续调用两个构造函数A(name)和A(),重复之前过程,三个构造函数分别调用结束,它们的帧栈弹出虚拟机栈,最后将A类对象赋给A类引用obj。
  7. JVM执行语句obj.toMyPrint(1); 先在虚拟机栈上创建toMyPrint()帧栈并放到栈顶,然后声明int型变量x和y并存储到帧栈,最后给x和y赋值1。
  8. JVM执行语句myPrint(y); 先在虚拟机栈上创建myPrint()帧栈并放到栈顶声明int型变量z并存储到帧栈,再然后给z赋值1,最后打印z的值到控制台上。
  9. myPrint()方法和toMyPrint()方法接连结束调用,它们的帧栈弹出虚拟机栈。
  10. JVM执行main()方法的最后一条语句打印静态变量a的值到控制台。
  11. main()方法结束调用,帧栈弹出虚拟机栈。
  12. 进程结束。
现在我们应该知道,为什么this关键字、非静态变量和非静态方法不能直接在静态方法中调用了吧?这是由于静态变量和静态方法在类第一次加载时即存储 在方法区 ,使用类.变量/类.方法调用即可,无须创建该类对象,所以根本没有“调用方法的那个对象”!而非静态变量和非静态方法必须使用引用才能调用(非静态方法中含有this),所以没有对象压根无法调用它们。
还有注意,String str_2=new String("abc")中字符串“abc”压根没有在堆上被创建。因为在类加载时,只有字面量和符号引用会被存储进运行时常量池。
也就是说,字面量和符号引用是编译时创建,其它都是运行时创建!!


现在Java基础语法学习结束,进入“面向对象编程”模块的学习