Java的内存分配和管理
Java的内存分配和管理
Java内存分配时涉及的区域:
寄存器:在程序中无法控制;
栈:存放基本类型的数据和对象的引用,但是对象本身不存放在栈中,而是存放在堆中;
堆:存放用new产生的数据;
静态域:存放在对象中用static定义的静态成员;
常量池:存放常量。
内存分配中的栈和堆
1.栈
在函数中定义的一些基本类型的变量数据,还有对象的引用变量都在函数的栈内存中分配。当在一段代码中定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,java会自动释放掉为该变量分配的内存空间。
栈内存,是java程序的运行区,是在线程创建时创建的。它的生命周期跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程结束,该栈就结束了。
栈中的数据是以栈帧(stackframe)的格式存在的。栈帧是一个内存区块,是一个数据集,是有关方法(method)和运行期数据的数据集。当一个方法A被调用时就会产生一个栈帧F1被压入到栈中,A方法再调用B方法,就会产生栈帧F2被压入栈,执行完毕后,先弹出F2,在弹出F1。
2.堆
堆内存用来存放由关键字new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾收集器来管理。
在堆中创建一个对象后,还可以在栈中定义一个变量,让这个变量的值等于对象在堆内存中的首地址,栈中的变量就是对象的引用,相当于java中的指针。当程序运行到对象所在的语句块之外,对象占据的内存不会自动释放,在没有引用变量指向它时,随后一个不确定的时间被垃圾收集器回收掉。
3.常量池(constantpool)
常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(int,long等)和对象型(string、数组等)的常量值(final),还包含一些以文本形式出现的符号引用。
类和接口的全限定名;
字段的名称和描述符;
方法和名称的描述符;
在程序执行时,常量池会存储在MethodArea(方法区)中,而不是堆中。
一个java虚拟机实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类、方法、常变量(const修饰的变量)放到堆内存中,堆内存分为三部分:
1)PermanentSpace永久存储区
永久存储区是一个常驻内存区域,用于存放jdk自身所携带的ClassInterface的元数据。也就是说它存储的是运行环境必需的类信息,被装载到此区域的数据不会被垃圾收集器回收,关闭java虚拟机才会释放此区域占的内存。
2)YoungGeneration Space新生区
新生区是类的诞生、成长、消亡的区域,新生区又分两部分:伊甸区(Edenspace)和幸存区(Survivorspace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(survivor0 space)和1区(survivor1space)。当伊甸区的空间用完时,程序再创建对象,虚拟机将对伊甸区进行垃圾回收,将伊甸区中的不再被引用的对象进行销毁,然后将伊甸区中的剩余对象移动到幸存0区,如果幸存0区也满了,将对该区进行垃圾回收,然后移动到1区,如果1区也满了,就会移动到老年区。
3)Tenuregeneration space老年区
老年区保存从新生区帅选出来的java对象。
Java虚拟机中为什么分堆区,栈区?
1)从软件的角度,栈区代表了处理逻辑,而堆代表了数据。分开,使得处理逻辑更为清晰,体现了模块化的思想。
2)虚拟机堆、栈的分离,使得堆中的内容可以被多个虚拟机栈共享(也可以理解为多个线程访问同一个对象,因为虚拟机栈是随着线程的创建而创建的),这种共享的益处很多,一方面提供了一种有效的数据交互方式(如共享内存);另一个方面,堆中的共享常量和缓存可被多有虚拟机栈访问,节省了空间。
4,堆和栈的合作
堆是一个运行时数据区,类的对象从中分配空间,堆的优势是可以动态地分配内存大小,生存期不必事先告诉编译器,但是缺点是,由于在运行时动态分配内存,存取速度慢。
栈的优势是存取速度比堆快,仅次于寄存器,缺点是栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中存放一些基本类型的变量数据和对象引用。
栈有一个重要特性,就是栈中的数据可以共享。
Int a =3;
int b =3;
编译器先处理inta =3;首先会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没有,就将3存放进来,然后将a指向3。接着处理intb =3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,a与b均指向3。如果再有语句a=4;编译器会重新搜索栈中是否有4值,如果没有,将4存放进来,并将a指向4,如果已经有了,直接将a指向这个地址。因此a的改变不会影响到b的值。
这种数据的共享与两个对象的引用同时指向一个对象的共享是不同的,因为这种情况下a的修改不会影响到b,它是由编译器完成的,有利于节省空间。而一个对象的引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。
Java中,String是一个特殊的包装类数据,有两种创建方式:
String str = newString(“abc”);
String str = “abc”;
第一种用new创建对象,是存在堆中,没调用一次会创建一个新的对象。
第二种是先在栈中创建一个对String类的对象引用变量str,然后通过符号引用去字符串常量池里查找有没有“abc”,如果没有,将“abc”放进字符串常量池,并让str指向“abc”,如果有,直接让str指向“abc”。
5, 运行时的数据区域
所有线程共享方法区和堆;
虚拟机栈、本地方法栈和程序计数器是线程隔离的数据区。
1) 程序计数器(ProgramCounter Register)
程序计数器是一块较小的内存空间,作用相当于当前线程所执行的字节码的行号指示器。在java虚拟机概念里,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令,很多基础功能都需要依赖这个计数器来完成,如分支,循环,跳转等。
每条线程都需要一个独立的程序计数器,并且各条线程之间的计数器互不影响,能够独立存储。
2) Java的虚拟机栈VMStack
虚拟机栈是类中方法的执行过程的内存模型,与程序计数器一样,虚拟机栈也是线程私有的,它的声明周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(stackframe)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应一个栈帧在虚拟机中从入栈到出栈的过程。
栈帧是虚拟机栈的栈元素。对于活动线程中栈顶的栈帧,成为当前栈帧,这个栈帧所关联的方法称为当前方法,正在执行的字节码指令都只针对当前栈帧进行操作。
通常把java内存分为堆内存(heap)和栈内存(stack),其中的栈就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型、对象引用、返回地址类型。
3) Java堆
堆是类实例和数组的分配空间,是多有线程共享的内存区域。堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
4) 方法区
方法区是虚拟机启动时创建,是多有线程共享的内存区域。用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
5) 运行时常量池
运行时常量池(RuntimeConstantPool)是方法区的一部分。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。