JVM
对象的内存布局
分为3块区域
对象头、实例数据、对齐填充
对象头分为两部分:第一,存储对象的自身运行时数据(Mark Word);第二,类型指针,jvm通过这个指针来确定这个对象是哪个类的实例(不一定都有)。
实力数据是真正存储的有效信息。
对齐填充仅仅起占位符的作用。
对象的访问定位
java程序是通过栈上的 reference数据 来操作堆上的具体对象,这个类型在java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式、去定位访问堆中的对象的具体位置
目前主流的访问方式有使用句柄和直接指针两种
使用句柄
使用直接指针访问
reference中存储的直接就是对象地址
使用句柄最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针而reference本身不需要修改.
用直接指针访问方式的最大好处就是速度快节省了一次指针定位的时间开销。
实战:OutOfMemoryError异常
第一个异常:java堆溢出
java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
解决办法
一般的手段是先通过内存映像分析工具对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是内存泄露还是内存溢出。如果是内存泄露可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收他们。掌握了泄露对象的类型信息及GC Roots引用链的信息就可以比较准确的定位泄露代码的位置。
如果不存在泄露,就是内存中的对象确实还必须存活着那就应当检查虚拟机的堆参数与机器物理内存对比,看是否可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
第二个异常:虚拟机栈和本地方法栈溢出
这里规定了两种异常
第一,如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出栈越界错误异常;
第二,如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出超出内存错误异常。
单线程条件下无论是栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是栈越界错误异常stackoverflowerror
java提供参数控制java堆和方法取的内存的最大值,内存减去最大堆容量再减去最大方法区容量,程序消耗内存忽略,进程本身内存不计算在内,剩下的内存由虚拟机栈和本地方法栈瓜分了,如果每个线程分配到的容量越大,可以建立的线程数量越少,建立线程时越容易把剩下的内存耗尽。
多线程条件下出现stackoverflowerror异常,有错误堆栈可以阅读,比较容易找到问题所在,但是如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。
第三种异常方法区和运行时常量池溢出
String.intern()作用:
如果字符串常量池中已经包含一个等于此String对象的字符串,返回代表池中这个字符串的字符串对象;否则将String包含的字符串添加到常量池中,并返回此String对象的引用。
第四种异常,本机直接内存溢出
由directMemory直接内存导致的内存溢出一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,程序中又直接或间接使用了NIO,可以考虑这方面的原因。
垃圾收集器与内存分配策略
主要解决三个问题
哪些内存需要回收?
什么时候回收?
如何回收?
垃圾收集器主要关注堆和方法区中内存的回收
需要确定哪些对象,还活着,哪些已经死去
判断对象是否存活的算法:
引用计数算法
给对象中添加1个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时技术气质就减1,任何时刻计数器为零的对象就是不可能再被使用的对象。
他的优点是实现简单判定效率高
应用案例-微软公司的com技术,使用actionscript3的flashplayer
但是主流的java虚拟机中并没有选用引用技础算法来管理内存主要原因是他很难解决对象之间相互循环引用的问题。
主流的商用程序语言主流实现中都是通过可达性分析判定对象是否存活。
基本思路是通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象不可用。
在java语言中作为鸡西如此的对象主要有--虚拟机栈中(栈帧中的本地变量表)引用的对象,方法其中类静态属性引用的对象,方法其中常量引用的对象,本地方法栈中jni(一般说的Native方法)引用的对象。
jdk1.2以前,引用的定义为如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就这块内存代表着一个引用。
jdk1点二之后,江引用分为强引用软引用弱引用虚引用四种,强度依次减弱。
强引用为使用new 新建对象之类的引用,只要强引用,还在垃圾收集器永远不会回收被引用的对象。
软引用是用来描述一些,还有用,但并非必要的对象,在系统将要发生内存溢出异常之前会对这些对象进行回收范围内第二次回收。
若弱引用也是用来描述非必须对象,但强度更弱,被弱引用关联的对象,只能生存到下一次垃圾收集发生之前垃圾收集器工作时无论当前内存是否足够,回收掉只被弱引用关联的对象。
虚引用也称为幽灵引用或者幻影引用一种最弱的引用关系,一个对象是否有引用的存在完全不会对其生存时间构成影响,也不会通过虚引用取得一个对象实例,一个对象设置虚引用关联的唯一目的就是在这个对象被收集器回收时收到一个系统通知。
在可达性分析算法中不可达的对象也并非是必须被回收的,要真正宣告一个对象死亡至少要经历两次标记过程,在可达性分析后发现没有与GC Roots相连的引用链,被第一次标记并且进行一次筛选,筛选条件是否有必要执行finalize()方法。
没有必要执行的条件是,对象没有覆盖此方法,或者此方法已经被虚拟机调用过。
如果有必要执行,则这个对象会被放置在一个队列之中,在这个队列之中会进行第二次标记,如果对象,与引用链上的任何一个对象,建立关联,则不会被回收。
回收方法区
在方法区中进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
回收废弃常量与回收Java堆中的对象非常类似。 以常量池中字面量的回收为例,假如一个字符串“abc”已经进人了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。 常量池中的其他类(接口),方法、字段的符号引用也与此类似。
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。 类需要同时满足下面3个条件才能算是“无用的类”:
口该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
口加载该类的 ClassLoader已经被回收。
口该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法.
垃圾收集算法
标记清除算法
首先标记出所有需要被回收的对象,在标记完成后统一进行回收。
他的不足主要有两个第一是效率问题,标记和清除2个过程的效率都不高,另一个是空间问题,标记清除之后会产生大量的内存碎片,内存碎片是不连续的,导致在运行过程中需要分配较大对象,无法获得连续内存,不得不提前触发另一次垃圾收集动作。
复制算法
主要针对效率问题,把内存容量分为相等的两块,每次只使用其中一侧的内存,用完后,把活着的对象复制到另外一块上面,然后再把使用的内存空间一次清理掉。
不足是把内存空间缩小了一半。
虚拟机中现在都采用这种收集算法来回收新生代。
新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的 Survivor空间,每次使用Eden 和其中一块Survivor当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot 虚拟机默认Eden和Survivor 的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。
如果另外一块survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
针对老年代的特点,提出了标记-整理算法。标记过程和标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
根据对象存活周期的不同,将内存划分为几块,一般分为新生代和老年代。新生代每次垃圾收集时都会都会有大批对象死去只有少量存活,用复制方法。需要付出少量存活对象的复制成本即可。老年代因为对象存活率高,没有额外空间对她进行分配担保,就需要使用标记整理或者标记清除算法来进行回收。
HotSpot算法实现
可达性分析对执行时间的敏感性体现在GC停顿上。因为分析工作必须在一个能确保一致性的快照中进行。一致性是指整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,如果不满足的话,分析结果准确性就无法得到保证。这点也是导致GC进行必须停顿所有执行线程的一个重要原因。
现在的主流虚拟机使用的都是准确是GC执行系统停下来,并不需要检查所有执行上下文和全局的引用位置,是有办法直接得知哪些地方存放着对象引用。在hotspot的实现中使用一组称为oopMap数据结构来达到这个目的,在类加载完成的时候,hotspot把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程,在特定的位置记录下栈和寄存器中哪些位置是引用,GC扫描时就可以直接得知这些信息。这时会准确而快速完成根节点枚举,但是这个时候会有一个很现实的问题,可能导致引用关系变化。也不会为每一条指令都生成对应的oopmap,空间成本太高。前面提到的特定的位置记录的信息,这些位置称为安全点。安全点,程序执行时并非在所有的地方都能停顿下来开始GC,只有到达安全点时才能暂停。安全点的选定基本上是以是否具有让程序长时间执行的特征为标准进行选定。
长时间之前最明显的特征就是指令序列复用,比如循环跳转,具有这些功能的指令才会产生安全点。安全点太少,会让GC等待时间太长,过于频繁会导致过分增大运行时的负荷。
安全点,另一个需要考虑的问题是如何在gc发生时让所有线程都到最近的安全点上停顿下来。主要有两种方案,抢先式中断和主动式中断。
抢先是中断,不需要线程的执行代码主动去配合,在gc发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点,就恢复线程,让他跑到安全点。现在几乎不用这种方式。
主动式中断是当GC需要中断线程的时候,不需要直接对线程操作,仅仅简单的设置一个标志,各个线程执行时会主动轮巡这个标志,发线中断标志为真时,就自己中断挂起,标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
安全点机制保证了程序执行时在不太长的时间内就会遇到可进入GC的安全点。在程序不执行的时候是什么情况呢?程序不执行就是没有分配CPU时间比如线程处于sleep状态,blocked状态,这时候线程无法响应虚拟机的中断请求走到安全的地方去中断挂起,显然也不太可能等待线程重新被分配cpu时间,这个时候需要安全区域来解决。安全区域是指在一段代码片段之中,引用关系不会发生变化,在这个区域中的任何地方开始GC都是安全的。
在线程执行到安全区中的代码时,首先标识自己已经进入了安全区,那样在这段时间里虚拟机要发起GC时就不用管标识自己为安全区状态的线程了,在线程离开安全区时,要检查系统是否已经完成了整个GC过程或者根节点枚举,如果完成了就继续执行,否则就必须等到可以安全离开安全区的信号为止。
垃圾收集器
JVM
连线的可以搭配使用
Serial收集器
他是一个单线程收集器,单线程的意义并不仅仅说明他只会使用一个cpu或1条收集线程去完成垃圾收集工作,更重要的是进行垃圾收集时必须暂停其他所有的工作线程,直到收集结束。
他的缺点是在垃圾收集时会导致停顿。它的优点是与其它收集器的单线程比,他简单而高效。
他是虚拟机运行在Client模式下的默认新生代收集器。
ParNew收集器
Serial收集器的多线程版本,其他的都一样。运行在Server模式下的虚拟机中首选的新生代收集器。主要的一个原因是只有它能与CMS收集器配合工作。
并行:只多条垃圾收集线程,并行工作,但此时用户线程仍然处于等待状态。
并发:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序继续运行而垃圾收集程序运行另一个cpu上。
Parallel Scavenge收集器
新生代收集器,使用复制算法,并行的多线程收集器,(开关参数)GC自适应调节策略
他的目标是达到一个可控制的吞吐量。吞吐量高则可以高效率利用CPU时间,尽快完成程序的运算任务,适合在后台运算,而不需要太多交互的任务。
而其他收集器主要的关注点是尽可能的缩短垃圾收集时用户现线程的停顿时间。
一共了两个参数,用于精确控制吞吐量,一个是控制最大垃圾收集停顿时间,另一个是直接设置吞吐量大小。
gc停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。
Serial Old收集器
Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。主要意义:给Client模式下的虚拟机使用。
在Server模式下,主要还有两大用途,一种用途是在jdk1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old收集器
Parallel Scavenge的老年代版本,使用多线程和标记整理算法。在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是如果新生代选择了去Parallel Scavenge收集器,老年代除了Serial Old的收集器外别无选择,由于老年代Serial Old在服务端应用性能上的拖累,使用了这个Parallel Scavenge未必能在整体应用上获得的吞吐量最大的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定要有ParNew加CMS的组合给力。
在注重吞吐量以及CPU资源敏感的场合都可以优先考虑Parallel Scavenge加
Parallel Old收集器。