面向对象的内存分析
这里首先说明一下,天下文章一大抄,你只看到了作业相互抄,没有看到编程语言之间互相抄。
所以只要明白了一个语言的底层原理,其他语言的底层也都差不多可以理解了
Java虚拟机的内存可以分为三个区域:栈(stack),堆(heap),方法区(method area)
方法区其实也在堆里面,或者说方法区是一个特殊的堆。所以本质上只有栈和堆这两个东西,只不过方法区的作用有些特殊,所以我们把他单拿出来说一下
栈
栈的特点:
栈描述的是方法执行的内存模型。每个方法被调用的时候都会创建一个栈帧(存储局部变量、操作数、方法出口等)
JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
栈属于线程私有,不能实现线程间共享
栈的存储特性是“先进后出,后进先出”
栈是由系统自动分配,速度快!栈是一个连续的内存空间
线程是一个新的名词,后续我们会知道什么是线程。我们启动一个程序的时候会包含很多的线程
注意:方法的执行的相关调用都在栈里面。每调用一次方法都会创建一个栈帧在栈里面(方法里面的局部变量、操作数、方法出口等都会存放在栈帧里面,这里我们只需要记住方法中的局部变量都在栈帧里面就好)。假设我们现在的这个方法为A,它调用了一个新的方法B(A方法没有结束),那么就会在栈里面创建一个B方法被调用时(单次被调用)的栈帧,然后当B方法执行完,B方法的栈帧就会关闭,然后才是A方法(后进先出)。而当这个线程都执行完后,整个栈空间也都会关闭。
栈是连续的空间,并没有像堆一样胡乱的摆放
堆
堆的特点:
堆用于存储创建好的对象和数组(数组也是对象)
JVM只有一个堆,被所有线程共享
堆是一个不连续的内存空间,分配灵活,速度慢!
当我们通过new关键字调用构造方法创建对象时,就意味着在堆里面有这样一个对象了
方法区
方法区(又叫静态区)特点:
JVM只有一个方法区,被所有线程共享!
方法区实际也是堆,只是用于存储类、常量相关的信息!
用来存放程序中永远是不变或唯一的内容。(类信息【Class对象】、静态变量、字符串常量等)
程序执行的内存变化过程
public class HelloWorld {
int id;
String name;
void helloMethod(){
System.out.println("HelloWorld");
}
public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
helloWorld.id = 1;
helloWorld.name = "HelloWorld";
Author author = new Author();
author.username = "mahe666";
helloWorld.helloMethod();
}
}
class Author{
String username;
}
首先需要明白以下几点:
栈空间(stack)
,连续的存储空间,遵循后进先出的原则,用于存放局部变量。
堆空间(heap)
,不连续的空间,用于存放new出的对象,或者说是类的实例。
方法区(method)
,方法区在堆空间内,用于存放:
类的代码信息;
静态变量和方法;
常量池(字符串常量等,具有共享机制)。
Java中除了基本数据类型,其他的均是引用类型,包括类、数组等等。
变量初始化:
成员变量可不初始化,系统会自动初始化;
局部变量必须由程序员显式初始化,系统不会自动初始化。
简单通俗的讲,一个完整的Java程序运行过程会涉及以下内存区域:
程序计数器(Program Counter Register)
,让虚拟机中的字节码解释器通过改变计数器的值来获取下一条代码指令,比如分支、循环、跳转、异常处理、线程恢复等;Java 虚拟机栈(Java Virtual Machine Stacks)
,栈顶存放当前方法,里面有局部变量表,本地方法栈(Native Method Stacks)
,则是为虚拟机使用到的Native 方法服务,作用同虚拟机栈。Java 堆(Java Heap)
,是Java 虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。方法区(Method Area)
与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
具体过程
首先Java虚拟机(JVM)会去方法区中寻找是否有当前文件中的public类的代码信息,如果存在,就直接调用。如果没有,则通过类加载器(ClassLoader)把.class
字节码加载到内存中
这个时候我们的方法区里面就会有我们的这个类的信息了,这个信息包括但不限于静态变量、字符串常量和静态方法等。(其中的字符串常量就包括了System.out.println("HelloWorld");
语句里面的HelloWorld
等常量)
我们知道,程序的入口是main()
,因而从main方法从上到下、从左到右进行分析。
执行完后我们这里就需要去找main()方法,这里需要注意一点:所有被static关键字修饰的方法都算是静态方法,在这之后会说static关键字的作用
所以这里当我们需要调用main方法的时候,我们就会在栈里面创建一个关于main方法的栈帧,然后他会初始化该方法里面的局部变量,存放在该方法的栈帧里面 HelloWorld helloWorld = null;
注意:这里还没有调用构造方法,只是初始化局部变量!
初始化后才会通过new关键字调用构造方法,以方法区的类信息为模板创建实例,而当我们调用构造方法的时候,在栈的里面也会创建一个新栈帧。
而当构造方法执行完,栈帧的空间关闭,且堆的里面会创建一个对象出来,这个对象就是我们通过new关键字创建的对象,这对象里面有属性有方法
这里需要注意的是属性在这里并没有被赋值,所以属性的值都是默认值
我们创建好对象后,开始执行赋值运算符
注意!我们赋给变量的值并不是我们的对象本身,而是对象在堆中的内存地址,对象通过四字节的地址(十六进制),引用该实例
如果我们没有重写toString
方法,我们可以去输出一下这个对象,我们就会发现类的后面跟了一个@xxxxx,这个@符号后面的字符串就是这个对象的内存地址
我们在把内存地址赋给变量之后,我们就会发现栈、堆和方法区就都关联起来了
然后我们来看给对象属性赋值,假设说赋值的时候给属性赋了一个字符串常量,那么是不会在堆里面把字符串常量的值赋给该属性的,而是把该字符串常量在方法区中的内存地址,赋给该属性。或者说该属性去引用方法区中的字符串常量
而如果是基本数据类型的属性的话,在赋值的时候就是直接在堆里面去操作了
而当我们调用实例的方法时,并不会在实例对象中生成一个新的方法,而是通过地址指向方法区中类信息的方法。
但是问题又来了,如果这个对象有一个属性是一个我们自己写的类的类型呢?
如果碰到了其他类型,例如属性的数据类型是引用类型,就会像加载当前类一样进行加载,有则调用,没有则加载到方法区
首先我们先初始化一个该类型的变量,然后该类型的变量的默认值为null。然后我们需要调用该类的构造方法。然后我们又要在栈里面开辟一个属于该构造方法的栈帧。
在这之后我们就熟悉了,栈帧空间关闭,然后在堆的里面创建一个对象,然后该对象的属性值进行初始化(该类的实例化对象也有一个内存地址),再然后将该对象的内存空间赋给之前提到的属性。然后该属性的对象如果引用了常量就还是会引用方法区内的字符串常量的内存地址
而当我们把新创建的实例赋值给另一个实例的属性的时候,也可以理解为另一个实例的属性指向了新创建的实例
而当整个程序都执行完之后,main方法的栈帧就会关闭,然后虚拟机就会停掉,然后很显然。所有的虚拟机中的内存也都会停掉
相关博客:
对象存储到什么地方
程序运行时,对象是怎么进行放置安排的呢?特别是内存是怎样分配的呢?对这些方面的了解会对你有很大的帮助。有五个不同的地方可以存储数据:
寄存器
。这是最快的存储区,因为它位于不同于其他存储区的地方一一处理器内部。但是寄存器的数量极其有限,所以寄存器根据需求进行分配。你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象(另一方面,C和C++允许您向编译器建议寄存器的分配方式)堆栈
。位于通用RAM(随机访问存储器)中,但通过堆栈可以从处理器那里获得直接支持。堆栈指针若向下移动,则分配新的内存,若向上移动,则释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时,Java系统必须知道存储在堆栈内所有项的确切生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些Java数据存储于堆栈中——特别是对象引用,但是Java对象并不存储于其中。堆
。一种通用的内存池(也位于RAM区),用于存放所有的Java对象。堆不同于堆栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。当需要一个对象时,只需用new写一行简单的代码,当执行这行代码时,会自动在堆里进行存储分配。当然,为这种灵活性必须要付出相应的代价:用堆进行存储分配和清理可能比用堆栈进行存储分配需要更多的时间(如果确实可以在Java中像在C++中一样在栈中创建对象)。常量存储
。常量值通常直接存放在程序代码内部,这样做是安全的,因为它们永远不会被改变。有时,在嵌入式系统中,常量本身会和其他部分隔离开,所以在这种情况下,可以选择将其存放在ROM(只读存储器)中。非RAM存储
。如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。其中两个基本的例子是流对象和持久化对象。在流对象中,对象转化成字节流,通常被发送给另一台机器。在“持久化对象”中,对象被存放于磁盘上,因此,即使程序终止,它们仍可以保持自己的状态。这种存储方式的技巧在于:把对象转化成可以存放在其他媒介上的事物,在需要时,可恢复成常规的、基于RAM的对象。Java提供了对轻量级持久化的支持,而诸如JDBC和Hibernate这样的机制提供了更加复杂的对在数据库中存储和读取对象信息的支持。