java-jvm学习(2)

       考虑这么一段代码:

package test;

/**
 * Test :
 * 
 * @author xuejupo [email protected] create in 2016-1-27 下午6:46:39
 */

public class Test {

	/**
	 * main:
	 * 
	 * @param args
	 *            void 返回类型
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Test t = new Test();
		Test1 t1 =  t.new Test1();
                String s = "test";
		t1.test1();
	}

	class Test1 {
		private static final int i = 1;
		private int i2 = 1;
		private Test2 t = new Test2();

		public void test1() {
			this.test2();
		}

		public void test2() {
			
		}
	}

	class Test2 {

	}

}

       它在jvm内存模型中的位置是怎么样的?代码里的变量分别在堆,栈,方法区,常量池的什么位置?

      首先,jvm对栈的操作是以栈帜为单位的,栈帜保存的是方法数据,而且栈是不可共享的,对于类的成员变量来说,不可能在栈中。方法区用于保存已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据(JIT),方法区物理上是堆的一部分,但逻辑上应该与堆区分开,看深入jvm上的解释,方法区保存的是类的信息,跟类的实例对象是没有关系的。

     所以,类的成员对象是保存在堆中的,如类Test1中的i2和Test2的引用t。

       对于方法区中的常量池,深入jvm是这么说的:存储编译期生成的各种字面量和符号引用。

      比较绕的是这个符号引用。。。 以下是根据网上查询的结果和自己的理解总结的:

       符号引用说白了就是字符串,这个字符串包含足够的信息,可以在使用的时候找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。   当第一次运行时,要根据符号引用(字符串)的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
     总之,常量池中保存的是不会改变的对象,如基本类型的包装类,String和符号引用(比如上段中,方法是肯定不会改变的)。
       所以,在上面的代码里,常量池中保存的是i的值,test1()方法在类Test1中的位置,
       方法区保存的是类的信息,类的静态变量等信息。对于以上代码,方法区中保存的是(除去常量池)Test1中i的引用(这点有疑问。。  不确定),三个类的信息,类中的方法信息等。
       最后说栈,jvm的栈的操作单位是 栈帜, 栈帜在深入jvm中的描述是:存放局部变量,操作数栈,动态连接,方法出口等信息。由此可知栈中存放的是方法中定义的局部变量。  在上面的代码里表现的就是:main方法中的引用t1,字符串引用s;以及Test1类,test方法中的Test2的引用和各个方法的相关信息。
        最后总结一下上面代码的执行过程(个人理解,如有不当,大神狂喷)
        1) 第一步,肯定是编译java文件,生成字节码class文件 
        2)虽然我们看来是执行main函数,但是在jvm看来,加载class文件肯定要比执行main函数优先级高得多。。。  第二步就是加载class文件。上面的代码里一共生成了三个class文件,一个Test,两个内部类:Test$Test1,Test$Test2,Test和Test$Test2没啥可说的,没有static变量,方法区只保存类的基本信息和main方法入口。加载 Test$Test1的时候,会加载变量i和方法test1,test2。
        3)加载完3个类,就开始执行main函数了。在这之前,是不涉及堆和栈的。 加载main函数,开始涉及栈,首先将main函数的相关信息(在方法区里)加载进栈中(这算一个栈帜 ),然后执行这个 栈帜,进行到Test t = new Test()这步的时候,在堆中创建类Test的实例对象,并初始化对象的成员变量(Test没有成员变量,所以只是根据方法区中Test类的信息创建一个Test的实例对象),然后这个对象的地址引用在main方法的 栈帜中(这就是平常所说的引用在栈,对象在堆)。
      4)执行到Test1 t1 =  t.new Test1();的时候,创建一个Test1的实例对象,并初始化对象的成员变量(i2,t,其中t又是类Test2的对象,所以这时候又在堆中创建了Test2类的一个实例对象),i2和t的引用地址在 栈帜中,i2的值在常量区中,t的值在堆中。
      5)执行到t1.test1();的时候,main函数所在的 栈帜根据引用地址t1,从堆中找出Test1对象,然后根据Test1对象的信息,从方法区的常量池中找到类Test1中方法test1()的地址(就像上面说的,第一次引用方法的时候,在常量池中保存的是方法的符号引用 ,如"Test.test1()V",是一串能够在类Test1中找到方法test1的字符串,运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置 )。找到test1之后,将test1的相关信息保存进一个 栈帜,然后将该 栈帜压入栈中。
      6)执行test1方法,根据5)中描述的过程,找到test2方法,然后将test2方法压栈
     7)执行test2方法,执行完毕后test2出栈;
      8)test2出栈后,栈顶就是test1方法了,然后看test1方法是否执行完毕,如果没完就继续执行,如果完了就出栈,直到栈空,整个程序结束。
        使用递归的时候,经常看到栈溢出的异常,就是说的这个虚拟机栈的 栈帜个数超过限制了。而且每一个 栈帜表示一个方法,每个 栈帜都是有大小的,这也是为什么很多大神不建议递归的原因,因为递归是需要消耗额外的资源的。
        以上都是自己整理深入jvm和网上各路大神的答案得出的结论,如有不当,欢迎狂喷!

猜你喜欢

转载自709002341.iteye.com/blog/2274402