读《深入理解java虚拟机》小结
之所以学习 jvm ,是因为在学习多线程相关知识时,对 volatile 关键字理解的不够透彻,总有种似懂非懂的感觉。于是通过在网上各种资料的查阅,最终将 volatile 和 jvm 联系上了,本身又对jvm充满好奇,因此踏上了 jvm 的学习之旅。
一、一个问题
Q:GC 是在什么时候,对什么东西,做了什么事情?
二、五张图
看了多遍该书以后,个人觉得可以将书中大部分内容通过以下书中的五张图来进行联想
-
运行时数据区
-
HotSpot 虚拟机的垃圾收集器
-
双亲委派机制
-
栈帧
-
JMM
2.1 运行时数据区

-
程序计数器可以看作是当前线程所执行的字节码的行号计数器。每个线程都需要有一个独立的程序计数器,以保证线程切换后能恢复到正确的执行位置,属于线程私有的内存。是唯一一个java虚拟机规范中没有任何 OutofMemoryError 情况的区域。
-
虚拟机栈描述的是 java 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧。也是线程私有的,其生命周期与线程相同。在 java 虚拟机规范中,对这个区域规定了两种异常情况:
-
线程请求的栈深度 > 虚拟机所允许的深度 ==> StackOverflowError
-
如果虚拟机栈可以动态扩展,那么扩展时无法申请到足够的内存 ==> OutofMemoryError
-
-
本地方法栈与虚拟机栈发挥的作用是相似的,只是虚拟机栈为虚拟机执行 java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会抛出上面的两种异常。
-
堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,唯一目的就是存放对象实例。java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。如果堆中没有内存完成实例分配,并且堆也无法在扩展时,将会产生 OutofMemoryError。
-
从 GC 角度来看,由于现在收集器基本都采用分代收集算法,堆还可以细分为:新生代、老年代
-
从内存分配角度看,堆中可能划分出多个线程私有的分配缓冲区
-
-
方法区也是被所有线程共享的一块内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2.2 HotSpot 虚拟机的垃圾收集器

-
Serial收集器是一个单线程的收集器,在进行垃圾收集时必须暂停其他所有的工作线程,直到它收集结束。新生代采用复制算法。
-
ParNew收集器 是Serial收集器的多线程版本,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。新生代采用复制算法。
-
Parallel Scavenge收集器和ParNew收集器相似,不同点在于其关注的是达到 一个可控制的吞吐量。
吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。主要适合在后台运算而不
需要太多交互的任务。
新生代采用复制算法。注意的参数包括-XX:MaxGCPauseMillis、-XX:GCTimeRatio、-XX:+UseAdaptiveSizePolicy
-
Serial Old收集器是Serial收集器的老年代版本,使用“标记-整理”算法。
-
Parallel Old收集器 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
-
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记—清除”算法实现 的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:
-
初始标记(CMS initial mark)
-
并发标记(CMS concurrent mark)
-
重新标记(CMS remark)
-
并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要 “Stop The World” 。初始标记仅仅只是 标记一下 GC Roots 能直接关联到的对象,速度很快,并发标记阶段就是进行 GC RootsTracing 的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变 动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点有以下三点:
-
器对CPU资源非常敏感,CMS默认启动的回收线程数是(CPU数量 +3)/4,即当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降
-
器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次 Full GC 的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。注意参数-XX:CMSInitiatingOccupancyFraction
-
基于“标记—清除”算法实现意味着收集结束时会有大量空间碎片产生,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
注意参数-XX:+UseCMSCompactAtFullCollection、-XX:CMSFullGCsBeforeCompaction
-
-
G1收集器的运作大致可划分为以下几个步骤:
-
初始标记(Initial Marking)
-
并发标记(Concurrent Marking)
-
最终标记(Final Marking)
-
筛选回收(Live Data Counting and Evacuation)
如果你现在采用的收集器没有出现问题,那就没有任何理由现在去选择G1,如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试的选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。
-
垃圾收集器参数总结:

2.3 双亲委派机制

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
2.4 栈帧的概念结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
-
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
-
操作数栈是一个后入先出(Last In First Out,LIFO)栈,当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
-
动态连接,Class 文件的常量池中存有大量的符号引用,一部分将在每一次运行期间转化为直接引用,这部分则称为动态连接。
-
方法返回地址
2.5 java内存模型

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以互相类比,但此处仅是虚拟机内存的一部分)。每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
三、小结
通过 2.1 和 2.2,联想记忆 jvm 的自动内存管理机制,通过 2.3 联想记忆虚拟机类加载机制,通过 2.4 理解虚拟机字节码执行引擎,通过 2.5 学习并发和线程安全相关内容。
四、一个思考
是否能用一个对象在 jvm 中的生命周期把知识点串起来?

浙公网安备 33010602011771号