JAVA虚拟机之垃圾收集与内存分配策略

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/manongxiaomei/article/details/85617521

最近再看《深入理解JAVA虚拟机》周志明写的第二版。现将学习笔记分享出来,方便日后复习,理解有误的地方欢迎指正!

1、运行时数据区:

程序计数器:一块较小的内存空间,保存当前线程所执行的字节码的行号指示器。

java虚拟机栈:生命周期与线程相同,每个方法在执行的同事都会创建一个栈帧用于存储局部变量、操作数栈、动态链接、方法出口等信息。这个区域存在2种异常情况:①线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError.②虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,抛出OutOfMemoryError.

本地方法栈:虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Navtive方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

java堆:在虚拟机启动时创建,此内存区的唯一目的就是存放对象实例,别名GC堆

方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。别名非堆

运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用

直接内存:并不是虚拟机运行时数据区的一部分。避免在Java堆和Native堆种来回复制数据。

2、hotspot虚拟机对象

对象的创建:虚拟机遇到new时:①在常量池中定位这个类的符号引用,并检查这个符号代表的类是否被加载过②虚拟机为新生对象分配内存空间③采用CAS保证内存分配的原子性,或每个线程在java堆中预先分配一小块内存(本地线程分配缓冲)④保存类的元数据信息、对象的哈希码、对象的GC分代年龄信息设置

对象的内存布局:分为3块区域:对象头、实例数据和对象填充

对象头:①存储对象自身的运行时的数据②保存类型指针(对象指向它的类的元数据的指针)

实例数据:对象真正存储的有效信息

对象填充:不是必然存在的,没有特别的含义,仅仅起着占位符的作用。

对象的访问定位:主流的访问方式有使用句柄和直接指针两种(详细见书)

内存泄漏or内存溢出?

异常实践:

堆溢出:-Xms20m -Xmx20m

栈溢出:-Xss128k

     注:-Xss设置栈内存:表示给每个线程分配得大小,值设置得越大,多线程时oom的可能性越高。

方法区和运行时常量池溢出:在JDK1.6版本,常量池被分配在永久带。1.7常量池属于方法区的一部分

3、垃圾收集器与内存分配策略

①判断对象是否还存活(不再被任何途径使用的对象)

引用计数算法:给对象添加一个引用计数器,每次被引用+1,引用失效-1,计数器为0时就是不再被使用的,进行gc。优点:效率高,但是主流的虚拟机没有选用引用计数法来管理内存,因为很难解决对象和之间互相引用的问题。

可达性分析算法:通过GC Roots对象作为起始节点,向下搜索(搜索走过的路径称为引用链)对象,从GC Roots到这个对象不可达时,证明这个对象是不可用的。

注:在java中,可作为GC Roots的对象包括:

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

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

方法区中常量引用的对象

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

缺点:可达性算法必须保证在一致性的快照中进行,避免分析过程中对象的引用关系还在不断发生着变化。这点导致GC进行时必须停顿所有java线程(stop the world)

生存还是死亡(是否要在本次被回收):

不可达的对象会经历2次标记的过程:

<1>第一次标记且筛选(筛选条件:是否有必要执行finalize()方法)

<2>如果有必要执行,将对象放到F-Queue队列之中,稍后GC会对F-Queue队列中的对象进行第二次标记。如果次是对象与引用链上的对象关联,这个对象将会被移出“即将回收的集合”,否则就会被执行GC

②垃圾收集算法

标记-清除算法:先标记需要回收的对象,然后统一清除

缺点:效率不高(2个过程的效率都不高)、空间问题(清除之后会产生大量碎片)

