Java内存分析一

来源:互联网 发布:php 手机回收网站源码 编辑:程序博客网 时间:2024/05/02 02:23
如果了解java内存的使用情况,对于程序的执行情况会更加清晰。关于java内存深度解析,请读者自行参考JVM有关书籍文档,会得到更多更完善的信息。下面通过一段简单的代码来分析。
首先简单介绍下JVM运行时内存数据区:

第一块:PC寄存器
PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。

第二块:JVM栈
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。保存局部变量的值,包括:1.用来保存基本数据类型的值;2.保存类的实例,即堆区对象的引用(指针)。也可以用来保存加载方法时的帧。

第三块:堆(Heap)
1.它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。
2.Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local AllocationBuffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效。的,但如果对象过大的话则仍然是直接使用堆空间分配。
3.TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。
4.用来存放动态产生的数据,比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。

第四块:方法区域(Method Area)
(1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。
(2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class。对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

第五块:运行时常量池(Runtime Constant Pool)
存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。

第六块:本地方法堆栈(Native Method Stacks)
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

下面通过一段简单的程序来分析:

点击(此处)折叠或打开

  1. public class Student {
  2.     
  3.     private int age;
  4.     private int height;
  5.     private String name;
  6.     
  7.     Student(){
  8.         
  9.         
  10.     }
  11.     
  12.     Student(int a,int h,String n){
  13.         
  14.         age = a;
  15.         height = h;
  16.         name = n;
  17.     }

  18.     public int getage(){
  19.         
  20.         return age;
  21.     }
  22.     
  23.     public int setage(int age1){
  24.         
  25.         age =0;
  26.         return age;
  27.     }
  28.     public int getheight(){
  29.         
  30.         return height;
  31.     }
  32.     
  33.     public String getname(){
  34.         
  35.         return name;
  36.     }
  37.     
  38.     public void setStudent1(Student h){
  39.         
  40.         h.setage(1);
  41.     }
  42.     
  43.     public void setStudent2(Student s){
  44.         
  45.         s = new Student(1,1,"www");
  46.     }
  47.     
  48.     public String toString(){
  49.         
  50.         return age + " " + height + " " + name;
  51.     }
  52.     public static void main(String[] args){
  53.         
  54.         Student one = new Student();    //1
  55.         int  s = 5;                                   //2
  56.         Student a = new Student(20, 177, "diy");   //3
  57.         Student b = new Student(30, 180, "os");    //4
  58.         System.out.println(one);        //p
  59.         one.setage(s);       //5
  60.         System.out.println(one.getage());   //m
  61.         one.setStudent1(a);                 //6
  62.         System.out.println(a);              //q
  63.         one.setStudent2(b);                 //7
  64.         System.out.println(b);              //r
  65.     
  66.     }
  67.     
  68. }

程序的执行从main函数开始:

1:在栈中存入了一类型为Student的局部变量one,其它的值我们未知,然后在堆中创建了一块对象内存区域,系统会调用构造函数来初始化对象成员变量,由于new Student(),无参,所以系统会调用无参数构造器来初始化成员变量:
上述无参构造函数可以写成:
Student(){
  
  age = 0;
  height = 0;
  name = null;

}
当执行p步骤时,系统会自动调用toString()方法,打印成员变量,得到的是:0 0 null
此时栈中的局部变量one指向了堆中被初始化的Student对象内存区域(其实one中存放的就是被初始化的Student对象在堆中的物理地址,作用和C/C++指针一样)

2.在栈中压入一局部变量s,并且赋值是5
由此可以发现,基础类型的变量就占用一块内存,引用类型的变量占用两块内存

3.在栈中申请一块内存存放类型为Student的局部变量a,并在堆中new出了一块Student对象内存,然后调用构造函数 Student(int a,int h,String n),在栈中依次分配三块内存区域存放局部变量a,h,n,这三个变量分别被赋值为20, 177”diy",在构造函数内部,该实例的成员变量分别被局部变量赋值:
        age = a;
     
 height = h;
      
name = n;
构造函数执行完毕后,局部变量的内存空间被依次收回n,h,a,其内存空间消失(注意栈中存储的特性,先入后出)
此时栈中Student 的引用a指向堆中的该Student 实例内存区域

4.分析同3

5.Student的引用one,调用函数setage(int age1),会在栈中分配局部变量age1的内存空间,并赋值为s的值为5,但是此时age被赋上了0,并返回了age的值,返回的值也会在栈中申请一块我们不知道的内存空间,用来放返回的age值,该函数调用完成,局部变量age1内存空间被收回,我们不知道名字的内存空间也被收回。

6.Student引用one调用函数setStudent1(),参数传入了3中的a 
setStudent1(Student h),该步在函数调用时,在栈中分配了一块内存用来存放类型为Student的局部变量h,h被赋值上a,此时a和h同时指向堆中的同一实例对象,h调用setage()函数来修改了age,此步与5有交叉,不做分析。该函数调用完成后,引用h的内存空间被收回,内存空间消失。但是此步骤的修改是永久性的。

7.Student的引用one调用函数setStudent2(),并传入参数4中的b
setStudent2(Student s),该步在函数调用时,在栈中分配了一块内存用来存放类型为Student的局部变量s,s被赋值上b,此时b和s同时指向堆中的同一实例对象,但是在函数内部,又在堆中new Student(1,1,"www"),new出了一块实例内存空间,此时把该实例所在堆中的地址赋值给s,所以s现在不再和b指向同一堆中实例内存空间,而是指向新的实例内存空间。当该函数调用完成,s的内存空间被收回,空间消失,而s之前指向的堆内存实例空间没有引用指向,JAVA的垃圾收集器会在一定的时机回收该堆内存空间。

关于p,q,r,系统会自动调用toString()函数,打印数据成员
而main()中,该函数执行完后,会根据入栈顺序相反的顺序而依次回收栈内存空间,在堆中new出的对象,也会在没有引用指向的同时,被Java垃圾收集器在一定的时机回收堆内存空间。

上述分析如有错误或不妥之处,请读者指正!

参考参考:点击打开链接

0 0
原创粉丝点击