java运行时数据区域
java虚拟机执行java程序时会将它管理的内存划分为若干个不同的数据区域。
程序计数器
程序计数器设计是一块较小的内存空间,可以看做当前线程执行字节码的行号指示器,是线程私有的。
如果线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果是native方法,这个计数器值为空。
唯一一个在java虚拟机规范中没有规定OutOfMemoryError的区域。
java虚拟机栈
虚拟机栈也是线程私有的,生命周期与线程相同。
虚拟机栈描述的是java方法执行的内存模型:每个方法执行的同时会创建一个栈帧,用于储存局部变量表、操作数栈、动态链接,方法出口等信息。
每一个方法从调用到完成的过程,都对应着一个栈帧从出栈到入栈的过程。
栈深度不够会抛出StackOverFlowError,如果栈可以动态扩展,虚拟机内存不够时会抛出OutOfMemoryError
本地方法栈
本地方法栈与虚拟机栈发挥的作用是非常类似的,不同的是虚拟机栈执行为执行java方法服务,本地方法栈为执行native方法服务。
堆
对大多数应用来说,堆是java虚拟机所赶礼的内存中最大的一块,是线程共享的,虚拟机启动时创建。
此内存区域唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存(native方法创建的对象可能不在堆上)。
java堆是垃圾收集的主要区域,很多时候被称为“GC堆”,现在的收集器基本上都使用分代收集算法,所以java堆还可以细分为新生代和老年代。
java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
堆无法扩展时会抛出OutOfMemoryEorror;
方法区
虚拟机启动时创建,各个线程共享,存储已被加载的类信息、常量、静态变量、即时编译后的代码等数据。
jdk1.8以前用永久代来实现,jdk1.8以后用元空间来实现方法区。
jdk1.7以后,java虚拟机将运行时常量池等从方法区移到了java堆中(字符串常量池包含在运行时常量池中)。
会抛出OutOfMemoryEorror;
直接内存
NIO可以使用Native函数库直接分配堆外内存,然后通过java堆中的DirectByteBuffer作为这块内存的引用进行操作。
垃圾回收
判断对象是否存活
1.引用计数器法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不在被使用的。
优点: 实现简单,判定效率高,在大部分情况下都是一个不错的算法。
缺点: 无法解决对象循环引用的问题。
User userA = new User();
User userB = new User();
userA.eat() = userB;
userB.eat() = userA;//循环引用
userA=null;
userB=null;//将对象设为空,GC回收
System.gc;
上述代码对象userA和userB互相引用,所以如果用引用计数器法的话虚拟机并不会回收它们,这样下去就会导致内存崩溃。所以,jvm中使用了可达性算法来判断对象是否存活。
2.可达性算法
这个算法的思想是通过一系列的”GC roots“ 的对象作为根节点,从这些节点开始向下搜索,搜索走过的路径称为”引用链“,当一个对象到GC Roots没有任何一个引用链相连,则证明这个对象是不可用的。
java解决新生代老年代互相引用问题,在程序执行时会专门开辟一块空间记录下来,称为rememberedSet,在新生代进行可达性分析时,以GC Roots和rememberedSet为依据。
java中可做为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中Native方法引用的对象。
3.对象自我拯救:finalize方法
如果发现一个对象没有与GC Root的引用链,在准备垃圾回收时,会先检查它有没有重写finalize方法,如果重写并且没有没有执行过此方法时,会先执行此方法;如果没有重写或者重写但是已经执行过一次此方法那么此方法都不会被执行。垃圾回收器将在下次回收时真正回收对象占用的内存。
如果对象在finalize方法执行完后重新连接上GC ROOT,便不会回收它。此方法由虚拟机自动创建的低优先级的线程执行,但不一定等他执行完这个方法。所以,即使这个方法使对象重新连接上了GC ROOT,这个对象还是有可能会被回收。
fianlize方法还可以用于回收JVM回收不了的垃圾,JVM只能回收堆上的垃圾,但是有一些对象(如Native方法创造的对象)不在堆上,这时JVM没有办法回收它们,但是可以在finalize方法中回收它们。
(番外)强、软、弱、虚引用
强引用:类似Object object = new object();,只要强引用还在,垃圾回收器就永远不会回收被引用的对象
软引用:一些也有用但非必须的对象;在即将抛出内存溢出异常之前才会回收,如果这次回收还是没有足够内存,才会抛出内存溢出异常
弱因用:也描述非必需对象,只能活到下一次垃圾回收之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收。
虚引用:一个对象是否有需虚引用不影响其生存时间,此引用唯一目的是在此对象被回收时收到一个系统通知。
垃圾收集算法
标记-清除算法
首先标记出要被回收的对象,标记完成后统一回收所有被标记的对象
缺点:1.标记和清除的效率都不算高
2.清除完之后会产生大量不连续的内存碎片,可能会导致以后创建大对象时找不到足够连续内存而提前触发垃圾回收。
复制算法
将内存化为大小相等的两块,当一块内存用完了就将还活着的对象复制到另一块,把已使用过的一次清理
优点: 不会出现内存碎片化
缺点: 将内存缩小为原来一半,代价有点高;对象存活率高时效率低
标记-整理算法
首先标记出要被回收的对象,标记完成后将活着的对象都向一端移动,然后清除边界以外的部分。
分代算法
根据对象存活周期不同,将对象分为新生代和老年代
新生代:
- java新生代垃圾回收运用的是复制算法。
- java新生代分为两部分:Eden区和两个Survivor区,它们的比例是8:1:1,两个Survivor区又分为 fromSurvivor区和to Survivor区,其中新生代每次进行Minor GC(新生代的GC)之前,to Survivor区一定是空的。
- 对象优先分配在Eden区,每当新创建一个对象而此时Eden区内存不足的时候,就会进行Minor GC,然后Eden区仍然活着的对象会复制到to Survivor区,而from Survivor区中年龄超过阀值的对象会保存到老年代中,没有超过阀值的对象会保存到to Survivor区中,然后to Survivor区和from Survivor区互换位置,保证每次都有一块Survivor区是空闲的。
- 在Survivor区中的对象每经过一次Minor GC年龄就会加1,默认年龄阀值为15,这个阀值可自行设置,设置方法请自行百度。
- 当Survivor区中年龄相同的对象加起来超过Survivor区一半内存大小的时候,Survivor区就会提前把大于等于此年龄的对象复制到老年代中。
- 某些大对象(内存大)创建的时候不经过新生代,而是直接放在老年代中,原因是减少每次复制大对象时浪费的时间,所以尽可能减少大对象的创建。
- 如果Survivor区空间不够用时,需要依赖老年代分配担保,分配担保时创建的对象直接进入老年代
分配担保:
在MinorGC之前,虚拟机会先检查老年代的最大可用连续内存是否大于新生代所有对象总空间。如果这个条件成立,MinorGC时安全的;如果不成立,虚拟机会查看是否允许担保失败。如果允许,会检查老年代最大可用连续内存是否大于历次晋升到老年代对象的平均大小。如果大于,将尝试进行一次MinorGC,如果小于或者不允许担保失败,就会进行full GC
老年代:
在进行Minor GC之前,JVM会先检查老年代最大可用连续空间是否大于新生代历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,否则就进行Full GC(全局GC)。
老年代运用标记-整理算法回收垃圾,将存活的对象向一端移动,清楚边界以外的部分。
hotSpot算法实现
枚举根节点
GC停顿:(Stop The World)可达性分析期间整个执行系统像是被冻结在某个时间点,不能出现分析过程中对象引用关系还在不断变化,导致GC发生时必须停顿所有java执行线程。
准确式内存管理:虚拟机可以知道内存中某个位置的数据具体是什么类型
当前主流虚拟机用的都是准确式GC,所以当执行系统停下来后,虚拟机有办法得知那些地方存在着对象引用,在HopStop中,虚拟机通过oopMap数据结构达到这个目的
安全点
在oopMap协助下,HopStop可以快速准确的的完成GC ROOT枚举,但oopMap内容变化指令非常多,如果为每一条指令都生成oopMap,将需要大量额外空间,这样GC空间成本非常高,所以hopStop只是在安全点记录了这些信息。所以程序不是在所有地方都能停下来执行GC,只有在安全点时才能暂停。
安全点选定标准:是否具有让程序长时间执行的特征,明显特征如:方法调用、循环跳转、异常跳转
在GC发生时,如果保证所有线程跑到最近的安全点在停下来:
1.抢占式中断:GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。
2.主动式中断:当GC需要中断线程时,在安全点上设置一个标志,各个线程执行时主动去轮询这个标志,如果发现标志为真就自动中断挂起
安全区域
安全点解决不了正处于sleep状态的线程,此时的线程无法响应JVM中断请求,无法跑到安全点中断挂起。
当线程执行到了安全区域时,标识自己在安全区域。那样,在这段时间JVM发起GC时,就不用管标志自己为安全区域的线程了。在线程要离开安全区域时,检查系统是否完成了根节点枚举,,如果完成线程就继续执行,否则必须等待收到可以安全离开安全区域的信号。
垃圾收集器
serial收集器
新生代垃圾收集器,单线程,在它进行垃圾回收时,必须停掉其他所有工作线程,直到它收集结束
运行在客户端模式下时是很好的选择
parNew收集器
是serial的多线程版本,也是新生代垃圾收集器。适用于服务端,因为它可以与CMS收集器配合工作
parallel Scavenge 收集器
新生代收集器,吞吐量规则
吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间)
serialOld收集器
serial收集器老年代版本,基于标记-整理算法,运行在客户端模式下。如果运行在服务端模式下,有两大用途:
1.与parallel Scavenge 收集器搭配使用;
2.作为CMS垃圾收集器的后背预案。
parallel Old收集器
parNew收集器老年代版本,基于标记-整理算法,搭配parallel Scavenge 收集器比serialOld更好。
CMS收集器
以获取最短停顿时间为目标,基于标记-清除算法,是老年代垃圾收集器。
垃圾收集过程:
1.初始标记:需要“Stop The World”,仅仅标记一下GC ROOT能直接关联到的对象,速度很快
2.并发标记:可达性分析,此时运行用户程序并发执行
3.重新标记:需要“Stop The World”,修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段停顿时间比初始标记时间略长,但远比并发标记时间短
4.并发清除:此时运行用户程序并发执行
缺点: 1.对cpu资源敏感
2.无法处理浮动垃圾。垃圾收集阶段用户线程还在运行,此时产生的垃圾只能在下一次GC时清理,这些称为浮动垃圾。因此,CMS收集器不能等老年代满了之后再清理,需要预留一部分空间提供并发收集时的程序运作使用。
3.标记-清除算法,会产生大量空间碎片