前几天看了周志明的《深入理解JAVA虚拟机》,记录一个大纲,用以复习和备忘。
一、运行时内存区域
包含5个:Method Area, VM Stack, Native Method Stack, Heap, Program Counter Register
1、程序计数器
线程私有,各线程计数器独立存储,互不影响;
JAVA方法:指向虚拟机字节码地址;
NATIVE方法:计数器值为空;
没有OOM的情况;
2、Java虚拟机栈
线程私有,生命周期和线程相同;
方法开始执行时创建栈帧,方法结束后出栈;
栈帧包含局部变量表、动态链接、方法出口等;
局部变量表:存放编译器可知的基本数据类型、对象引用、返回地址;在编译器分配好确定内存,不会再改变;
重点:两种异常情况
①线程请求的栈深度大于虚拟机允许的深度:StackOverflowError
②虚拟机栈可以动态扩展,如果要扩展时无法申请足够的内存,则OutOfMemoryError
3、本地方法栈
和虚拟机栈类似,不过执行的是Native方法,也会抛出那两种异常;
4、Java堆
被所有线程共享;
唯一目的是存放对象实例;
是垃圾回收的主要区域,也被称呼为GC堆;
采用分代收集算法,Java堆分为新生代和老年代;
可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer)
可以处于物理上不连续的空间,可扩展;
如果没有内存完成实例分配,并且堆无法再扩展时,抛出OutOfMemoryError
5、方法区
线程共享,存储已被加载的类信息、常量、静态变量;
这部分的垃圾回收少见,但是是必要的;
异常:当无法满足内存分配,抛出OutOfMemoryError
6、运行时常量池
Runtime Contant Pool 是方法区的一部分;
String.intern()方法将新的常量动态放入常量池
7、直接内存
直接在虚拟机外分配的内存空间,比如NIO类就用到了。
二、hotspot虚拟机的对象管理
1、对象创建
检查常量池-先加载没有加载的类-分配内存
内存大小在类加载后就确定,按照堆内内存分布不同,分配内存方式分为:
连续空间:“指针碰撞”、交错空间:“空闲列表”
分配内存可能会有并发问题;
分配好内存后进行初始化;
2、对象的内存布局
分为:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding),对象大小必须是8字节的整数倍;
3、对象的访问定位
通过栈上的reference数据来操作对象
4、会抛出OOM的情况
java堆溢出、方法区溢出、直接内存溢出
三、垃圾收集
1、判断对象是否存活
1、引用计数算法
给对象设置一个计数器,当引用时+1,引用失效时-1;
主流JVM没有用这个方法,因为难以解决对象间循环引用的问题。
2、可达性分析算法
通过判定是否通过GC Roots可达来判定对象是否存活。
可作为GC Roots的对象:栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象
3、关于引用
分为强引用、软引用、弱引用、虚引用,强度依次减弱
强引用:Object obj = new Object()这种,永远不会回收被引用的对象
软引用:有用但并不必要,在OOM之前会回收这些内存,还不够才抛出OOM
弱引用:描述非必须对象,GC工作时会回收掉只被弱引用关联的对象
虚引用:唯一作用是对象被回收时给予反馈
2、垃圾收集算法
1、标记-清除
最基础的算法,效率不高,并且会产生大量不连续的内存碎片。
2、复制算法
将内存分成两块,当一块内存用完,则将存活的对象复制到另外一块,然后清理这块的内存。多采用这种方法回收新生代,适用于对象存活率很低的。
3、标记-整理
和标记清除相似,但是会先将存活的对象移动到一起,适用于老年代对象。
4、分代收集
根据对象的存活周期分,新生代和老年代。
3、HotSpot的算法实现
1、枚举根节点
根据OopMap来得知存放引用的位置,而不需要遍历,否则会消耗大量时间
2、安全点
就是令线程都暂停执行,用来生成OopMap。
在特定的位置生成OopMap,此称为安全点,比如方法调用、循环跳转、异常跳转这些指令会生成安全点。
中断线程可以抢占式也可以是非抢占式(主动),几乎都用后者,让线程自己在安全点停下来。
3、安全区域
对于没有在执行的线程(Sleep或者Blocked的),需要保证在GC时不干扰,引用关系不能被改变。
Safe Region可以看做扩展了的Safe Point。
4、垃圾收集器
1、Serial收集器
单线程、古老,方式就是,先暂停所有线程,然后新生代采用复制算法,老年代采用标记-整理算法(Serial-Old)。
Serial仍然是如今Client模式下默认的新生代收集器。
2、ParNew收集器
多线程,其他与Serial相似,能够与CMS(老年代收集器)配合完成GC,默认线程数和CPU数相同。
3、Parallel Scavenge收集器
关注吞吐量(用户代码运行时间/用户代码运行时间+垃圾收集时间),还是使用复制算法的新生代收集器,并行多线程。
4、Serial Old收集器
单线程,标记-整理算法。
5、Parallel Old收集器
多线程、标记-整理算法,与上面的新生代收集器配合,适用于注重吞吐量、对CPU资源敏感的场合。
6、CMS收集器
CMS:Concurrent Mark Sweep,应用标记-清除算法,作用于老年代。过程包括:
①初始标记:标记GC Roots能直接关联的对象,耗时短
②并发标记:进行GC Roots Tracking,与用户线程并发,耗时较长
③重新标记:修正并发标记期间因用户线程而造成的引用变动,耗时短
④并发清除:与用户线程并发,耗时较长
过程中,并发标记和并发清除可以和用户线程一起工作,而这两个过程的耗时是相对最长的,所以可认为GC与用户线程并发。
特点是并发与低停顿,缺点是:
1、占用CPU,导致并发时用户线程效率下降,尤其是CPU较少时。
2、不能清理“浮动垃圾”,即在“并发-清理”过程中新出现的垃圾,因此,在收集阶段还需要预留足够的空间使用户线程运行,需要设置一个参数:当老年代使用了“多少%”的空间开始CMS收集,调高阈值可以降低回收次数以提高性能。如果预留内存不够了,则出现"Concurrent Mode Failure"并启用Serial Old
3、会产生大量内存碎片,不利于大对象分配内存,CMS可以开关内存碎片整理(不并发)
7、G1收集器
Garbage-First,具有以下特点:
1、并行GC、与JAVA线程并发;
2、分代收集,不需要其他收集器配合;
3、使用复制算法和标记-整理算法的整合;
4、可预测的停顿时间,几乎达到实时了
运作过程:
1、初始标记
2、并发标记
3、最终标记
4、筛选回收(Live Data Counting and Evacuation)
5、内存分配和回收策略
1、新生对象优先在新生代Eden区分配
2、大对象(大量连续内存)直接进入老年代(写程序时应避免短命的大对象),可以设置直接进入老年代的大小参数:-XX:PretenureSizeThreshold,单位是字节
3、长期存活的对象将进入老年代。每个对象都有一个AGE参数。新生代经过第一次MinorGC还存活,且能被Survivor容纳,则进入其中,且AGE=1,之后每次MinorGC,AGE++,直到一个阈值后,晋升老年代:-XX:MaxTenuringThreshold
4、动态对象年龄判定。如果Survivor空间中,具有某相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于等于此的所有对象直接进入老年代。
5、空间分配担保。在MinorGC前,会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立,则为安全,不需要担保。否则,虚拟机会根据HandlePromotionFailure来进行担保与否,如果允许则先检查老年代最大可用连续空间是否大于历次晋升老年的对象的平均大小,然后以此经验来判断是否冒险进行MinorGC,或者不冒险而进行FullGC。总结,FullGC的条件是:HandlePromotionFailure为false、第二个大小比较为小。大部分的HandlePromotionFailure为真,避免FullGC过于频繁。但是JDK6过后没有这个判断了,直接进行两个大小比较,有一个为真就进行MinorGC,否则FullGC。