`

J2SE自学(五)——Java内存管理

阅读更多
1、变量就是计算机中一块块内存空间

2、 Java内存管理原理(是三方关系——程序<-->JVM<-->操作系统) :
    1)、程序运行前:有些编程语言编写的程序会直接向操作系统请求内存,但 Java 并不支持那么做。这样做的一个重要优点是保证了程序的平台无关性。既然 Java 程序不负责创建内存空间,那由谁创建呢?是由 JVM 完成。当程序准备执行时,由JVM 预先向操作系统请求一定的内存空间,称为初始内存空间。程序执行过程中所需的内存都由Java 虚拟机从这片内存空间中划分。

    2)、程序运行中:Java 程序一直向 Java 虚拟机申请内存,当程序所需内存空间超出初始内存空间时,Java 虚拟机会再次向操作系统申请更多的内存供程序使用。

    3)、内存溢出:程序接着运行,当 Java 虚拟机已申请的内存达到了规定的最大内存空间,但程序还需要更多的内存,这时会出现内存溢出的错误。 


至此可以看出,Java 程序所使用的内存是由 Java 虚拟机进行管理、分配的。Java 虚拟
机规定了 Java 程序的初始内存空间和最大内存空间,开发者只需要关心 Java 虚拟机是如何
管理内存空间的,而不用关心某一种操作系统是如何管理内存的。

3、 内存空间逻辑划分:方法区,堆与栈 ——JVM 会把申请的内存从逻辑上划分为三个区域,即:方法区、堆与栈。

//这个class整体就是一个方法区!

public class Student {

	public String name ;
	
	public int age ;
	 
	public void study() {
		System.out.println("我在学习java!");
	}
}



//这个class中的stu和i都是变量,而 new Student()是对象
public class Demo {

	public static void main(String[] args) {

		Student stu = new Student() ;
		stu.name = "张三" ;
		stu.age = 18 ;
		int i = 10 ;
	}

}



Java虚拟机对应的在内存中划分了三个区域“方法区”、“堆区”和“栈区”,分别保存类结构、对象中的数据和变量(基本型和引用型)。 这三个内存区域都有大小限制,任何一个区域内存溢出都会导致程序出现错误,栈内存溢出会发生StackOverflowException错误,堆内存溢出会发生OutOfMemoryError错误。

1)、方法区中的内存分配:方法区默认最大容量为64M,Java虚拟机会将加载的java类存入
方法区,保存类的结构(属性与方法),类静态成员等内容。编写中小型程序时,一般不会
造成方法区的内存溢出。

2)、堆中的内存分配:堆默认最大容量为64M,堆存放对象持有的数据,同时保持对原类的
引用。可以简单的理解为对象属性的值保存在堆中,对象调用的方法保存在方法区。

3)、栈中的内存分配:栈默认最大容量为1M,在程序运行时,每当遇到方法调用时,Java虚拟机就会在栈中划分一块内存称为栈帧(Stack frame),栈帧中的内存供局部变量(包括基本类型与引用类型)使用,当方法调用结束后,Java虚拟机会收回此栈帧占用的内存。
  另:还有数组内部也封装了指针,即便是基本数据类型的数组,也封装了指针,数组也是引用类型。


4、JAVA 数据类型
   C 语言可以声明指针类型的变量。那就会问,java 中有没有指针呢?您一定要明确回答:“JAVA 中没有指针”。问题并没有到此终结,您要紧接着说“但 JAVA 底层封装了指针”。对,Java 底层封装了指针,但我们要说java 中没有指针,也就是不能创建指针类型的变量。
   但也并不是所有的类型都封装了指针根据这点不同,java 的数据类型分为两类,即:值类型(基本数据类型)和引用类型。

   1)、分类原则:根据是否封装指针(分配空间形式)
   2)、基本数据类型:值类型(又称基本数据类型和基元数据类型)——只在栈中分配一块内存(四类八种,详见J2SE自学(三)——Java语法基础(一))  
   3)、引用数据类型(就是底层封装指针的数据类型。):在栈和堆中各各分配一块内存,例如:类、数组、接口等——他们在内存中分配两块空间,第一块内存分配在栈中,只存放别的内存地址,不存放具体数值,我们也把它叫指针类型的变量,第二块内存分配在堆中,存放的是具体数值,如对象属性值等。


5、声明对象与创建对象
    声明对象,相当分配指针类型变量,在栈中分配内存
    类是引用类型,那声明对象就相当于在栈中声明指针类型的变量,它的内存不存放具体的数值,而只存放另一块堆中内存的地址。

    创建对象,创建具体内存空间,在堆中分配内存
    在java 中一般使用new 关键字创建对象(当然还有更高级的反射可以不用new 关键字)例如:
    zhouxingxing = new Student()就是创建对象。这行代码一共做了两件事情:第一件是在堆中分配一块存放学生具体数值的内存(例如:35DF),第二件是把这个内存的首地址赋给上面声明的指针变量。

    对象必须创建后才能使用,如果只声明不创建,那么调用对象属性和方法时将会报空指针异常(NullPointerException)
    只声明对象,只相当于在栈中分配一块不存放具体数值的内存;没有创建,在堆中没创建存放具体数值的内存,当我们调用其属性方法时,系统找不到具体堆内存,则报空指针异常。同时我们也可以反过来说,空指针异常是因为没有指向堆中内存引起的。


6、JAVA 值传参与引用传参——定义:方法根据调用后的效果不同,即是否改变参数的原始数值
   按值传递参数数值不变,按引用传递参数数值改变。
   对于java的数据类型而言,基本数据类型就是按值传递的,又称为值类型,而引用类型是按引用传递的。如:
   public class TestValueAndRef { 
  /** 
   * 方法参数有三个 
   * @param pa 基本类型  值类型 
   * @param pstu 自定义类 引用类型  
   * @param parr 数组 引用类型 
   */ 
  void change(int pa, Student pstu, int[] parr) { 
     
    //方法体中改变值类型pa的值 
    pa = pa + 10; 
    //方法体中改变引用类型stu,parr的值
    pstu.stuAge = pstu.stuAge + 10; 
    parr[0] = parr[0] + 10; 
     
    System.out.println("方法体改变后pa = " + pa); 
    System.out.println("方法体改变后pstu.stuAge = " + pstu.stuAge); 
    System.out.println("方法体改变后parr[0] = " + parr[0]); 
  } 
 
} 


运行类:
public class Test { 
  public static void main(String[] args) { 
     
    Student student = new Student(); 
    student.stuAge = 10; 
    int a = 10; 
    int arr[] = new int[]{9, 5, 27}; 
     
    System.out.println("初始值 a = " + a); 
    System.out.println("初始值 student.stuAge = " +  
student.stuAge); 
    System.out.println("初始值 arr[0] = " + arr[0]); 
     
    TestValueAndRef testValueAndRef = new TestValueAndRef(); 
    testValueAndRef.change(a, student, arr); 
     
    System.out.println("调用函数后 a = " + a); 
    System.out.println("调用函数后 student.stuAge = " +  
student.stuAge); 
    System.out.println("调用函数后 arr[0] = " + arr[0]); 
 
  } 
} 


运行结果:
初始值 a = 10
初始值 student.stuAge = 10
初始值 arr[0] = 9
方法体改变后pa = 20
方法体改变后pstu.stuAge = 20
方法体改变后parr[0] = 19
调用函数后 a = 10
调用函数后 student.stuAge = 20
调用函数后 arr[0] = 19


我们看到,基本数据类型int 变量a 虽然在方法体中改变了数值,但方法调用完后其原始数值并没有改变。而引用数据类型Student 在方法体中改变年龄的数值,方法执行完其数值发生了改变,数组也是引用类型,所以其值也发生了改变。也就是说:值类型按值传递参数,方法调用结束后,原始数值不变,引用类型按引用传递参数方法调用结束后,原始数值改变。

不管是按值传递还是按引用传递,都是把栈中的数据备份了一份给参数变量,只不过值类型备份的是具体的数值,而引用类型备份的是内存地址,所以从这个角度看,我们也可以说Java 全部是按值传参的,因为它都是把数据备份了一份。

7、String类
   特例String 类型,虽然它也是引用类型,但用引用传参后,原始数值却不会改变

   public class TestString { 
   public static void main(String[] args) { 
     
    String name = "zhangsan"; 
    TestString testString = new TestString(); 
     
    System.out.println("方法调用前:" + name); 
    testString.change(name); 
    System.out.println("方法调用后:" + name); 
  } 
   
  void change(String str) { 
    str = "张三"; 
    System.out.println("方法体内修改值后:" + str); 
  } 
} 

运行结果:
方法调用前:zhangsan
方法体内修改值后:张三
方法调用后:zhangsan

虽然参数String 是引用数据类型,但其值没有发生改变,这是因为String 类是final 的,它是定长。方法体内改变的仅仅是改变了str的引用!对方法体外的name并没有任何影响!

以后遇到类似而的问题要“多思考,多画图,多实践!”
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics