此文仅为学习JVM时所做的摘抄笔记。书比较老旧,第一版,内容可能不全,往后会做补充。图源来自网络。
第二章 Java内存区域与内存溢出异常
-
2.2.1 程序计数器
- Java虚拟机的多线程通过线程轮流切换并分配处理器执行时间*(时间片)*
- 一个处理器只会执行一条线程中的指令,线程切换后为了能恢复到正确执行位置,所以每条线程都有一个独立的程序计数器,各个线程不影响,独立存储。
-
2.2.2 Java虚拟机栈
- Java虚拟机栈线程私有,生命周期与线程相同。每个方法执行时创建一个栈帧,存储局部变量表、操作栈、动态链接、方法出口等
- 每一个方法被调用到执行完成的过程,就对应一个栈帧在虚拟机入栈到出栈。
- 局部变量表所需的内存空间在编译期间完成分配,进入一个方法的时候,这个方法需要分配多大的局部变量空间是完全确定的,在运行期间是不会改变局部变量表的大小的。
- 线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常。
-
2.2.3 本地方法栈
- 虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。
- 这里我理解成操作系统里面的原语,不知道对不对,后面再进行矫正
-
2.2.4 Java堆
- Java虚拟机管理的内存中最大的一块。被所有线程共享的一块内存区域。在虚拟机启动时创建,这块内存区域用于存放对象实例。
- Java堆是垃圾回收器管理的主要区域。
- Java堆处于物理上不连续的内存空间。
- 既可以固定大小,也可以扩展。在无法扩展时,会抛出OutOfMemoryError异常。
-
2.2.5 方法区(别名NonHeap非堆)
- 与Java堆一样各个线程共享的内存区域,用于存储已经被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。
- 不需要连续的内存空间,可以选择固定大小或扩展,并且可以选择不实现垃圾回收。
- 这个区域的垃圾回收目标主要针对产量池的回收和对类型的卸载。
-
2.2.6 运行时常量池
- 运行时常量池是方法区的一部分。
- Class文件除了有类的版本、字段方法、接口等描述文件,还有常量池,用于存放编译期生成的各种字面量和符号引用,这部分的内容在类加载后存放到方法区的运行时常量池中。
- Java虚拟机对运行时常量池没有特别严格的细节要求。不会要求运行时常量池在每一个字节用于存储哪种数据上遵循任何规范。
- 运行时常量池和Class文件常量池相比,具备动态性。运行期间可以将新的常量放入运行时常量池。
-
2.2.7 直接内存
- 没理解这段。阅读到后面再查。
- 可能与通道(Channel)与缓冲区(Buffer)的IO有关。
-
2.3 对象访问
-
Object obj = new Object();
- Object obj 反映到Java栈的本地变量表中,作为reference类型数据出现。
- new Object() 反映到Java堆中,形成一块存储了Object类型所有实例数据值的内存。这块内存的长度不固定。
- Java堆中还必须包含能查到此对象类型数据(对象类型、父类、实现的接口、方法)的地址信息,这类信息保存在方法区中。
-
reference类型只规定了一个指向对象的引用,并未定义应该通过哪种方式去定位和访问具体位置,因此根据虚拟机实现的对象访问方式会有所不同。
-
通过句柄访问方式
- Java堆中划分一块内存作为句柄池
- reference中存储对象的句柄地址
- 句柄中包含了对象实例数据和类型数据的各自具体地址信息
-
直接指针访问方式
- Java堆中必须考虑如何放置访问类型数据的相关信息
- reference中直接存储对象地址
-
句柄访问的最大好处是:reference中存储稳定的句柄位置,在对象被移动(垃圾回收时移动对象)时只会改变句柄中的实例数据指针,reference本身不需要修改。
-
直接指针访问的好处是:速度快,节省了一次指针定位的时间开销。
-
-
第三章 垃圾回收器和内存分配策略
-
3.2.1 引用计数算法
- 不完全准确的描述:给对象添加一个引用计数器,每一个地方引用的时候,计数器值加1;当引用失效时,计数器值减1;任何时候计数器都为0的对象就是不可能再被使用的。
- 缺点是:难以解决对象之间互相循环引用的问题。
-
3.2.2 根搜索算法
- GC Root对象作为起点,从这个节点开始向下搜索,搜索所有走过的路径称为引用链,当一个对象到GC ROOTS没有任何引用链相连时,证明该对象不可用。
- 可作为GC ROOTS的对象包括以下
- 虚拟机栈 栈帧中的本地变量表 中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中产量引用的对象
- 本地方法栈中JNI Native方法 的引用对象
-
3.2.3 再谈引用
-
JDK1.2之前
- 如果reference类型的数据中存储的数值代表的另一块内存的起初地址,就称这块内存代表一个引用。
-
JDK1.2之后
- 强引用
- Object obj = new Object()
- 只要强引用还在,垃圾回收机制就不会回收被引用的对象
- 软引用
- 描述有用,但并非必须
- 在系统将要发生内存溢出异常的时候,才会将这些对象列入回收范围之中并进行二次回收
- 弱引用
- 描述非必需对象,比软引用弱
- 被弱引用关联的对象只能生存到下一次垃圾回收发生之前
- 虚引用
- 最弱的引用关系
- 对象是否有虚引用,不影响其生存时间。同时也无法通过虚引用来取得对象实例。虚引用的作用是在这个对象被回收的时候能够有一个系统通知
- 强引用
-
-
3.2.4 生存还是死亡?
- 一个对象的死亡,需要经历两次标记过程。
- 发现没有与GC Roots相连接的引用链,被第一次标记。
- 第二次进行筛选判断是否需要执行finalize()方法
- 没有必要执行的条件:
- 没有覆盖finalize()方法
- finalize()方法已经被调用
- 没有必要执行的条件:
- finalize()处理原理
- 对象在初始化的过程中会判断是否重写了finalize,方法是判断两个字段标志has_finalizer_flag和RegisterFinalizersAtInit。
- 如果重写了finalize,那就把当前对象注册到FinalizerThread的ReferenceQueue队列中。注册之后的对象就叫做Finalizer。方法是调用register_finalizer函数。此时java虚拟机一看当前有这个对象的引用,于是就不进行垃圾回收了。
- 对象开始被调用,FinalizerThread线程负责从ReferenceQueue队列中获取Finalizer对象。开始执行finalize方法,在执行之前,这个对象一直在堆中。
- 对象执行完毕之后,将这个Finalizer对象从队列中移除,java虚拟机一看对象没有引用了,就进行垃圾回收了。
- 任何一个对象的finalize()方法都只会被系统调用一次,当面临下一次回收的时候,finalize()方法不会被再次执行。
- finalize()可以用,但是不建议
-
3.2.5 回收方法区
- 永久代的垃圾收集主要回收:废弃常量和无用的类
- 废弃常量:与回收Java堆中的对象类似,假如一个常量没有任何一个对象引用,这个时候如果发生内存回收,且必要的话,这个常量就会被系统请出常量池,常量池中的类、接口、方法、字段的符号引用类似。
- 无用的类:同时满足:①该类所有实例都被回收,Java堆中不存在该类任何实例。②加载该类的ClassLoader已经被回收。③该类对应的java.lang.Class对象没有在任何地方被使用,在任何地方都没有通过反射来访问该类的方法。
- 大量使用反射、动态代理、CGLib等字节码框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,来保证内存永不溢出
-
3.3 垃圾收集算法
-
3.3.1 标记-清除算法
- 算法分为标记和清除两个阶段
- 首先标记出需要回收的对象,在标记完成后统一回收掉被标记的对象。
- 缺点:①标记和清除的效率不高。②标记清除后会产生大量空间不连续的内存碎片。空间不连续的碎片导致下一次程序运行过程中需要分配较大对象时无法找到足够的内存区域,而不得以提前触发另一次垃圾回收动作。
- 算法分为标记和清除两个阶段
-
3.3.2 复制算法
- 复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。
- 内存耗尽的时候,JVM将程序暂停,开启复制算法GC线程。GC线程将存活的对象全部复制到空闲区间,并按照内存地址进行排序,同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。
- 此时空闲区域于活动区域交换,而垃圾对象已经全部留在原来的区域。
- 缺点:①浪费内存。②假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。
-
3.3.3 标记-整理算法
- 过程上结合“标记-清理算法”和“复制算法” 的特点。标记出需要回收的对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
-
3.3.4 分代收集算法
- 根据对象的存活周期不同,将内存划分成不同的几块,一般把Java堆分成新生代和老年代,再根据各个年代的特点采用最适当的收集算法。
- 例如新生代每次垃圾回收都有大批对象死亡,可以选用复制算法。而老年代因为对象的存活率高,没有额外的空间进行分配,就必须使用标记-清理或标记-整理。
-
3.4 垃圾收集器
-
3.4.1 Serial收集器(单)
- 可能是新生代、单线程收集器,进行垃圾回收的时候会暂停其他所有的工作线程,直到结束。
-
3.4.2 ParNew收集器(多)
- 新生代收集器、Serial收集器的多线程版本。可以与CMS收集器配合工作。
-
3.4.3 Parallel Scavenge收集器(多)
- 新生代收集器、使用复制算法收集器、并行的多线程收集器。
- 目标是为了达到一个可控制的吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)
- 停顿时间短越适合与用户交互的程序,良好的响应速度能提升用户体验。高吞吐量可以最高效率地利用CPU时间,尽快完成程序的运算任务,适合在后台运行而不用太多交互的任务。
-
3.4.4 Serial Old收集器(单)
- Serial收集器的老年代版本、单线程收集器、标记-整理算法。
- 主要是被Client模式下的虚拟机使用
- 如果是Server模式下,能与Parallel Scavenge搭配使用,也能作为CMS收集器的后备预案。
-
3.4.5 Parallel Old收集器(多)
- Parallel Scavenge收集器老年代版本、多线程收集器、标记-整理算法
- 为了弥补新生代选择Parallel Scavenge,老年代使用Serial Old的尴尬(Parallel Scavenge无法和CMS收集器配合工作)
-
3.4.6 CMS收集器
- 以获取最短回收停顿时间为目标的收集器。并发收集、低停顿
- 重视服务的响应速度,希望系统停顿时间最短。
- 基于标记-清除实现的
- 初始标记:初始标记仅标注GC Roots能直接关联的对象,速度快。
- 并发标记:并发标记阶段就是进行GC Roots Tracing的过程
- 重新标记:重新标记阶段为了修正并发标记期间,因为用户程序正在运行而导致标记产生变动。
- 并发清除
- 初始标记和重新标记仍需要“stop the world”。
- 缺点:
- CMS对CPU资源敏感,并发阶段虽然不会导致用户线程停顿,但是因为占用了一部分线程,会导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是(CPU数+3)/4。当cpu不足四个时,会对用户程序造成很大影响。所以提出了增量式并发收集器。
- CMS收集器无法处理浮动垃圾
- 因为采用“标记-清除”算法,所以会产生大量的空间碎片
-
3.4.7 G1收集器
- 基于标记-整理算法实现的收集器,可以精准控制停顿时间
- 不同于之前的虚拟机将Java堆划分为“新生代”和“老年代”,G1收集器将整个Java堆划分成多个大小固定的独立区域,并跟踪区域内的垃圾堆积程度,在后台维护一个优先列表,每次根据收集时间,优先回收垃圾最多的区域。 -
3.5 内存分配与回收策略
- 自动内存管理最终解决两个问题:给对象分配内存以及回收分配给对象的内存
- 分配的规则不是百分之百固定,细节取决于使用哪一种垃圾回收器组合,和虚拟机当中与内存有关的参数配置 -
3.5.1 对象优先在Eden分配
- 大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机发起一次Minor GC
- 新生代GC:指发生在新生代的垃圾收集动作,因为Java对象大多数朝生夕灭,所以Minor GC频繁,回收速度较快。
- 老年代GC:指发生在老年代的垃圾收集动作,出现了Major GC经常会伴随至少一次Minor GC。Major GC的速度一般比Minor GC慢10倍以上。 -
3.5.2 大对象直接进入老年代
- 大对象指的是需要大量连续内存空间的Java对象
- 经常出现大对象容易导致内存还有不少空间的时候就提前触发垃圾收集以获取足够的空间来存放他们。
- 虚拟机提供了 -XX:PretenureSizeThreshold参数,使大于这个设置值的对象直接在老年代中分配,目的是避免Eden区以及两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)
- PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效。 -
3.5.3 长期存活的对象进入老年代
- 为了识别那些对象放在新生代,哪些对象放在老年代,以采用分代收集的思想管理内存。虚拟机给每个对象定义了一个年龄计数器。
- 一个对象在Eden出生,经历了一次Minor GC后仍然存活,并且被Survivor(幸存者)容纳的话,将被移动至Survivor空间,并将对象年龄设为1。对象在Survivor中每熬过一次Minor GC,年龄就增加1岁。
- 当年龄增加到一定程度的时候(默认15)就会晋升到老年代中。 -
3.5.4 动态对象年龄判定
- 不一定非要年龄增加到默认值15(MaxTenuringThreshold)的时候才能晋升老年代。
- 如果Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于该年龄的对象则可以直接进入老年代。 -
3.5.5 空间分配担保
- 发生Minor GC的时候,虚拟机会自动检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,只会进行Minor GC;如果不允许,则改为进行一次Full GC。