java内存结构和对象创建的过程

来源:互联网 发布:希区柯克剧场 知乎 编辑:程序博客网 时间:2024/05/01 01:46

点击打开链接


在学到毕老师视频中介绍关于java内存结构的时候,感觉这些内容特别重要。结合视频和对《深入理解java虚拟机》这本书的一些理解写了这篇日记。 

java内存分配和管理是java的核心技术之一。一般java程序运行时会涉及到如下几个存储区域: 

程序计数器: 
可以看做是当前所执行字节码的行号指示器。通常是改变计数器字节码的值来抉择下一步索要执行的代码,程序员不直接控制。 

寄存器: 
在程序中我们无法直接控制,由编译器管理。 

栈区:   
1,每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义的对象的引用以及局部变量。 


2,每个栈中的数据都是私有的,其他栈不能访问。 


3,当局部数据用完时,所占内存空间会自动释放。 
JVM栈一个最大的用处是为执行 java方法服务的,每一个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。 
每一个方法从调用直至执行完成的过程都对应着一个栈帧在虚拟机中从入栈到出栈的过程。 
栈的优势在于存取速度比较快,仅次于寄存器。缺点是存储在栈中的数据的大小和生存周期是确定的,缺乏灵活性。 


堆区: 
1,通过new建立的数组和对象都存储在堆中。 


2,每一个对象都有相应的内存地址值。 


3,对象中的变量都有默认初始化值。 


4,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。 


另外要明确的是每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。在java堆中还必须包含能查找到对象类型数据(如对象类型,父类,实现的接口,方法等)的地址信息, 
这些类型数据(优先于对象存在)都存储在方法区中。 
堆的优势在于可以动态地分配内存的大小,生存周期也不必事先告诉编译器,因为它是运行时动态地分配内存的,并用垃圾回收机制回收那些不用的数据。缺点是存取速度比较慢。 


方法区: 
方法区本身又分为以下几个区域: 
1.静态数据区,存放静态数据的地方 

2.常量区:就是下面要介绍的常量池。 

3.代码区:存放方法代码的区域 
总之,方法区是JVM在装载类文件时,用于存储类型信息的(类的描述信息)地方,是一片给各个线程共享的内存区域。 

常量池: 
常量池是Class文件结构中与其他项目关联最多的数据类型,它本身是方法区中的一个数据结构。在方法区中,每个类型都对应一个常量池, 
常量池中存储了诸如文字字符串、final变量值、类名和方法名常量。JVM把常量池组织为入口列表的形式,可通过索引来访问常量池中的各个入口, 
并用入口标识来表示该常量池存储的数据类型。常量池在编译期间就被确定,并被保存在已编译的.class文件中的一些数据,一般分为两大类,字面量和引用量。 
字面量入全面提到的文本字符串,final变量等。类名和方法名属于引用量。 
引用量最常见的一种用法是在调用方法的时候,根据方法名找到方法的引用,并以此定位到函数体进进行函数代码的执行。 
引用量: 
1,类和接口的权限定名。 
2,字段的名称和描述符。 
3,方法的名称和描述符。 

通过String来了解常量池,例如有如下五个赋值语句: 
String str1 = new String("abc"); 
String str2 = new String("abc"); 
String str3 = "abc"; 
String str4 = "abc"; 
String str5 = "ab"+"c"; 

System.out.println(str1 == str2); //1 
System.out.println(str1 == str3); //2 
System.out.println(str3 == str4); //3 
System.out.println(str4 == str5); //4 

程序依次输出的值是: 
false 
false 
true 
true 
首选要说名的是用String str = "abc";这种方式创建的字符串对象是存储在常量池中的,JVM首先去字符串常量池里找有没有"abc",没有,则将"abc"放入常量池,否则直接将指向常量池中"abc"的引用给str。而通过new创建的字符串对象是存储在堆内存中的。"=="运算符比较的是对象的引用值,str1和str2都有自己的对应的堆空间,故引用值不等,输出false。通过new创建的对象永远都不可能存储在常量池空间!!,故第二个也输出false。java确保常量池中的一个字符串常量只有一个拷贝。故第三个输出为true。当一个字符串有多个字符串常量连接而成时,它本身也是一个字符串常量,所以str5本身也是一个字符串常量的引用,它指向常量池中的"abc",从而第四个也输出true。 


