您的位置:首页 > 编程语言 > Java开发

深入理解Java虚拟机——Java内存区域及垃圾回收

2018-03-22 09:47 169 查看
本文是《深入理解Java虚拟机》Java内存区域及垃圾回收章节的读书总结

虚拟机内存区域

运行时数据区域



程序计数器(Program Counter Register):一块较小的内存空间,是当前线程执行所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,各条线程独立存储互不影响,为线程私有内存。该内存区域没有规定任何OOM错误。

Java虚拟机栈(VM Stack):是线程私有的,生命周期与线程相同。描述Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口信息等。该区域会抛出SOF和OOM错误。

本地方法栈(Native Method Stack):与虚拟机栈类似,区别在于虚拟机栈执行Java方法,而本地方法栈执行的是Native方法。该区域也会抛出SOF和OOM错误。

Java堆(Java Heap):堆被所有线程共享,在虚拟机启动时创建。堆的唯一目的就是存放实例,几乎所有对象实例以及数组都在堆上分配。堆是垃圾回收的主要区域,从内存回收角度可以将堆细分为新生代和老年代,新生代又可以分为Eden、From、To空间。堆中无可用内存且无法再扩展时,抛出OOM错误。

方法区(Method Area):各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、类静态变量、即时编译器编译的代码等。别名叫Non-Heap,习惯上成为永久代。虚拟机规范该区域可以选择不实现垃圾回收,该区域的内存回收目标主要针对常量池的回收以及类型的卸载。该区域在无内存分配时,抛出OOM错误。

运行时常量池(Runtime Constant Pool):该区域是方法区的一部分,用于存放编译期生成的字面常量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。

直接内存(Direct Memory):不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,可能抛出OOM错误。JDK 1.4的NIO类引入通道与缓存区的IO方式,可以使用Native函数库直接分配堆外内存,然后通过DirectByteBuffer对象进行引用。

HotSpot虚拟机对象

对象的创建

类是否已经被加载、验证、解析,加载。

为对象分配内存并将内存空间初始化为零值。

设置对象的类实例、元数据信息等。

执行初始化。

对象的内存布局

对象头:一部分用于存储对象自身的运行时数据,另一部分是类型指针即对象指向它的类元数据的指针。

实例数据:对象中真正存储的有效信息,代码中定义的各项字段内容。

对齐填充:不是必然存在的,起占位符作用,HotSpot需要对象的大小必须是8字节的倍数。

对象的访问定位

句柄访问:堆中划分出一块内存作为句柄池,reference存放对象的句柄地址,而句柄中中包含了对象实例数据和类型数据各自的具体地址信息。

直接指针访问:堆中对象的布局考虑如何放置访问类型数据的相关信息,reference中存储直接就是对象的地址。

直接指针减少一次指针定位的时间开销,HotSpot采用直接指针访问对象

垃圾收集器及内存分配策略

GC需要完成的三件事:

哪些内存需要回收

什么时候回收

如何回收

程序计数器、虚拟机栈、本地方法栈这3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行出栈和入栈操作。而Java堆和方法区内存的分配和回收都是动态的,GC主要关注的是这部分内存。

判断对象是否死亡

引用计数算法 Reference Counting

算法:给对象添加一个引用计数器,有一个地方引用它计数加一,引用失效计数减一,计数为零即不再被使用。

优点:实现简单、判定效率高。

不足:很难解决对象之间的循环引用问题。

可达性分析算法 Reachability Analysis

主流实现中,通过可达性分析判断对象是否存活。算法基本思想就是通过一系列
GC Roots
对象作为起始点,从这些节点开始向下搜索,搜索路径称为引用链,当一个对象到
GC Roots
没有任何引用链相连时(即
GC Roots
到这个对象不可达),则此对象不可用。

在Java语言中,可以作为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象

方法区中类静态属性引用的对象

方法区中常量引用的对象

本地方法栈中JNI(即一般说的Native方法)引用的对象

总结就是,方法运行时方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象。

四种引用

Java从1.2版本对引用的概念进行扩充,引入了4种引用,这4种引用的级别由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用。

强引用(StrongReference):强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

软引用(SoftReference):如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用(WeakReference):弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用(PhantomReference):“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引
4000
用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

finalize()方法

宣告一个对象死亡至少需要经过两次标记,可达性分析发现对象不可达时进行第一次标记,并筛选是否有必要执行finalize()方法。有必要执行的话对象会放到F-Queue队列之中,等待Finalizer线程执行,但是不保证一定会执行。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC会对F-Queue进行第二次标记,如果需要拯救自己只需要与引用链上的对象建立关联即可,如将this指针赋值给类变量或对象成员。

任何一个对象的finalize()方法最多只会执行一次。尽量避免使用finalize()方法,使用try-finally会做得更好、更及时。

回收方法区

虚拟机规范不要求实现方法区垃圾回收,而且回收效率较低。永久代垃圾回收分为两部分:废弃常量和无用的类。

判定一个类是无用的类:

该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例。

加载该类的ClassLoader已被回收。

该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

在大量使用反射、动态代理、CGLib、OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,保证永久代不会发生OOM。

垃圾收集算法

标记清除算法 Mark-Sweep

算法分为两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收。该算法存在两点不足:一是效率问题,标记和清除两个阶段效率都不高;另一个是空间问题,标记清除之后会产生大量的内存碎片。

复制算法 Copying

