一 概述
众所周知c语言是鼻祖.而让c语言的特点之一就是指针.那在Java中是没有指针这个概念的.但是没有并不表示不存在,在Java中,每次new一个对象的时候,其实就是在内存中开辟了一块控件,对象的引用实际上就是指针.只不过java中把对内存的操作交给了JVM.
c语言的指针让你可以操作内存,但是同时你也要去维护这个指针.而java中操作内存的工作交给了JVM.所以一定程度上减轻了程序猿的负担.当然若是在这种情况下出现内存泄漏和内存溢出问题.如果没有了解JVM是怎样使用内存的,将会导致异常排除变得非常困难.
二 JVM内存分布
如图.JVM的内存主要包括两个子系统和两个组件.两个子系统分别是CLASS LOADER(类加载器)和EXECUTION ENGINE(执行引擎).两个组件分别是RUNTIME DATA AREA(运行数据区域)和NATIVE INTERFACE(本地接口库)组件.
1.CLASS LOADER。java运行的时候并不是直接运行代码,而是要通过类加载器把java class加载到JVM中然后运行.负责加载的这部分就是CLASS LOADER.我们可以通过重写ClassLoader来拓展程序的功能.例如:1)在执行非置信代码之前,自动验证数字签名.2)动态地创建符合用户特定需要的定制化构建类.3)从特定的场所取得java class,例如数据库中.4) 等等
2.EXECUTION ENGINE。执行classes中的指令。任何JVM specification实现(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好坏主要就取决于他们各自实现的Execution engine的好坏。
3.NATIVE INTERFACE .用作与其他语言编程的接口.可以通过这个组件调用其他语言的程序.
4.Runtime Data Area组件 这就是我们常说的java内存了
1、Heap (堆):一个Java虚拟实例中只存在一个堆空间,是Java内存管理中最大的一块.也是被所有的线程所共享的一块.在虚拟机启动的时候创建.
堆内存中存储着基本上所有的对象.因为技术的更新 现在也不是绝对的存放在堆内存中,java中的gc也是主要对堆内存进行操作,所以堆内存也可以称为GC内存.gc采用的算法是分代收集算法,所以在堆中又可以分为,年青代(Young)、年老代(Tenured).更细致的可以划分为 Eden空间,From Survivor空间和To Survivor空间.不论怎么划分.堆都是用来存放对象.
根据Java虚拟机规范规定,java堆可以处于物理上不连续的内存空间中(多条内存条),只要逻辑连续即可.在创建堆的时候我们可以固定堆的大小,也可以使用可拓展的堆,如果堆中没有足够的内存来分配对象就会报出OutOfMemory的错误
2、Method Area(方法区域):Method Area和Heap一样,也是被所有线程所共享的一块,它是用于存储已被虚拟机加载的类的信息,常量,静态变量,即时编译器编译后的代码等数据,java对这个区域的管理比较轻松,除了和堆一样可以选择固定内存和拓展以外,还可以对方法区选择不进行垃圾回收机制,相对而言,gc在这里触发的概率相对较小,但也不意味着不会被回收,这个区域的回收工作主要是针对常量池的回收和类型的卸载.当方法区无法满足内存分配的时候,会抛出OutOfMemory的错误,
3、JavaStack(java的栈):Java Virtual Machine Stacks 是属于线程私有的,他的生命周期和线程的相同.每个方法被执行的时候都会在栈中创建一个栈帧.栈帧中包括局部变量表,操作栈,动态链接,方法出口等.
局部变量表用于存储方法的参数和局部变量.
操作栈,也成为"基于栈的执行引擎",主要是运行过程中的算数计算和调用其他方法参数的传递.
动态链接是每个栈帧在执行时常量池中都有一个引用,Class文件中会有大量的符号引用,字节码中方法调用就是使用这些符号引用,这些符号引用在第一次加载的时候就转换为直接引用的成为静态解析(静态方法),和每一次调用的时候才转换为直接引用的成为动态链接.
方法出口,当方法运行的时候只有两种情况会退出方法,一种是异常退出,另一种是执行到方法出口(return).当方法执行完可能进行的操作是1.恢复上层方法的局部变量表和操作数栈.2.把返回值压入调用者调用者栈帧的操作数栈.3.调整 PC 计数器的值以指向方法调用指令后面的一条指令.
4、Program Counter(程序计数器):每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。这块内存不会抛出OutOfMemory
5、Native method stack(本地方法栈):保存native方法进入区域的地址
三 Java对象调用
介绍完java内存的分布,现在来说说java的对象是怎样的一个存在.最普通创建对象的方法是
Object object=new Object();
这里面包含了前面所说的三块内存的使用.Object object 这句话就是值在栈中保存了Object对象的引用,new Object();这段话则表示在堆内存中开辟了一段内存存储Object的对象.Object() 这个方法是存在于方法区中.
在Java中reference类型引用对象有两种主流的方式,一种是通过句柄访问,一种是直接访问.
1.句柄访问会在堆内存中划分出一块区域用于存储句柄,reference只需要指向这个句柄,而句柄会指向对象和这个对象的方法
2.直接引用 reference中直接存储对象的地址
这两种方式各有优缺点,采用句柄的形式,是在对象发生变化的时候(被回收了)只需要改变句柄的指向对象,不需要修改reference的指向.
采用直接指针访问的时候,因为少了一层指针的指向,所以速度更快.
四 对象的回收
说完对象的调用,接下来就是对象的回收,在Java中gc的回收机制还是比较常见的.一下以堆内存中的分代收集算法做举例.
Sun的JVM Generational Collecting(垃圾回收)原理是这样的:把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)
如上图所示,为Java堆中的各代分布。
1. Young(年轻代)
年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
2. Tenured(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
3. Perm(持久代)
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。
通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的Method Area,不属于Heap。
了解完这些之后,以下的转载一热衷于钻研技术的哥们Richen Wang关于内存管理的一些建议——
1、手动将生成的无用对象,中间对象置为null,加快内存回收。
2、对象池技术 如果生成的对象是可重用的对象,只是其中的属性不同时,可以考虑采用对象池来较少对象的生成。如果有空闲的对象就从对象池中取出使用,没有再生成新的对象,大大提高了对象的复用率。
3、JVM调优 通过配置JVM的参数来提高垃圾回收的速度,如果在没有出现内存泄露且上面两种办法都不能保证内存的回收时,可以考虑采用JVM调优的方式来解决,不过一定要经过实体机的长期测试,因为不同的参数可能引起不同的效果。如-Xnoclassgc参数等。