以创建Pernson对象为例,分析对象创建过程中在内存中的分配情况以及方法的调用过程。 
class Person 

   private String name; 
   private int age; 
   private static String Country = "CN"; 


   DemoTest dTest = new DemoTest(); 


   public Person(String name, int age) 
  { 
       System.out.println("这是person的构造函数"); 
       this.name = name; 
       this.age = age; 
   } 


  { 
       System.out.println("这是person的构造代码块"); 
   } 


   static  
  { 
       System.out.println("这是person类的静态代码块"); 
   }    
   public void setName(String name) 
  {www.huiyi8.com 
       this.name = name; 
   } 


   public void show() 
  { 
      System.out.println("name = "+name + "::"+"age = "+age); 
   } 


   public static void showCountry() 
  { 
      System.out.println("Country = "+Country); 
   } 



class DemoTest 

     public DemoTest() 
    { 
         System.out.println("这是一个测试的类"); 
     } 



public class test 

     public static void main(String[] args) 
    { 
         Person p = new Person("zhangsan",11); 
         p.setName("lisi"); 
         p.show(); 
     } 



main函数执行后的结果为: 


这是person类的静态代码块 
这是一个测试的类 
这是person的构造代码块 
这是person的构造函数 
name = lisi::age = 11 


过程分析: 
首先要明确的一点是: 类的成员变量在不同对象中各不相同,都有自己的存储空间(存储在各自对象所占用的堆内存空间中)。 
而类的方法却是该类的所有对象共享的,它们存放在了方法区中,各方法的引用存储在了常量池中。方法区优先于对象存在而且对象只保存了成员变量和一些地址信息。 


首先栈中的main函数执行Person = new Person("zhangsan",11);这个简单的语句会涉及到如下几个步骤: 
1,由于是要创建Person类对象,java虚拟机(JVM)先去找Person.class文件,如果有的话,将其加载到内存。 


2,将类型信息(包括静态变量,方法等)加载进方法区。 


3,执行该类中static代码块,如果有的话,对Person.class类进行初始化。(此时输出‘这是person类的静态代码块’) 


4,到这时才进行堆内存空间的开辟,并为对象分配首地址。 


5,在堆内存中建立对象的成员属性,并对其进行初始化(先进行默认初始化再进行显示初始化。(此时输出  ‘这是一个测试的类’) 


6,进行构造代码块的初始化,由此看出构造代码库初始化的优先级要高于对象构造函数的初始化。 (此时输出 ‘这是person的构造代码块’) 

7,对象的构造函数进行初始化。(此时输出 ‘这是person的构造函数’); 

8,将堆内存中的地址付给栈内存中的p变量。 

接下来执行p.setName("lisi")过程如下: 
1,首先在栈中为对象p开辟一个栈空间供执行方法时使用,每个对象在执行自己的方法的时候得到自己的一份栈空间。然后根据引用p定位到堆中的对象,再根据对象持有的引用定位到方法区中的函数setName(),同时获得方法的字节码(引用)。 

2,在栈中压入一个局部数据作为方法的实参,通过方法字节码,程序计数器跳转到相应的方法中执行, 
并将调用此方法的对象的引用赋值给this,接着便将实参赋值给this所指向的对象在堆中相应的成员变量。 

3,方法调用结束,会通过执行类似汇编中的ret指令,自动释放局部数据所占用的空间,并重置程序计数器的值以执行main函数中调用方法语句后的下一条语句。 

注意: 每一个不同的对象(由相同的类创建而成)在执行同一个函数时,引用的是方法区中同一个字节码!! 

执行p.show()的过程与执行p.setName("lisi")的过程类似。(输出 ‘name = lisi::age = 11’)。 

以上是我的java虚拟机机制的一些理解,由于JVM的复杂性,完全理解它是很困难的,仅仅是写下这篇浅显的日志也花费了大量的时间去理解。 
0 0
原创粉丝点击