将内存按照容量会分为相等的两块,每次使用一块,一块用完了就将活着的对象复制到另一块,然后将使用过的内存空间一次清理掉。实现简单,运行高效,但是需要浪费一部分内存空间。

现代商业虚拟机都是采用该算法回收新生代,将内存分为
Eden:From Survivor:To Survivor=8:1:1
三部分,每次使用Eden和一块Survivor空间。新生代的可用容量为90%,10%的空间被浪费,当新生代的回收后有多余10%的对象存活时,需要由老年代进行担保分配。

标记整理算法 Mark-Compact

对象存活率较高时,复制算法效率较低,且需要分配额外的担保空间,不适合老年代。老年代通常采用标记整理算法,过程与标记清除算法类似,但是后续步骤不是直接清理可回收对象,而是将所有存活的对象移到一端,然后直接清理掉端边界以外的内存。

分代收集算法

将Java堆分为新生代和老年代,新生代采用复制算法,老年代采用标记清除或标记整理算法。

HotSpot的算法实现

枚举根结点:可达性分析必须在一个能够确保一致性的快照中进行,一致性意味着在分析期间整个执行系统必须停留在某个时间点上,称为“Stop The World”。

安全点:HotSpot会在特定位置记录下OopMap(Ordinary Object Pointer)信息,这些位置称为安全点(Safepoint),程序执行只有到达安全点时才能停下来执行GC。只有方法调用、循环跳转、一场跳转等这些功能的指令才会产生安全点。GC时如何让线程跑到安全点停下来,有两种方案:抢先式中断和主动式中断。

抢先式中断不需要线程的执行代码主动配合,在GC发生时,先把所有线程中断,如果有不在安全点的线程就恢复该线程,让它跑到安全点上。

主动式中断的思想时当GC需要中断线程时,不直接对线程操作,而是设置一个标志,各个线程主动去轮询这个标志,发现中断标志为真时自己中断挂起。轮询标志的地方与安全点是重合的,再加上创建对象需要分配内存的地方。

安全区域:当线程处于Sleep或Blocked状态时,无法响应JVM的中断请求,跑到安全点中断挂起,此时需要安全区域(Safe Region)来解决。安全区域是指在一段代码之中,引用关系不会发生变化,在这个区域的任何地方开始GC都是安全的,可以看作是扩展的安全点。当线程执行到安全区域中的代码时,首先标识自己进入了安全区域,离开时需要检查系统是否已经完成了根结点枚举(或者整个GC过程),如果没有完成就必须等待,知道接收到可以安全离开Safe Region的信号为止。

垃圾收集器



Serial收集器:单线程,Stop The World,简单高效。

ParNew收集器:Serial多线程版本,除了Serial只有它能和CMS收集器配合工作。

Parallel Scavenge收集器:新生代并行多线程收集器,关注点在于达到一个可控制的吞吐量。

Serial Old收集器:Serial的老年代版本,配合Parallel Scavenge使用,作为CMS收集器的后备方案。

Parallel Old收集器:Parallel Scavenge的老年代版本,与Parallel Scavenge配合工作。

CMS收集器:Concurrent Mark Sweep收集器以获取最短停顿时间为目标的收集器。分为四个步骤:初始标记、并发标记、重新标记、并发清除。初始标记和重新标记需要Stop The World。

优点:并发收集,低停顿。

缺点:CMS对CPU资源敏感,无法处理浮动垃圾(标记过程之后产生的垃圾),内存空间碎片。

G1收集器:并行并发、分代收集、空间整合、可预测停顿。分为四个步骤:初始标记、并发标记、最终标记、筛选回收。

内存回收与分配策略

对象优先在Eden分配:大多数情况下对象在Eden中分配,当Eden没有足够空间进行分配时,虚拟机会触发一次Minor GC。

大对象直接进去老年代:需要大量连续内存空间的Java对象,如很长的字符串以及数组对象,大对象对虚拟机分配是一个坏消息,应该尽量避免。

长期存活的对象将进入老年代:新生代对象每活过一次Minor GC,年龄加1岁,默认到达15岁时进入老年代。

动态对象年龄判断:在Survivor空间中相同年龄所有对象的大小的总和超过Survivor空间的一半时,大于等于该年龄的对象进入老年代。

空间分配担保:Minor GC前会先检查老年代的最大可用连续空间是否大于新生代所有对象总空间,若成立则Minor GC可以确保是安全的。否则,虚拟机会检查是否允许担保失败。若允许则检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,若大于则尝试进行一次Minor GC(有风险);弱小于或者设置不允许冒险,则会进行一次Full GC。

Minor GC 与 Full GC

Minor GC:指发生在新生代的垃圾收集动作,非常频繁,回收速度也比较快。

Full GC:指发生在老年代的垃圾收集动作,出现了Full GC一般会伴随一次或多次Minor GC(非绝对),一般Full GC比Minor GC慢10倍以上。

虚拟机常用参数

参数作用
-verbose:[classgc
-X输出非标准选项的帮助
-Xloggc:将 GC 状态记录在文件中 (带时间戳)
-Xbatch禁用后台编译
-Xms设置初始 Java 堆大小
-Xmx设置最大 Java 堆大小
-Xss设置 Java 线程堆栈大小
-XX:PermSize初始MethodArea的大小
-XX:MaxPermSize最大MethodArea的大小
-XX:+PrintGCDetails打印内存回收日志
-XX:SurvivorRatioEden区与一个Survivor区的空间比
-XX:PretenureSizeThreshold令大于这个设置值的对象,直接在老年代中分配。参数大小Byte为单位
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: