注:部分摘自
Java内存模型有五个:方法区、Java堆、Java栈、程序计数器、本地方法栈
方法区
方法区在一个JVM实例的内部,类型信息存在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的。类静态变量也存放在方法区。
一旦一个类要被使用,Java虚拟机就会对其进行装载、连接( 验证、准备、解析 )、初始化。而装载后的结果就是由.class文件转变为方法区的一段特地的数据结构。有以下信息:
类型信息
这个类型的全限定名
这个类型的直接超类的全限定名
这个类型是类类型还是接口类型
这个类型的访问修饰符
任何直接超接口的全限定名的有序列表
字段信息
字段名
字段类型
字段的修饰符
方法信息
方法名
方法返回类型
方法参数的数量和类型(按照顺序)
方法的修饰符
其他信息
除了常量以外的所有类(静态)变量
一个指向ClassLoader的指针
一个指向Class对象的指针
常量池(常量数据以及对其他类型的符号引用):常量池中的数据项是通过索引访问的。
注:构建一个对象时,JVM会在队中给对象分配空间,用于存储当前对象实例属性以及其父类的实例属性。所以,在实例化父类的某个子类时,JVM同时也会构建父类的一个对象,(从调用当前类构造方法时,首先会调用其父类的构造方法,构造方法的调用意味着实例的创建)
类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在JVM使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。
方法区主要有以下几个特点:
1、各个线程共享,因此,方法区里的数据访问必定是线程安全的。若有两个线程企图访问方法区中同一个类,而这个类还没有被装载,那么只允许一个线程去装载它,另一个必须等待。
2、方法区的大小不固定,也不一定连续。可以根据应用动态调整。
3、方法区可被垃圾回收。
可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
Java堆
Java堆被所有线程共享,在虚拟机启动时创建。用于存放对象实例。堆是垃圾收集器管理的主要区域,因此也被叫做“GC堆“。
堆又分为 新生代和老年代持久代 因为要实现分代回收。
新生代:用于存放新创建的对象。分为Eden Space和两块大小相同的Survivor Space。
内存大小由-Xmn:PermSize
老年代:用于存放经历多次GC仍然存活的对象,新建的对象也有可能直接存放在老年代。1、大的数组对象,且数组中没有引用外部对象 2、大的对象,可通过启动参数设置-XX:Pretenure Size Threshold=1024,设置超过多大事不在新生代分配
内存大小为-Xmx对应的值减去-Xmn对应的值。
持久代:用于存放静态文件
内存大小为-XX:Permsize -XX:MaxPermSize
堆的大小可以通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
Java栈
线程私有,描述的是Java方法执行的内存模型,每个方法都会在执行时创建一个栈帧,用于存放局部变量表、操作栈、动态链接、方法出口
对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的。即当前栈帧,所有的指令都是根据当前栈帧数据进行操作的。
局部变量表
用于存放方法参数和方法内部定义的局部变量。在Java被编译成class文件时,就在方法的code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。
局部变量表的容量以变量槽(slot)为最小单位。虚拟机是使用局部变量表完成参数数值到参数列表的传递过程的,如果是实例方法,那么局部变量表的第0位索引的slot默认是用于传递当大所属对象实例的引用,在方法中通过this访问。
slot是可以 重用的,当slot中的变量超出了作用域,那么下一次分配slot时候,会覆盖原来的数据,slot对对象的引用会影响GC.要是被引用,就不会被回收
操作数栈
操作数栈是一个以字长为单位的数组,但是是通过 压栈、出栈 访问的。byte 、short、char类型的值在压入操作数栈之前会被转为int.
虚拟机把操作数栈作为它的工作区。大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。
动态连接
虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成每个方法的间接引用,如果代表栈A的方法想调用代表代表栈帧B的方法,如果这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须将符号引用转换为直接引用,然后通过直接引用才可以访问到真正方法。
如果符号引用是在类加载阶段或是第一次使用的时候转换为直接引用,则为静态解析,如果是在运行期间转换为直接引用,那么就是动态连接。
返回地址
无论以哪种方式结束,退出当前方法时都会跳转到当前方法被调用的位置。
一、正常退出,退出后根据方法定义决定是否将返回值传给上层调用者,调用者的PC计数器的值可以作为返回地址
二、异常结束,不会将返回值传给上层调用者。根据异常处理表确定。
方法的的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操作,因此方法退出时可能做的事情包括:恢复上层方法的局部变量表以及操作数栈,如果有返回值的话,就把返回值压入到调用者栈帧的操作数栈中,还会把PC计数器的值调整为方法调用入口的下一条指令。
异常
一、线程请求的栈深度大于JVM所允许的深度 StackOverFlowError
二、虚拟机栈扩展时无法申请到足够多的内存会抛出 OutOfMemoruError
程序计数器
较小的内存空间,可以看做是当前执行线程所执行的字节码的行号指示器,
线程私有 :由于JVM的多线程是通过线程轮流切换并分配处理器执行时间实现的,在任何一个确定的时刻,一个处理器只会执行一条线程的指令,为了线程切换后能恢复到正确执行位置,每个线程都需要一个独立的程序计数器
如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
本地方法栈
和Java栈的区别在于:Java栈为虚拟机的Java方法,即字节码服务,本地方法栈为虚拟机使用到的native方法服务。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
内存溢出和内存泄漏
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out ofmemory。
Java 堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heapspace”。
要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
内存分配过程
1、JVM 会试图为相关Java对象在Eden Space中初始化一块内存区域。
2、当Eden空间足够时,内存申请结束;否则到下一步。
3、JVM 试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收)。释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。
4、Survivor区被用来作为Eden及Old的中间交换区域,当Old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区。
5、当Old区空间不够时,JVM 会在Old区进行完全的垃圾收集(0级)。
6、完全垃圾收集后,若Survivor及Old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现“outofmemory”错误。
对象访问
对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区域之间的关联关系,如下面的这句代码:
Object obj = newObject();
假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。
由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。
如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。