复制算法:将可用内存按容量大小分为相等的2块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另一块上,然后把已用的内存空间一次性清理掉。(现在商业虚拟机都采用这种方式来回收新生代

通常将内存分为一块较大的Eden空间和2块较小的survivor空间,每次使用Enden和其中一块survivor。

E+S1->S2  E+S2->S1   HotSpot默认E和S的大小比例是8:1

如果另一块Survivor空间没有足够空间存放上一次新生代手机下来的存活对象,就通过分配担保机制进入老年代。

标记-整理算法:先标记需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

分代收集算法:新生代只有少量存活,选用复制算法。老年代存活率高,没有额外空间分配担保,选用标记整理算法。

③HotSpot算法实现

枚举根节点:HotSpot引用一组OopMap的数据结构,在类加载完成的时候,就把对象内什么偏移量上是什么类型的数据计算出来。在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

注:由于可达性算法对时间的敏感性,即使号称(几乎)不会发生停顿的CMS收集器中,枚举根节点也是要必须停顿的。

安全点:HotSpot并没有为每一条指令都生成OopMap,只是在安全点才记录这些信息,也就是说程序只有到达安全点时才能停顿下来开始GC.安全点的选定标准“是否具有让程序长时间执行的特征”。例如方法调用、循环跳转、异常跳转等具有长时间执行的功能的指令才会产生安全点。

如何让GC发生时,所有线程都跑到最近的安全点上停下来?

抢先式中断:把所有线程都中断,如果又不在安全点上的就恢复线程,让它跑到安全点上。(几乎没有虚拟机使用这种方式)

主动式中断:不直接对线程操作,在安全点和创建对象需要分配内存的地方设一个标志,各线程主动轮询这个标志,发现中断标识时自己中断挂起。

如果程序没有分配CPU时间(sleep或blocked),就无法响应JVM的中断请求。此时需要安全区域来解决。

安全区域:是指在一段代码片段中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。在线程执行到安全区域时,先给自己打个标识,如果这段时间JVM要发起GC,就不需要管已经打标识的线程。在线程要离开安全区域时,要先检查

系统是否已经完成了根节点枚举(或者整个GC过程),如果没完成就需要等待接受可以离开安全区域的信号。

④垃圾收集器

serial、parnew、parallel scavenge、serial old、parallel old、cms收集器

并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态

并发:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。

CMS收集器:基于“标记-清除”算法实现的。

<1>初始标记:stop the world,标记GC Root能直接关联到的对象,速度快

<2>并发标记:进行gc roots tracing

<3>重新标记:stop the world,标记并发标记期间程序运行导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般比初始标记阶段稍长,但是远比并发标记时间短。

<4>并发清除:

参数-XX:CMSInitiatingOccupancyFraction老年代内存空间百分比阈值触发CMS收集器进行GC

参数-XX:UseCMSCompactAtFullCollection开关(默认开始)在CMS要开始Full GC之前开启内存碎片的合并整理

参数-XX:CMSFullGCsBeforeCompaction用于设置执行多少次不压缩的Full GC后执行一次压缩(默认0,表示每次进入Full GC都进行碎片整理)

G1收集器特点:

并行与并发:利用多cpu多核的硬件优势

分代收集:保留分代收集的概念

空间整合:G1运作期间不会产生内存空间碎片

可预测的停顿:G1在后台维护一个优先级列表,优先回收价值最大的Region(garage-first)提高回收效率,可以建立可预测的停顿时间模型

G1收集器运作步骤:初始标记、并发标记、最终标记、筛选回收

理解GC日志:

①前面的数字:代表了GC发生的时间,从java虚拟机启动以来经过的秒数

②GC日志开头的 [GC 和 [Full GC 代表停顿类型,如果有Full 代表发生了stop the world

③[DefNew:不同收集器定义的新生代、老年代、永久代名字,表示GC发生的区域

④方括号内部:3324K -> 152K(3712K)  GC前该内存区域已使用容量->GC后改内存区域已使用容量(该内存区域总容量)

方括号之外:GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)

⑤0.0025825 secs 表示该内存区域GC所占用的时间,单位是秒

垃圾收集器参数总结:

内存分配与回收策略:

①对象优先在Eden分配

②大对象直接进入老年代:-XX:PretenureSizeThreshold参数可以设置令大于这个参数值的对象直接在老年代分配。

③长期存活的对象将进入老年代:-XX:MaxTenuringThreshold参数可以设置对象在新生代存活超过多少次进入老年代。

④动态对象年龄判定:如果survivor空间中相同年龄所有对象大小的综合大于survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代。

⑤空间分配担保:发生Minor GC之前会检查老年代的空间是否大于新生代,如果比新生代大就进行Minor GC,如果比新生代小就看HandlePromotionFailure设置是否允许冒险,如果允许冒险就检查老年代剩余的连续内存大小是否比历代进入老年代的内存大,符合就Minor GC,否则就Full GC。(见下面流程图)

注:

新生代GC(Minor GC):指发生在新生代的GC。执行频繁,回收速度较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,老年代GC一般比新生代GC慢10倍

猜你喜欢

转载自blog.csdn.net/manongxiaomei/article/details/85617521