java程序的生命史梗概——笔记整理

从开始到生命周期结束,包括:编译、类加载、运行、GC几个步骤;
一、编译
1、词法、语法分析
词法分析是将源代码的字符流变成Token集合,而语法分析是根据Token序列抽象构造语法数的过程。
2、填充符号表
是由一组符号地址和符号信息构成的表格。后期由于开发需要,新开发了许多的数据结构和算法。大致有以下这些:无序链表中的顺序查找、有序数组中的二分查找、二叉查找树、平衡查找树(主要是红黑树)、散列表等。
3、语义分析
主要是对结构上正确的源程序进行上下文有关性质的检查。主要有以下几步:标注检查、数据和控制流分析、解语法糖。java中的语法糖主要有泛型、变长参数、自动拆箱/装箱、便利循环等,JVM在运行时不支持这些语法,便一阶段还原到简单的基础语法结构。
4、字节码生成
此阶段中,前面各个步骤中生成的信息转化成字节码写到磁盘中,还会进行少量代码添加和转换的工作。
二、类加载
  类加载的过程是在虚拟机内存的方法区进行,涉及到虚拟机内存。虚拟机内存呗划分为:程序计数器、栈、本地方法栈、堆、方法区、直接内存。
1、程序计数器:较小的内存空间,JVM概念模型中,字节码解释器工作就是改变计数器的指来选取下一条需要执行的字节码指令。
2、栈:
用于存储局部变量表、操作数栈、动态链接、方法出口等信息。其中局部变量表存放了编译时期产生的各种基本的数据类型、对象引用。
3、本地方法栈
本地方法栈为虚拟机使用的Native方法服务,甚至有的虚拟机会将两者合二为一。
4、堆
JVM管理内存中的最大一块。唯一的目的是存放实例对象。也是垃圾收集管理的主要区域,从内存回收的角度看:现在垃圾收集器都采用分代收集算法(后面会详细介绍),所以Java堆还可以进一步细分:新生代和老年代,而新生代进一步细分:Eden空间、From Survivor空间、To Survivor空间。为了效率考虑,堆还可能划分为多个线程私有的分配缓冲区(TLAB)。无论如何划分,都与存放内容无关,无论哪个区域,存放的依然是对象实例,它们存在的目的只是为了更好的回收和分配内存而已。
5、方法区
方法区与堆一样,都是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。而运行时常量池是方法区的一部分,它主要用于存放编译期声明各种字面量和符号引用。
6、直接内存
直接内存并不是虚拟机运行时数据区的一部分,也是不Java规范中定义的内存区域,你可以简单理解为堆外内存,内存分配不受Java堆大小的限制但受整个内存大小的限制。
三、加载
1、在加载阶段,JVM需要完成三个步骤:首先通过类的全限定名来获取定义此类的二进制字节流,然后将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,最后在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据入口。在第一步获取二进制字节流中并没有明确指出从一个*.class文件中获取,规定的灵活性导致我们可以从ZIP(为JAR、EAR/WAR格式提供基础)包中获取,从网络获取(Applet),运行时计算生成(动态代理),其他文件产生(JSP文件生成的Class类),从数据库获取。
2、验证
了确保Class文件字节流中包含信息符合JVM的要求,因为Class文件的来源途径不一定中规中矩的从编译器产生,也有可能用十六进制编辑器直接编写Class文件。校验流程为文件格式校验、元数据验证、字节码验证
3、准备
正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都在方法区进行分配。
4、解析
解析过程无非就是对类或接口、字段、接口方法进行解析
5、初始化
类初始化阶段是类加载过程的最后一步,在准备阶段,变量已经赋过一次初始值,而在这一步,则会根据程序猿定制的要求进行初始化类变量和其他资源。在这个阶段就是执行前面编译字节码生成流程提到的<clinit>()方法的过程。虚拟机也保证在多线程环境下这个方法被同时调用时被正确的加锁、同步,保证只有一个线程去执行这个方法而其他线程阻塞等待。
四、GC
JVM是如何判断程序药丸的呢?这地方其实采用了可达性分析算法,这个算法的基本思路是通过一系列的称为“GC Roots”的对象作为起始点,从这个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(用图论话说,就是从GC Roots到这个对象不可达),则证明此对象不可用,这时候就被判定为可回收的对象。当我们已经知道要回收的对象何时触发垃圾收集呢?安全点,安全点就是一些让程序暂定执行从而进行GC的位置,由此我们很容易知道GC停顿的时间是垃圾收集的核心。所有的垃圾收集算法以及衍生出来的垃圾收集器无不围绕着尽量减少GC停顿时间产生的,现在最新的G1垃圾收集器可以建立可预测的停顿时间模型,有计划的避免在整个Java堆中进行全区域的垃圾收集。
主要的垃圾收集算法及对应的垃圾收集器
1、标记-清除算法
最基础的收集算法,算法分为标记和清除两个阶段:首先标记处所有要回收的对象,在标记完成之后统一回收所有被标记的对象。它最大的不足是效率不高,还会产生大量不连续的内存碎片,这样导致的问题当程序运行过程分配较大对象时,即使堆中还有足够的内存,但是无法找到足够的连续内存只能不得不触发一次GC操作。这地方对应的垃圾收集器是CMS收集器。
2、复制算法
复制算法是为了解决效率问题而生的,它可以将可用内存容量划分为大小相等的两块,,每次只使用其中一块,当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样每次会对整个半区进行GC,并且不会产生内存碎片等问题。现在的商业虚拟机大多采用这种算法来回收新生代,另外划分内存比例也不是1:1,像HotSpot默认Eden(一块Eden区)和Survivor(两块Survivor区)的大小比例为8:1,每次使用Eden和其中一块Surviovr区,也就是新生代中可用内存空间是整个新生代的90%,当回收时,将Eden和其中一块Survivor中还存活的对象一次性复制到另一块Survivor中,最后清理掉Eden和刚才用到的Survivor空间,细心的读者在这地方也许会有发现,如果复制过程那块没使用的Survivor不够用怎么办呢?这时候需要依赖老年代进行分配担保。
3、标记-整理算法
算法是应用在老年代垃圾回收的算法,因为老年代不像复制算法那样回收频率高,另外它还会浪费空间。标记-整理过程与标记-清除差不多,无非后续步骤不是直接对可回收对象进行清除,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这地方对应的垃圾收集器是Serial Old收集器、Parallel Old收集器。
4、分代收集算法
当前商业虚拟机都采用这种算法,它的思想就是我们前面提到的对堆内存区域进行分代,新生代和老年代,不同的区域采用不同垃圾收集算法。新生代用复制算法,老年代用标记-整理或标记-清除算法。
五、整理
举个例子回顾下整个流程,当我们new一个对象的时候,会经历什么呢?结合前面所说的,JVM遇到一个new指令时,首先去检查整个指令参数能否在方法区的常量池定位到一个类的符号引用,并且检查整个符号引用代表的类是否已被加载、解析和初始化过,如果没有,则必须先执行对应类加载过程。类加载检查通过后,接下来JVM将会为新生对象分配内存,这个过程是在堆中进行的,分配大小在类加载完成后就可以确定,如果堆内存是规整的,则采用指针移动对象大小相等距离即可,这种分配方式叫“指针碰撞”,如果是零散的,则JVM维护一个列表记录哪些内存可用,分配并更新列表记录,这种方式叫“空闲列表”,至于采用哪种方式,取决于我们前面提到的堆采用了哪种垃圾收集器决定的。划分完对象内存之后,虚拟机会进行必要的初始化操作,接下来需要对对象进行必要的设置,这些信息设置在对象头(类元数据信息、对象的哈希码、对象的GC分代年龄等等)里面,这些工作完成之后,一个新对象产生了,这地方其实还没结束,再下一步就是调用<init>()方法进行程序猿计划的对对象字段进行的赋值操作,最后设置栈中的引用指向这个堆中对象所在的内存地址(直接引用),这时候一个真正可用的对象已经产生了,至于后续对对象进行的各种操作及最后的死亡就是前面提到的字节码执行引擎啊GC。




猜你喜欢

转载自bigshirly.iteye.com/blog/2343517