1. java内存区域与内存溢出异常
java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。java 虚拟机管理的内存包括以下几个运行时数据区域:程序计数器,java 虚拟机栈,本地方法栈,java堆,方法区,运行时常量区。
1.1运行时数据区
1.1.1程序计数器(线程私有)
程序计数器是一块较小的内存区间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储。
线程执行java方法,计数器记录的是正在执行的虚拟机字节码指令的地址。执行Native方法,计数器值为空(undefined)。
此内存区域是虚拟机规范中唯一没有规定任何oom情况的区域。
1.1.2 java虚拟机栈(私有)
虚拟机栈描述的是java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧(stack frame)用于存储局部变量表,操作数栈,动态链接,方法出入口等信息。方法从调用到执行完成对应着栈帧在虚拟机栈中入栈到出栈过程。
常说的栈内存(stack)也就是虚拟机栈,或者说虚拟机栈中局部变量表部分。表中存放了编译期可知的各种基本类型数据(boolean,byte,char,short,int,float,long,double),对象引用(reference类型,不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
long,doube(64位长度)类型的数据占用2个局部变量空间,其余数据类型占用一个。表中所需的内存在编译期间完成分配,进入一个方法时,方法需要在栈中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小。
对该区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError,如果虚拟机扩展时无法申请到足够的内存,将会抛出OOMError。
1.1.3 本地方法栈(私有)
与虚拟机栈作用类似,虚拟机栈执行java方法,本地方法栈执行native方法。
1.1.4 java堆(共享)
虚拟机创建时启动,目的是存放对象实例,几乎所有对象的实例都在这里分配内存。是垃圾收集器管理的主要区域,也称为“(GC堆)”(garbage collection heap)。
从内存回收角度,收集器基本都采用分代收集算法,java堆可分为新生代和老生代。
java堆可处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,会抛出oom。
1.1.5 方法区(共享)
存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,会抛出oom。
1.1.6 运行时常量池
属于方法区的一部分,用于存放编译期生成各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
除了保存class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在池中。此外,运行时也可将新的常量放入池中(string的intern()方法)。
当常量池无法申请内存时抛出oom。
1.2 虚拟机对象
1.2.1 对象的创建
虚拟机遇到new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已经被加载,解析和初始化过。如果没有就去执行类加载过程。类加载检查通过后,虚拟机将为新生对象分配内存,所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,保证了对象的实例字段在java代码中可以不赋初始值就直接使用,程序将访问到对应的零值。
next,虚拟机将把这个对象是那个类的实例,如何找到累的元数据信息,对象的哈希码,GC分代年龄等信息放在对象头中。
至此,从虚拟机角度看新对象已产生,但从java角度,对象创建刚刚开始,方法还没执行,所有字段都还为零。所以,一般来说,执行new指令后会执行方法,把对象进行初始化来产生真正可用的对象。
1.2.2 对象的内存布局
对象在内存中布局可分为3块区域:对象头,实例数据,对齐填充。
对象头包括两部分:
1.用于存储对象自身的运行时数据,如哈希吗,GC分代年龄等。
2.类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。在父类中定义的变量会出现在子类之前。
3.对齐填充不是必然存在的。自动内存管理系统要求对象起始地址必须是8字节的整数倍。
1.2.3 对象的访问定位
java程序需要通过栈上的reference数据来操作操作堆上的具体对象。主流的访问方式有使用句柄和直接指针两种。
句柄访问:java堆中划分出一块内存作为句柄,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。好处:reference中存储的是稳定的句柄地址,在对象移动(垃圾收集时移动对象时非常普遍的行为)时只会改变句柄中的实例数据指针,reference本身不需要修改。
指针访问:reference数据中存储的直接就是对象地址。好处:速度更快。对象访问十分频繁,积少成多后将成为非常客观的执行成本。
2 .垃圾收集器与内存分配策略
�程序计数器,虚拟机,本地方法栈等3个区域随线程而生,随线程而灭。每一个栈帧分配多少内存基本上是类结构确定下来时就已知的。
java堆和方法区在程序处于运行期间才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
2.1 判断对象是否存活
引用计数算法:很少使用,难以解决对象之间相互循环引用的问题。
可达性分析算法:java,c#都用可达性分析判断对象是否存活。思路:通过一系列称为“GC Roots”的对象为起始点,从这些节点向下搜索,搜索走过的路径称为引用连,当一个对象到GC Roots没有任何引用链相关连时,对象不可用。
可以作为GC Root 引用点的是:
虚拟机栈(栈帧中的本地变量表)中的引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用指向的对象。
Native方法中JNI(一般所说的native)引用的对象。
2.2 生存还是死亡
即时在可达性分析算法中不可达的对象也不是非死不可。宣告一个对象死亡至少经历两次标记过程:如果对象在进行可达性分析后没有与GC Root相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。
1.当对象没有覆盖finalize(),finalize()已经被虚拟机调用过,虚拟机将这两种情况视为没有必要执行。
2.对象有必要执行finalize()方法,这个对象会放在F-Queue的队列中。稍后,GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中拯救自己,只要重新与引用链上的任何一个对象建立关联即可。如果这时候对象还没有逃脱,那么他就真的被回收了。
2.3 垃圾回收算法
2.3.1 标记—清除算法
最基础的收集算法是“标记-清除”算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。不足:1.标记和清除两个过程的效率不高。2.空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.3.2 复制算法
把内存划分成等量的两块,每次只使用一块。一块用完后把存活着的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。优点:每次对整个半区进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点:内存缩小为原来的一半,代价太大。
目前商业虚拟机都采用这种算法来回收新生代。因为新生代是朝生夕死,所以不需要按照1:1的比例来划分内存空间而是划分成较大的Eden空间和两块较小的survivor空间,每次使用Eden空间和一块survivor空间。回收时把Eden和survivor空间中存活的对象一次复制到另一个survivor空间,然后清理Eden和survivor空间。
2.3.3 标记—整理算法
主要用于老年代,标记过程与“标记-清除”算法一样,但后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边以外的内存。
2.3.4 分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”算法,根据对象存活周期把内存分为几块,一般是把java堆分为新生代和老生代,这样可以根据各个年代的特点采用最合适的收集算法。
在新生代中,每次垃圾收集时都有大批对象死去,之后少量存活,那就选用复制算法。老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
2.4 hotspot的算法实现
hotspot虚拟机上实现以上算法的时候,必须对算法的执行效率有严格的考量,才能保证虚拟机高效运行。
2.4.1 枚举根节点
如果在应用中使用可达性分析从GC Root节点找引用链这个操作,会消耗很多时间。(可作为GC Root节点的主要在全局性引用(例如常量或类静态属性)与执行上下文中(例如栈帧中的本地变量表))而且,可达性分析对执行时间的敏感性还体现在GC停顿上,因为该分析工作分析期间整个执行系统看起来就像被冻住在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。这是导致GC进行时必须停顿所有java执行线程的其中一个重要原因。
主流的java虚拟机使用的是准确式GC(虚拟机可以知道内存中某个位置的数据具体是什么类型),所以当系统停顿下来之后,不需要一个不漏的检查完所有的执行上下文和全觉得引用位置,虚拟机有办法直接得知哪些位置存放着对象引用。在hotpot的实现中,用OopMap的数据结构来实现,在类加载完成后,就把对象内什么偏移量上是什么类型的数据计算出来,在jit编译过程中也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描的时候就可以直接得知这些信息了。
2.4.2 安全点
OopMap只在特定的位置记录这些信息,这些位置称为“安全点”。即程序执行时并非在所有的地方都能停下来开始GC,只有在到达安全点时才能暂停。
安全点选取需要考虑两点:
1.安全点的选定不能让GC等待时间人太长,也不能过于频繁以至于过分增大运行时的负荷。所以安全点的选定事宜程序是否具有让程序长时间执行的特征为标准进行选定的。长时间的最明显特征就是指令复用,例如方法调用,循环跳转,异常跳转等,所有具有这些功能的指令才会产生安全点。
2.如何在GC发生时让所有的线程都跑到安全点上再停下来。1.可选用抢先式中断,不需要线程的执行代码去主动配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。2选用主动式中断,当GC需要中断线程时,不直接对线程操作,设置一个标志,各线程执行时主动去轮询这个标志,发现中断标志为真就自己中断挂起,轮询标志的地方和安全点时重合的,另外再加上创建对象需要分配内存的地方。
2.4.3 安全区域
当程序不执行时,没有分配cpu时间(比如线程处于sleep状态活着blocked状态),这时候线程时无法响应jvm的中断请求,走到安全点去中断挂起,jvm也不太可能等待线程重新被分配cpu时间。这时需要安全区域来解决。
安全区域是指在一段代码片段中,引用关系不会发生变化,这个区域中任意地方开始GC都是安全的。可以看作被扩展了的安全点。
2.5垃圾收集器
2.5.1 serial收集器(单线程)
这是最基本,发展历史最悠久的收集器。是虚拟机运行在client模式下默认的新生代收集器。
用一个cpu或一条收集线程完成垃圾收集工作,在进行垃圾收集时必须暂停其他所有的工作线程,直到它收集结束。
优点:简单而高效。对于限定单个cpu的环境来说,没有线程交互的开销,专心做垃圾收集可以获得最高的单线程收集效率。
2.5.2 parnew收集器(多线程)
是serial收集器的多线程版本。时许多运行在server模式下虚拟机中首选的新生代收集器。只有它能与cms收集器配合工作。
在单cpu情况下不如serial,但是多cpu有优势。
2.5.3 parallel scavenge 收集器 (多线程)
新生代收集器,使用复制算法,并行的多线程收集器。
达到可控制的吞吐量,可以高效的利用cpu时间,尽快完成运算任务。适合在后台运算而不需要太多交互的任务。
2.5.4 serial old 收集器 (单线程)
是serial的老年代版本。
2.5.5 parallel old 收集器 (多线程)
是parallel的老年代版本。
2.5.6 cms收集器(多线程)
尽可能地缩短垃圾收集时用户线程的停顿时间,良好的响应速度能提升用户体验。
2.5.7 G1收集器
2.6 内存分配与回收策略
java内存体系中所提倡的自动内存管理最终可以归结为自动化的解决了两个问题:给对象分配内存及回收分配给对象的内存(垃圾收集器体系以及运作原理)。
对象的内存分配就是在堆上分配,对象主要分配在新生代的Eden区上,少数情况下也可能会直接分配在老年代中。
以下为集体哦啊最普遍的内存分配规则:
2.6.1 对象优先在Eden分配
新生代GC:指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕灭的特性,所以新生代GC十分频繁,一般回收速度也比较快。
老年代GC:指发生在老年代的GC,出现了老年代GC,经常会伴随至少一次的新生代GC。速度比新生代GC慢10倍以上。
大多数情况下,对象在新生代Eden区中分配,当该区没有足够空间进行分配时,虚拟机将发起一次新生代GC。
2.6.2 大对象直接进入老年代
大对象是指需要大量连续内存空间的java对象,最典型的就是那种长的字符串以及数组。
2.6.3 长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(age)计数器,如果对象在Eden出生并经过第一次新生代GC后仍然存活,并且能被survivor容纳的话,将被移动到 survivor空间中,并且对象年龄设为1。每在survivor区中安过一次新生代GC,age都会增加1,年龄增加到一定程度(默认15)会被晋升到老年代中。
2.6.4 动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到最大(15)才能晋升到老年代。如果survivor空间中相同年龄所有对象的大小总和大于survivor空间的一半, 年龄大于或等于该年龄的对象就可直接进入老年代,不需要按照最大年龄来。
2.6.5 空间分配担保
发生新生代GC之前,虚拟机会先检查老年代中最大可用连续空间是否大于新生代所有对象总空间。成立则新生代GC确保是安全的。如果不成立,虚拟机会产看是否允许分配担保失败,如果允许会继续检查老年代中最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于尝试进行一次新生代GC(有风险)。如果小于,或者不允许就会进行老年代GC。
网友评论