呼呼,突然有种感觉,研究生只读三年真是太短了,或者应该说,本科生阶段虚度了太多大好的时间,于我心有戚戚焉 ~~~
以前自学的写码的时候比较注重结果,以为只要把程序写出来,结果搞出来就万事大吉了。最近重新去看java的一些经典书籍,才发现之前的学习过程根本就是囫囵吞枣,忽视了实现过程中很多的细节。去copy网上现成的代码,没有自己调试的过程,就很难去理解一个程序的内涵。在此多言,以为警醒。
接下来的学习中会着重于JVM中的对象极其内存的管理机制。
首先,上图。
然后,开刷。
方法区
在Java虚拟机中,关于被装载类型的信息是存储在一个逻辑上被称为“方法区”的内存中的。需要指出的是,方法区的大小不必是固定的,虚拟机可以根据应用的需要进行动态的调整。同样,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。
当虚拟机装载某一个类型时,它首先使用类装载器定位相应的class文件,然后读入这个class文件—一个线性二进制的数据流—然后将它传输到虚拟机中。紧接着虚拟机会提取其中的类型信息,并将这些信息存储在方法区中。该类型中的类(静态)变量同样也是存储在方法区中。
方法区
一般来说,虚拟机会在方法区中存储以下类型信息:
1. 这个类型的全限定名(最原始的地方援引到具体的对象的过程,类似于“绝对路径”);
2. 这个类型的直接超类的全限定名(除非这个类型是java.lang.Object,它是所有类的祖宗,没有超类);
3. 个类型是类类型还是接口类型;
4. 这个类型的访问修饰符(public, abstract, final的某个子集);
5. 任何直接超接口的全限定名的有序列表;
6. 该类型的常量池(一个有序集合,包括直接常量[string, integer和floating point常量]和对其它类型、字段和方法的符号引用);
7. 字段信息(字段名、类型、修饰符);
8. 方法信息(方法名、返回类型、参数数量和类型、修饰符);
9. 除了常量以外的所有类(静态)变量;
10. 指向ClassLoader类的引用(每个类型被装载时,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的);
11. 指向Class类的引用(对于每一个被装载的类型,虚拟机相应地为它创建一个java.lang.Class类的实例。比如你有一个到java.lang.Integer类的对象的引用,那么只需要调用Integer对象引用的getClass()方法,就可以得到表示java.lang.Integer类的Class对象)
堆
Java程序在运行时创建的所有类实例或数组(数组在Java虚拟机中是一个真正的对象)都放在同一个堆中。
由于Java虚拟机实例只有一个堆空间,所以所有线程都将共享这个堆。又由于一个java程序独占一个java虚拟机实例,因而每个java程序都有自己的堆空间--虽然它们可能在同一块内存上,但确感受不到彼此的存在!WHAT A SAD STORY!就好像生活在不同次元空间的生物。。。。咳咳咳,开个脑洞,回来—但是同一个java程序中的多个线程却共享着一个堆空间,在这种情况下,就得考虑多线程访问对象(堆数据)的同步问题了—先给自己挖个,以后再来填多线程的坑。
需要注意的是,Java虚拟机有一条在堆中分配对象的指令,却没有释放内存的指令,因为虚拟机把这个任务交给垃圾收集器处理。Java虚拟机规范并没有强制规定垃圾收集器,它只要求虚拟机实现必须“以某种方式”管理自己的堆空间。比如某个实现可能只有固定大小的堆空间,当空间填满,它就简单抛出OutOfMemory异常,根本不考虑回收垃圾对象的问题,但却是符合规范的。
对象的内部表示。Java虚拟机规范并没有规定Java对象在堆中是如何表示的。对象内部的表示也影响着整个堆以及垃圾收集器的设计,它由虚拟机的实现者(程序世界的“神”)来决定—从这个意义上来说,所谓的“上帝”,不就是这个世界的程序设计者么23333~~~~
一个可能的设计方法如下:划分对象池和方法池
这种设计方案把堆分成了两个部分:句柄池和对象池。一个引用对象是一个指向句柄池的本地指针。
句柄池的每个条目又有两个部分:一个指向对象实例变量的指针,一个指向方法区类型变量的指针。这种设计的好处是有利于碎片的整理,当移动对象池中的对象时,句柄部分只需要修改指针指向对象的地址就可以了—就是句柄池中的那个指针。缺点是每次访问对象的实例变量时都要经过两次指针传递。
再来一个(上面这个图费好大力气才画好,受不了了,第二个就偷懒不画了),保持数据对象在一起。另一种设计方式是使对象指针直接指向一组数据,而该数据包括对象实例数据以及指向方法区中类数据的指针。直观的理解就是,将上图的“实例数据”和“指向对象池的指针”合并在一起,即不需要“指向对象池的指针”,直接存储“实例数据”。这样设计的优缺点正好和上述设计的相反。
栈(一口老血,原来这个念“zhan”,我一直以为是“zhai”!)
每当启动一个新线程时,Java虚拟机都会为其分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会对Java栈进行两种操作:以帧为单位的压栈或出栈。
当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中,当该方法返回时,这个栈帧就从Java栈中弹出。Java栈存储线程中Java方法调用的状态--包括局部变量、参数、返回值以及运算的中间结果等。Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑,同时也便于Java虚拟机在只有很少通用寄存器的平台上实现。另外,基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。关于栈还需要进一步学习,这里就不继续累述了,因为我也不大懂。
PC寄存器
对于一个运行中的Java程序而言,每一个线程都有它的程序计数器,也叫PC寄存器。
对于一个运行中的Java程序而言,每一个线程都有它自己的程序计数器。
程序计数器既能持有一个本地指针,也能持有一个returnAddress。当线程执行某个Java方法时,程序计数器的值总是下一条被执行指令的地址。这里的地址可以是一个本地指针,也可以是方法字节码中相对该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时程序计数器的值是“undefined”。
本地方法栈
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的栈,虚拟机只是简单地动态连接并直接调用指定的本地方法。
其中方法区和堆由该虚拟机实例中所有线程共享。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后把这些类型信息放到方法区。当程序运行时,虚拟机会把所有该程序在运行时创建的对象放到堆中。
像其它运行时内存区一样,本地方法栈占用的内存区可以根据需要动态扩展或收缩。
最后我来传个教,有了这个架子,边看书边写码成为一种享受好吧!