Java虚拟机
通过软件模拟硬件,有自己想象中的硬件:处理器,堆栈,寄存器,还有相应的指令系统
java虚拟机的体系结构 java程序运行的过程
JVM寄存器
通过定义软件实现的jvm寄存器便可从中得到更多信息不必对栈或内存进行访问,提高运行速度,jvm设置了4个常用的寄存器:pc程序计数器
optop操作数栈顶指针
frame当前执行环境指针
vars指向当前执行环境中第一个局部变量的指针
所有寄存器均为32位。pc用于记录程序的执行。optop,frame和vars用于记录指向Java栈区的指针。
JVM栈结构
java栈是JVM存储信息的主要方法,当jvm得到一个java字节码后,便为该类字节码每个方法创建一个栈框架,以保存该方法的状态信息。每个栈框架包括以下:
局部变量:局部变量用于存储一个类的方法中所用到的局部变量。vars寄存器指向该变量表中的第一个局部变量.
执行环境:执行环境用于保存解释器对Java字节码进行解释过程中所需的信息。它们是:上次调用的方法、局部变量指针和操作数栈的栈顶和栈底指针。执行环境 是一个执行一个方法的控制中心
操作数栈:用于存储运算所需操作数及运算的结果
JVM碎片回收堆
Java类的实例所需的存储空间是在堆上分配的,解释器具体承担为类实例分配空间的工作.解释器在为一个实例分配完存储空间后,便开始记录对该实例所占用的内存区域的使用。一旦对象使用完毕,便将其回收到堆中,在SUN公司开发的Java解释器和Hot Java环境中,碎片回收用后台线程的方式来执行。这不但为运行系统提供了良好的性能,而且使程序设计人员摆脱了自己控制内存使用的风险.JVM内存区
常量缓冲池:常量缓冲池用于存储类名称、方法和字段名称以及串常量方法区:方法区则用于存储Java方法的字节码,静态变量
堆区:用于存放类的对象实例。
栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。
JVM生命周期
当java虚拟机启动后,有以下情况,jvm将结束生命周期执行System.exit()方法
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致java虚拟机进程终止
类的生命周期
加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,类的加载的最终产品是位于堆区中的Class对象加载.class文件的方式
1、从本地系统中直接加载
2、通过网络下载.class文件
3、从zip,jar等归档文件中加载.class文件
4、从专有数据库中提取.class文件
5、将Java源文件动态编译为.class文件
jvm的类加载器:
1)根类加载器(使用C++编写,程序员无法在Java代码中获得该类)
2)扩展加载器,使用Java代码实现
3)系统加载器(应用加载器),使用Java代码实现
2、用户自定义的类加载器
java.lang.ClassLoader的子类
用户可以定制类的加载方式
委派机制,具体加载器的使用参考我的另一篇博文: http://blog.youkuaiyun.com/senssic/article/details/8913884
连接
验证
当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等,这里的验证时为了避免有人恶意编写class文件,也就是说并不是通过编译得到的class文件。准备
准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:
基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
引用类型的默认值为null。
常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。
关于类的jvm默认值参考另一篇博文:http://blog.youkuaiyun.com/senssic/article/details/8903243
解析
这一阶段的任务就是把常量池中的符号引用转换为直接引用。比如:我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。初始化
在类的初始化阶段,java虚拟机执行类的初始化语句,为类的静态变量赋值,在程序中,类的初始化有两种途径:(1)在变量的声明处赋值。
(2)在静态代码块处赋值
public class Test
{
public static int a = 0;//第一种初始化
public static int b ;
static{
b=2;//第二种初始化,类的静态变量的初始化是有顺序的
}
}
类的初始化必须在主动使用后才会进行!!!
主动使用(六种) 方式:
–创建类的实例
–访问某个类或接口的静态变量,或者对该静态变量赋值
–调用类的静态方法
–反射(如Class.forName(“com.bzu.csh.Test”))
–初始化一个类的子类
–Java虚拟机启动时被标明为启动类的类(Java Test)
类初始化步骤:
(1)假如一个类还没有被加载或者连接,那就先加载和连接这个类
(2)假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类
(3)假如类中存在初始化语句,那就直接按顺序执行这些初始化语句
注:调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。初始化类需要先初始化其父类,接口则不需要(只有使用到其静态变量才会被初始化)。
关于类的加载顺序参考我的另一篇博文:http://blog.youkuaiyun.com/senssic/article/details/8914657
使用
使用初始化好的类,按照jvm的类字节码生成的jvm指令运行,不断创建和使用。卸载
当不再使用此类字节码时,将被垃圾回收回收,并卸载类位于jvm的信息jvm的垃圾回收机制
Java 的垃圾回收器要负责完成3 件任务:1.分配内存
2.确保被引用的对象的内存不被错误回收
3.回收不再被引用的对象的内存空间。
当垃圾回收器在进行回收操作的时候,整个应用的执行是被暂时中止(stop-the-world)的。这是因为垃圾回收器需要更新应用中所有对象引用的实际内存地址。不同的硬件平台所能支持的垃圾回收方式也不同。比如在多CPU 的平台上,就可以通过并行的方式来回收垃圾。而单CPU 平台则只能串行进行。JVM 中提供了多种垃圾回收方法以及对应的性能调优参数,应用可以根据需要来进行定制。
引用的分类
强引用(Object obj = new Object( );):永远不会回收软引用:发生内存溢出之前会回收
弱引用:下次垃圾回收时会被回收
虚引用:对生存时间不影响,无法通过它获得对象实例,出现的目的是被回收是能收到一个系统通知
判断对象是否“存活”的两种方式
(1)引用计数算法给对象加一个引用计数器,有一个地方引用它则 +1 ,当引用失效则 -1,当为 0 时则该对象不再使用
但是,这样的话会造成循环引用场景无法回收
(2)根搜索算法
通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径为引用链,当一个对象到GC Roots没有引用链相连(也就是从GC Roots到这个对象不可达——图论的话)时,判定为可回收对象
jvm基本垃圾回收算法
标记-清除(Mark-Sweep)
第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。它停止所有工作,收集器从根开始访问每一个活跃的节点,标记它所访问的每一个节点。走过所有引用后,收集就完成了,然后就对堆进行清除(即对堆中的每一个对象进行检查),所有没有标记的对象都作为垃圾回收并返回空闲列表。优点:实现起来很简单,可以容易地回收循环的结构,并且不像引用计数那样增加编译器或者赋值函数的负担
缺点:1.收集暂停可能会很长,在清除阶段整个堆都是可访问的,这对于可能有页面交换的堆的虚拟内存系统有非常负面的性能影响
2.每一个活跃的(即已分配的)对象,不管是不是可到达的,在清除阶段都是可以访问的。这会产生区域性问题并可以造成分配失败,即使看来有足够的 自由内存可用。此算法需要暂停整个应用,同时,会产生内存碎片。
复制(Copying)
把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。优点:每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题
缺点:需要两倍内存空间。
标记-整理(Mark-Compact)
第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放优点:避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
缺点:需要经常遍历整个堆
分代收集算法
把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,在新生代中使用复制算法,即Minor-GC,当一些对象经过多次的Minor-GC后还留在新生代,则会被搬移到老年代中。而老年代中使用标记-清理或标记-整理算法,即Major GC/Full GC。垃圾回收器
串行收集器
Serial收集器
是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。并行收集器
ParNew收集器
ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。Parallel Scavenge收集器
Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。并发收集器
CMS收集器
是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:①.初始标记(CMS initial mark)
②.并发标记(CMS concurrenr mark)
③.重新标记(CMS remark)
④.并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
G1收集器
G1(Garbage First)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。垃圾收集器参数总结
-XX:+<option> 启用选项-XX:-<option> 不启用选项-XX:<option>=<number>-XX:<option>=<string>
参数 | 描述 |
---|---|
-XX:+UseSerialGC | Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
-XX:+UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 |
-XX:+UseConcMarkSweepGC | 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。 |
-XX:+UseParallelGC | Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收 |
-XX:+UseParallelOldGC | 使用Parallel Scavenge + Parallel Old的收集器组合进行回收 |
-XX:SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1 |
-XX:PretenureSizeThreshold | 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代 |
-XX:UseAdaptiveSizePolicy | 动态调整java堆中各个区域的大小以及进入老年代的年龄 |
-XX:+HandlePromotionFailure | 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 |
-XX:ParallelGCThreads | 设置并行GC进行内存回收的线程数 |
-XX:GCTimeRatio | GC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效 |
-XX:MaxGCPauseMillis | 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效 |
-XX:CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70 |
-XX:+UseCMSCompactAtFullCollection | 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效 |
-XX:+CMSFullGCBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用 |
-XX:+UseFastAccessorMethods | 原始类型优化 |
-XX:+DisableExplicitGC | 是否关闭手动System.gc |
-XX:+CMSParallelRemarkEnabled | 降低标记停顿 |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m |
Client、Server模式默认GC
新生代GC方式 | 老年代和持久代GC方式 | |
---|---|---|
Client | Serial 串行GC | Serial Old 串行GC |
Server | Parallel Scavenge 并行回收GC | Parallel Old 并行GC |
Sun/oracle JDK GC组合方式
新生代GC方式 | 老年代和持久代GC方式 | |
---|---|---|
-XX:+UseSerialGC | Serial 串行GC | Serial Old 串行GC |
-XX:+UseParallelGC | Parallel Scavenge 并行回收GC | Parallel Old 并行GC |
-XX:+UseConcMarkSweepGC | ParNew 并行GC | CMS 并发GC 当出现“Concurrent Mode Failure”时 采用Serial Old 串行GC |
-XX:+UseParNewGC | ParNew 并行GC | Serial Old 串行GC |
-XX:+UseParallelOldGC | Parallel Scavenge 并行回收GC | Parallel Old 并行GC |
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC | Serial 串行GC | CMS 并发GC 当出现“Concurrent Mode Failure”时 采用Serial Old 串行GC |