这篇文章(包括后面两篇)是在详细读完《深入理解Java虚拟机》这本书并且阅读了大量网络博文之后总结而成的。
包含3个部分:
(1)理解Java虚拟机的组成部分
(2)Java虚拟机的垃圾收集算法
(3)分代收集器的详细机制
整个文章是按照虚拟机的不断发展而逐步展开的。先说明虚拟机内存组成,说明各个部分内存的管理方式,第二部分则是管理方法的不断演变,而第三部分则是现在比较成熟的管理方式。
在学习Java内存分配原理的时候一定要牢记这一切都是在JVM中进行的,JVM是内存分配原理的基础与前提。
在学习Java内存分配原理的时候一定要牢记这一切都是在JVM中进行的,JVM是内存分配原理的基础与前提。
JAVA内存管理的全景图。
按照数据用途的不同,JAVA分为Stack和heap,Stack中为固定字长,heap中为不定长度,存储的是各种Object。JAVA会争对这些Object做自动的垃圾回收。垃圾回收的原理是确定仍活着的(有用的)Object,然后清除剩下的,确定仍然活着的Object方法,最早采用的方法是引用计数法,但是这种方法无法解决无用Object之间的循环引用的问题。所以现在的采用的都是跟踪收集器,跟踪收集器从一些gc root对象(主要为虚拟机栈中的内容)开始,依次向下遍历记录所有的可达的Object,其他不可达到的区域就全部是可以回收的部分。清除算法也演进了很多代,最早的是标记-清除算法,但是它会导致很多的内存碎片。解决内存碎片于是出现了复制算法,为了减少复制算法的内存浪费,一次出现了复制收集器(2个区域),增量收集器(多个区域),分代收集器。
1》理解Java虚拟机中的Stack,Heap和Method Area
在《深入理解java虚拟机》一书中,对java的内存管理机制有非常深入的理解。需要说明的是该书所介绍的是《Java虚拟机规范》中所定义的虚拟机机制。实际上各个厂家(sun,IBM)在实际实现时会根据需求和具体情况自己定义内部情况。
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和消失。
生命周期同用户线程的数据区:
程序计数器:每条线程都需要有一个独立的程序计数器(线程私有的内存)。
Java虚拟机栈:用于存储局部变量表(存储基本数据类型和引用),操作数栈,动态链接,方法出口等信息
一直存在的数据区:
Java堆:被所有线程共享,在虚拟机启动时创建,内存数据区中最大的一块,其唯一的目的就是存放对象实例(包括数组)。Java堆可以处于物理上不连续的内存空间中,只要在逻辑上是连续的即可。当前主流的虚拟机都是按照可扩展来实现的(通过-Xms和-Xmx控制)。如果队中没有内存完成实例分配,且对也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区:各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器变异后的代码等数据。
运行时常量池是方法区的一部分
Stack(栈)主要存储基本类型变量(int count)和对象引用。生命周期与线程相同。每个方法被被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。Stack管理很简单,pop,push一定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop一定字节长度数据或者指令,Stack指针弹栈。Stack的速度很快,管理很简单,并且每次操作的数据或者指令字节长度是已知的和确定的。
Heap(堆)被所有线程共享,在虚拟机启动时创建。主要用于存储Object(对象实例)中的属性(属性的类型和属性值)。在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。而对象实例(例如数组,StudentBean)在Heap 中分配好以后,需要在Stack中保存一个4字节(32位系统)的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。(堆上还需要有对象类型数据在方法区中的地址,某个类的方法信息不应该每个对象实例都复制一份,因为对于不同对象来说,它们都是相同的)
Method Area(方法区)被所有线程共享,在虚拟机启动时创建。主要用于存储Ojbect中的对象类型数据(如对象类型(类名),访问修饰符,常量池,父类,实现的接口,方法等)。方法区在虚拟机启动时创建。尽管方法区在逻辑上时heap的一部分,简单的实现仍然可以选择对它既不回收也不压缩。
Stack的内存管理是随着线程的生命周期自动管理的;
Heap 则是随机分配内存,不定长度,存在内存分配和回收的问题
Method Area则主要回收两部分内容:废弃常量和无用的类。一般回收效率很低,只有在大量使用反射,CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景才需要虚拟机具备类卸载的功能,以保证方法区(永久代)不会溢出。
运行时数据区域(如图)
非静态方法和静态方法的区别:
非静态方法有一个和静态方法很重大的不同:非静态方法有一个隐含的传入参数,该参数是JVM给它的,和我们怎么写代码无关,这个隐含的参数就是对象实例在Stack中的地址指针。因此非静态方法(在Stack中的指令代码)总是可以找到自己的专用数据(在Heap 中的对象属性值)。当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得Stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法。
静态方法无此隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法即可被调用。当然此时静态方法是存取不到Heap 中的对象属性的。
总结一下该过程:当一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap 区没有数据。然后程序技术器开始执行指令,如果是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问Heap 数据区的;如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行前,要先new对象,在Heap 中分配数据,并把Stack中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时能够访问到Heap 数据区了。
静态属性和动态属性:
在JVM中,静态属性保存在Stack内存区,动态属性保存在Heap内存区。
链接知识:
(1)参考字符串对象的优化:
Java虚拟机中Stack的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double, boolean, char)和对象句柄。
栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:
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;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量 (这个特性在需要处理大量String时很有用)