java与c++之间有一堵由动态分配和垃圾收集技术所围成的高墙,墙内的想出去,墙外的人却想进来。
重申下个人观点:我说的都是别人讲过的东西,老生常谈,然后会加一下自己的理解和看法,甚至有的时候有些东西也是自己的猜测。可能会有错的,欢迎指出,有异议的也欢迎交流。然后我之所以写出来一来是我觉得单独看一遍书其实理解的不深,自己打一遍,而且为了让文章言之有物我还要自己推敲琢磨,这样对我自己的理解本身是一种好处。另外就是如果读者看到了,觉得有帮助,那也算是一件好事。毕竟我是打在笔记本里自己看和打在简书里大家看,对我而言是差不多的!还有!我也是在学习阶段,我也是小白,刚刚也说了也会有错误。但是我不觉得一定要特别特别厉害才有资格写文章(最近有人问我整天写文章能挣钱咋的,所以针对这种看法我做一下解释。)
内存到底谁来管理好?
因为这本书都是对比JAVA和C++来讲解的,所以我也多少说两句。其实我不太有发言权,因为我目前为止只接触过java,不过各种资料看了不少,大概明白c++的机制是内存完全由程序员管理,也就是从创建到销毁都要维护。而java省事多了,在虚拟机自动内存管理机制的帮助下,一般都不需要去考虑内存。
但是!又到了举例子的时候了:打个比方,如果一个公司老板很不信任手下,啥事都要管,都要问。这样可能会弄得老板自己很累啊,员工也觉得麻烦啊,但是起码老板是很有权力和对情况的掌握的!如果有谁有点啥问题,作风不好啊,用公款大保健了啊,很容易就被发现了,然后老板惩罚或者换人,反正出不了什么大事。但是如果一个公司的老板很信任手下,啥事都让经理,副经理啥的处理。如果你眼光好,没啥问题,指不定因为员工知恩图报,你又清闲又盈利呢!但是假如你信任的是个白眼狼,好么,可能公司都赔没了但你却最后一个才知道。你不能单纯的说啥都管老板好或者不好,也不能说甩手老板好或者不好,各有利弊。同样你不能说C++的自己管理内存好不好,也不能断言java的自动内存管理机制好不好。
java运行时数据区域
java虚拟机会在执行java程序的时候,把它管理的内存划分成不同的数据区。这些区域各有用途,各有创建和销毁的时间。有的区域随着虚拟机进程创建,有的用户的行为出发的创建。(不夸张的说话这个图用了半个小时,最后还是用的微信图片编辑)
其实这个也没啥,应该学过java的或者有一丢丢了解的就能说出来,就是堆栈方法区。只不过书上把栈说的更细致一点,又本地方法栈还有虚拟机栈。另外有个程序计数器。
然后方法区和堆是线程共享的,我特意选个绿色背景。共享的颜色嘛。
程序计数器
程序计数器是一块比较小的内存空间,。它可以看作是当前线程所执行的字节码文件的行号指示器。在虚拟机的概念模型里(仅仅是概念模型,各种虚拟机实际上可能会用更高效的方式去实现),字节码解释工作时就是通过改变这个计数器的值来选取下一条指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖于这个程序计数器来完成的。
其实我个人感觉,这个应该是有点类似于一个指针的功能。不过书的原文里没这么讲。这个属于猜测。但是我们可以逐字的去读。字节码文件的行号指示器,改变这个计数器的值来选取下一条指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖于这个程序计数器来完成。我感觉,咱们用的编译器,我个人习惯用eclipse,他在代码的执行的时候,怎么跳转,执行到哪行代码,分支循环之类的,我不知道底层实现的原理,但是感觉应该就是有一个类似于程序计数器这样的东西吧。反正我是这么理解的,有不同意见的欢迎交流,我要是理解错了帮忙指出来。
然后java虚拟机的多线程是轮流切换并分配处理器执行时间的方式实现的。也就是任意时刻一个处理器(多核就是一个内核)只会处理一条线程中的指令。为了防止你执行一半时间到了,下次再回来不知道你执行到哪里了,所以要给你一个单独记录,这个就是程序计数器。所以程序计数器要线程之间个不影响,线程私有的内存。
这个更好理解了,感觉没啥解释的必要都。自己跑到哪了不记住,下次回来找不到要么少跑几行命令,要么重复跑几行命令。那还得了?
最后说明的一点:如果线程执行的是java方法,则这个计数器记录的是虚拟机字节码指令的地址,但如果执行的是本地方法,则计数器值为空(undefined)。
java虚拟机栈
与程序计数器一样,这个虚拟机栈也是线程私有的。它的生命周期与线程相同。咱们总听说的内存分堆栈,这个栈就是虚拟机栈,或者说虚拟机栈中的局部变量表部分。
其实虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧。用来存储局部变量表。操作数栈,动态链接,方法出口等(我其实也就知道第一个局部变量表)。每一个方法从调用到执行完成,就对应着一个栈帧从入栈到出栈的过程。
咱们继续说局部变量表吧。局部变量表存放的是编译器可知的各种基本数据类型,对象的引用,和returnAddress类型(这个我也第一次看到,不过看英文大概能理解是啥玩意,指向的是字节码指令的地址。结合上文我们程序计数器就是指针,指向字节码指令地址)。
书中这里还说到了占用空间问题,我也跟着说说。long和double占用两个局部变量空间,别的数据类型都是一个。局部变量表所需要的内存空间在编译器就能确定。当进入一个方法,需要在栈帧中分配多大局部变量空间是完全确定的。方法运行期间也不会改变局部变量表大小。
然后这有两个异常说一下:StackOverflowError(栈溢出)。这个官方的解释是线程请求的栈深度大于虚拟机所允许的深度。听着高大上云里雾里,但是我实际中不小心用了死循环就报这个错。我大概理解为jvm处理的时候栈里先执行这个方法,分一块地方。因为里面递归,还得执行这个方法,又分一块地方,一直分一直分栈里没地方了,然后就报这个错了。告诉你栈溢出了。
还有一个异常:OutOfMemoryError(内存不足错误)。这个打个比方,人家jvm一共1G内存,分给虚拟机栈500M内存(有的可以动态扩展,有的固定长度。不同虚拟机配置不一样)。但是别管咋地,你一下子跟虚拟机说你要10个G内存。那虚拟机自己卖了也不够给你的啊,所以就告诉你一声OutOfMemoryError,内存不足了,给不了你了。
本地方法栈
跟着书走,接下来说本地方法栈。与虚拟机栈差不多,区别就是虚拟机栈是为执行java方法提供服务的,本地方法栈是为了Native方法(本地方法)服务的。也会抛出那两个异常,就不多说了。
堆
对于大多数应用来说,java堆是虚拟机所管理的内存中的最大的一块。java堆是被所有线程所共享的区域。在虚拟机启动时创建。这个区域就是放对象实例的。几乎所有的对象都在这里分配内存。但是!!!重点是几乎所有,也就是不那么绝对的所有的对象都分配在堆上。
java堆是垃圾收集器管理的主要区域。因此也叫GC堆(下一篇文章就是讲垃圾回收的)。从内存回收的角度,因为垃圾收集是分代的,所以java堆也可以细分:
新生代和老年代。再细致一点Eden,Form Survivor,To Form Survivor等(如果你现在不懂看完下一篇垃圾收集的就懂了)。
然后堆内存不足的话,也会报OutOfMemoryError异常。
方法区
方法区和堆一样,也是线程共享的。它存储的是被虚拟机加载的类信息,常量,静态变量。也就是编译器编译后的代码等数据。
Sun/Oracle JDK的HotSpot VM中,直到JDK7都有“持久代”(Permanent Generation,简称PermGen)。也称为方法区。
Oracle JDK8的HotSpot VM去掉“持久代”,以“元数据区”(Metaspace)替代之。
据说java虚拟机规范把方法区描述为堆的一个逻辑部分。但是它还叫Non-heap(非堆)。所以说它不属于堆。也有人习惯把方法区叫做”永久代“。其实也不是,那有什么永久的。java虚拟机规范允许方法区的不进行垃圾回收。因为一般情况方法区里的很少需要垃圾收集,但也不是可以完全不收集的!
然后这个方法区无法满足内存分配,也会抛出OutOfMemoryError异常。
OutOfMemoryError异常
内存不足,这个除了程序计数器剩下的都可能会发生这个异常。然后这里简单的讲解一下出现的环境(书里就举了几个例子。我估计还有别的情况也会异常,自己研究吧)
堆内存溢出:
一个不断创建对象的死循环,然后直接跑的我笔记本要炸了,过了一会儿报错了。啧啧
public static void main(String[] args) {
List<Object> list = new ArrayList<Object>();
while(true) {
list.add(new Object());
}
}
栈内存溢出:
这里有个问题,栈是有两个异常的,而且有重叠部分、当栈空间不足无法分配,到底是内存小还是使用的栈空间大你不知道啊。所以说,反正书里作者多次尝试都是StackOverflowError这个异常。因为不断创建线程可能会导致操作系统假死,所以我这里就跑一下死递归得了。
public static void main(String[] args) {
new demo().stack();
}
public void stack() {
stack();
}
方法区常量池溢出:
我笔记本跑了两分钟多才出异常,我差点以为是没起来~~啧啧,太吓人了。
public static void main(String[] args) {
List<Object> list = new ArrayList<Object>();
System.out.print("跑了没?");
int i = 0;
while(true) {
list.add(String.valueOf(i++).intern());
}
}
反正大概这章就简单的介绍了下内存的区域划分,和各个区域的特性,作用啥的。然后我画的丑萌丑萌的图是重点。堆,方法区两个绿色的线程共享。剩下的私有的,线程隔离。然后这个程序计数器和后文的引用计数器别混了。就是相当于一个走到哪里了的书签或记录。
今天就到这吧,然后全文手打,如果帮到你了麻烦点个喜欢点个关注支持一下。最近今天在看jvm,如果有问题或者你也在学习可以一起交流下。欢迎评论私信。