这两个概念我觉得讲的很好的地方在百度词条:堆(Heap)栈(Stack)。
堆栈是一种数据结构。主要功能是暂时存放数据和地址。
-
堆(数据结构):堆可以被看成是一棵树,如:堆排序
-
栈(数据结构):一种先进后出的数据结构。
- 在Java中:栈(stack)与堆(Heap)都是Java用来在RAM中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置堆和栈。
-
栈
-
栈的优势是:存取速度比堆快,仅次于直接位于CPU中的寄存器。
- 缺点是:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
- 栈数据在多个线程或者多个栈之间是不可以共享的,但是在栈内部多个值相等的变量是可以指向一个地址的。
-
-
堆
- 堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,JAVA的垃圾收集器会自动收走这些不再使用的数据(C#亦如此)。
- 缺点:要在运行时动态分配内存,存取速度较慢。
Java和C#中的数据类型有两种。
第一种是基本类型:8种:int、short、long、byte、float、double、boolean、char(注意:并没有string的基本类型)。
int a=3;
long b=255L;
- 如上,称为自动变量。自动变量存的是字面值,不是类型的实例,即不是类的引用。这种字面值的数据。由于大小可知,生存期可知(定义在某个程序块中,程序块退出后,字段值消失),出于追求速度的原因,存在于栈中。
- 栈中的数据可以共享。
int a=3;
int b=3;
-
上述代码的过程:编译器先处理int a=3;首先它会在栈中创建一个变量为a的内存空间,然后查找有没有字段值为3的地址,没找到,就开辟一个存放数字3这个字面值的地址,然后将a指向3的地址。接着处理int b=3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
-
特别注意的是,这种字面值的引用与类型引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。
相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变。
就是说:上面代码执行完:再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,他就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果有了,则直接将a指向这个地址。因此a的值改变不会影响b的值。
第二种是包装类数据:【如Integer,String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于【堆】中】,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。
String是一个特殊的包装类数据。即可以用String str = new String("abc");的形式来创建,也可以用String str = "abc";的形式来创建
String str = "abc";
上述代码的执行步骤:
(1)先定义一个名为str的堆String类的对象引用变量:String str。
(2)在【栈】中查找有没有存放值为“abc”的地址,如果没有,则开辟一个存放字面值为“abc”的地址,接着创建一个新的String 类的对象o,并将o的字符串指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。
(3)将str指向对象o的地址。
一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!
using System;
// 值传递和引用传递
// 值类型在赋值(复制)的时候,传递的是这个值本省;
// 引用类型在赋值(复制)的时候,传递的是对这个对象的引用。
namespace _05面向对象_多态06值传递和引用传递
{
// 在内存中,有堆和栈之分;
// 值类型:int double char decimal bool enum struct
// 引用类型:string 数组 自定义类 集合 object 接口
// 区别:值类型的值:存储在栈中;引用类型的值,存储在堆中;
// 我们定义一个值类型变量时:比如:int n1 =10;
// 在栈中开辟一部分区域空间,存值10。这部分区域空间名叫n1;
// int n2 = n1;把n1的值赋值给n2;在赋值时,传递的是值本身;把10给n2。
// 在内存中重新开辟一部分空间,值10,名为n2;
// 当n2 = 20;时,此时n2为名的栈空间中原来的10值就没了改为了20;
// 没有影响到n1;
// 引用类型传递代码逐行分析
// Person p1 = new Person(); // new 以后,首先这个对象在堆中创建。
// 此时这个new Person()在堆中;
// 而此时p1在栈上,p1变量表示的区域在栈中存储的是:对象在堆中的地址:代表一种指向。
// Person p2 = p1;代表的是:将p1的引用地址值复制给p2。p1的实际值在栈中,这个值存储的是指向对象在堆中地址。
// 此时p1和p2在栈中存储的值相同,这个值均指向堆中的同一个位置,这个位置代表的是new Person()这个对象的堆中地址。
// 让p2在栈中的空间存储的值(地址)也指向p1栈空间存储的值(地址)指向的对象newPerson()的地址;
// 如果此时堆中的值变动了,那么p1和p2,甚至如果有更多指向同一堆中位置的其他引用变量,在重新读取这个位置值时都随之改变。
// 这也就是为何明明修改的是p2.Name,p1.Name的值却改变了的原因。因为引用类型指向了堆中同一块位置。
// 这就是说要有链接这个概念:软连接和硬链接,深copy和浅copy的原理一样。一个直接另外开辟空间复制一份,一个是只是建立一个链接指向。
// 更类似的应用场景:是在opencv中,如果直接在原图image上处理,会污染原图,最好是image.copy()一份,在复制图上操作。
// 查看方式:断点、调试、窗口、即时中,设置&p1,&p2,观察他们在堆和栈中的地址。
class Program
{
static void Main(string[] args)
{
// 值类型传递;
int n1 = 10;
int n2 = n1;
n2 = 20;
Console.WriteLine(n1); // 10
Console.WriteLine(n2); // 20
// 引用类型传递;
Person p1 = new Person();
p1.Name = "张三";
Person p2 = p1;
p2.Name = "李四";
Console.WriteLine(p1.Name);
Console.WriteLine(p2.Name);
// 方法调用的情况:
Person n = new Person();
n.Name = "王五";
Test(n);
Console.WriteLine(n.Name); // 赵六
static void Test(Person nn)
{
Person n = nn;
n.Name = "赵六";
}
// 所以说,引用类型的函数方法中,要注意,返回值不是早先的样子了。
// 唯一特例:string类型,字符串类型的不可变性。
string s1 = "第一值";
string s2 = s1; // 重新开辟一个空间。
s2 = "迪士尼";
Console.WriteLine(s1);
Console.WriteLine(s2);
// 陷阱1;
int number = 10;
TestTwo(number);
Console.WriteLine(number); // 输出为10;原因,函数没有返回值,
// 除非添加一个ref,reg的作用:是将一个变量以参数的形式传递给函数,然后再将值返回回来。
// 实现原理:把值传递变为引用传递;本来没有ref的话,是直接把10这个值传递过去。运算,现在是将变量的栈地址传递过去。运算会修改栈中的值。
// 本来不是同一块空间,加上ref之后,成了同一块空间。
static void TestTwo(int num)
{
num += 10;
}
Console.WriteLine("Hello World!");
}
}
// 自定义类,属于引用类型
public class Person
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
}
对堆和栈的观察实践:
实验一:
class BadGuy
{
public void BadMethod()
{
int x = 100;
this.BadMethod(); // 只递不归,不断调用,栈中不断新建
}
}
一会就会提示你:Stack overflow。栈爆掉了。
实验二:
class Program
{
static unsafe void Main(string[] args)
{
int* p = stackalloc int[9999999];
}
}
或者这样写法。
class Program
{
static void Main(string[] args)
{
unsafe
{
int* p = stackalloc int[9999999];
}
}
}
不过需要在:【项目P】中最下面一行:【引用类型 属性】中在【生成】勾选【允许不安全代码】。这样编译器不会报错。
stackalloc从栈上去切割内存。直接把栈爆掉。返回的是地址指针。
认识一下堆:选用占用内存比较多的window对象。
namespace WpfApp1
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
List<Window> winList;
private void Button1_Click(object sender, RoutedEventArgs e)
{
winList = new List<Window>();
for (int i = 0; i < 15000; i++)
{
Window w = new Window();
winList.Add(w);
}
}
private void Button2_Click(object sender, RoutedEventArgs e)
{
winList.Clear(); // gcc找一个合适的实际回收。
}
}
}
然后点击【生成】【生成解决方案】
在【bin】中【Debug】下找到生成的exe应用程序。WpfApp1.exe。双击运行。
程序没运行的时候是装在硬盘中的。一个程序从硬盘加载到内存中,就形成一个进程Process。或者说一个程序正在运行的实例。会有一个进程ID,叫PID。从静态变成了动态。
在windows系统中:按Win+R:输入:perfmon。打开:【性能监视器】。观察内存使用情况。
默认在监控整个内存的使用情况,点击×号关闭对整体的监控,点击旁边的+号,添加对单一程序运行的情况。
点击Process中的Private Bytes,勾选下面的【显示描述】解释这个选项的意义。然后从进程中找到我们的实例:WpfApp1。添加到计数器中。点击【确定】。
一开始检视表中:红线在最高处,重新设置max之后, 红线降下来,开始点击按钮观察。每次点击按钮都会去占用堆。红线上升。
每一次点击按钮,红线都会上升。
以后在程序中,观察我们的程序哪一步占用了大量的内存,可以通过此方法来实现跟踪观察。这是调试程序非常重要的技巧。
值变量放在栈中,引用变量放在堆中。类的实例对象也放再堆中。