您的位置:首页 > 编程语言 > Java开发

深入理解Java虚拟机 JVM

2018-02-06 16:23 204 查看
1. Java内存模型

 


(1) 程序计数器(线程私有的)

相当于当前线程所执行的字节码的行号指示器。我们知道多线程是由上下文切换实现的,线程上下文切换时例如从A线程切换到B线程时,需要切换到线程B上一次被中断时的代码行,这时候就需要程序需计数器。

如果当前执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是一个JNI方法,这个计数器记录的值为Undefined。

(2) 虚拟机栈(线程私有的)

虚拟机栈描述了Java方法执行的内存模型,每个方法被执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接和方法出口,每一个方法执行到完成的过程就是栈帧从入栈到出栈的过程。

局部变量表:局部变量表用于存储方法参数和定义在方法体内的局部变量,包括各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference)和returnAddress。

操作数栈:进行运算的地方

动态链接:Java Class文件中有很多符号引用,一部分在类加载的时候转化为直接引用,另一部分在每一次运行期间转化为符号引用,这部分被称为动态链接

方法出口:当一个方法执行的时候,只有两种可以退出方法的方法。第一种是JVM碰到任意一个方法返回的字节码指令,被称为正常完成出口。另一种是在执行方法中抛出异常并且未对异常进行处理,被称为异常完成出口。方法退出的时候相当于把栈帧出栈。

(3) 本地方法栈(线程私有)

本地方法栈与虚拟机栈锁发挥的作用是相似的,不过本地方法栈是为虚拟机使用到的Native方法服务。

(4) Java堆(线程共享的)

堆是所有线程共享的一块内存区域,在虚拟机启动时创建,几乎所有的对象实例都在堆上分配。

堆分为新生代和老年代,新生代包括一个Eden,两个Survivor(From Survivor,To
Survivor),新建的对象首先会分配到Eden区,经过一次GC之后,存活的对象会被转移到Survivor区,Survivor区采用复制算法,当Survivor区的变量经历了15次GC之后

2. GC

2.1 判断对象是否可以被回收的方法

(1) 引用计数算法

当有一个地方引用对象的时候,引用计数就加1,任何时候引用计数器为0的时候就是不可以再使用的。缺点是无法解决循环引用的问题

(2) 可达性算法分析

可达性算法分析是通过一系列GC Roots为起点,当一个对象与GC Roots没有任何引用链的时候就被回收

可以作为GC Roots的对象

<1>本地方法栈中JNI引用的变量

<2>虚拟机栈中引用的变量

<3>方法区中类静态属性引用的变量

<4>方法区中常量引用的变量

 

2.2对象的引用

对象的引用分为强引用,软引用,弱引用,虚引用

(1) 强引用 Object o=new Object();

当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError,也不会随意回收这个对象,如果不使用这个1对象的时候,可以设置o=null

(2) 软引用

如果一个对象只有软引用,如果内存空间足够,就不会回收它,如果内存空间不够,就会回收这部分内存,软引用可以和一个引用队列联合使用,如果软引用所引用的对象被回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

(3) 弱引用

只要发现只有弱引用的对象就会立即回收,eg.ThreadLocalMap

(4) 虚引用

虚引用用来跟踪对象被垃圾回收器回收的活动。虚引用和软引用与弱引用的区别在于虚引用必须和引用队列联合使用,当虚引用被加入到引用队列的时候,说明这个对象已经被回收,可以在所引用的对象回收之后可以采取必要的行动。

2.3 GC算法

(1)标记清除算法:分为标记和清除两个阶段,首先先标记所有要回收的对象,在标记完成之后统一回收被标记的对象。缺点是效率低和会产生大量不连续的内存碎片

(2)复制算法:主要用于新生代的回收,把内存划分成大小相等的两块,每次只使用其中的一块,每次把GC后存活的对象复制到另一块上,然后清理已经使用过的内存。优点是效率高和实现简单

(3)标记整理算法:主要用于老年代的回收,先标记需要回收的对象,然后将存活的对象移向一端,然后直接清理掉边界外的内存

2.4 GC收集器

(1)Serial/Serial Old,单线程收集器

(2)Parallel/Parallel Old
吞吐量收集器

(3)ParNew Serial的多线程版本

(4)CMS收集器 并发收集器

 GC分为4个步骤

 <1>初始标记

 <2>并发标记

 <3>重新标记

 <4>并发清除

CMS的优点:低停顿

CMS的缺点:

<1>CMS对CPU资源很敏感,在并发阶段,会占用一部分资源导致应用程序变慢

<2>CMS不能处理浮动垃圾,所谓的浮动垃圾就是在并发标记过后新的垃圾

<3>CMS用标记清除算法,会有大量的碎片

(5) G1收集器

2.5Minor GC和Full GC什么时候发生

(1)Minor GC:Eden申请空间失败时,就会触发Minor
GC

(2)Full GC

 <1> 老年代空间不足

 <2> 永久代空间不足

 <3> System.gc()被显式调用

2.6对象的定位访问

(1)句柄访问(间接)

 在Java堆中划分一块内存作为句柄池,reference中存储的是对象在句柄池中等地址,得到了句柄池的地址就可以知道对象的实例数据和类型数据的位置。

 


(2)直接指针访问

 reference中存储的直接就是对象的实例数据地址,而实例数据中自己有一个指针存储对象类型数据的地址(方法去中),不需要reference来存储

 


3. 类加载

3.1类加载的过程:加载,验证,准备,解析,初始化

(1)加载:先根据类的全限定名找到定义这个类的二进制文件流,然后把这个静态的结果转化成方法区的运行时结果,最后在方法区里面生成这个类的Class对象,作为类访问的入口

(2)验证:包括文件格式验证,元数据验证,安全性验证等

(3)准备:为静态属性设置初始值

(4)解析:把符号引用转化为直接引用。符号引用是无歧义的可以定位到这个目标的字面量,直接引用是直接指向目标的指针,句柄等,是一个直接在内存中的位置

(5)初始化,就是执行<clinit>()的过程,clinit()的内容就是类变量和静态语句块的赋值,静态语句块中的变量只能赋值不能访问

4. 双亲委派模型

某个特定的类加载器在接到类加载的请求时,先把加载的任务委托给父类加载器,如果父类加载器可以加载,就可以成功返回。如果父类加载器无法加载时,子类加载器才进行加载。

启动类加载器 扩展类加载器
应用程序类加载器

protected synchronized Cl
c258
ass<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{

    //check the class has been loaded or not

    Class c = findLoadedClass(name);

    if(c == null){

        try{

            if(parent != null){

                c = parent.loadClass(name,false);

            }else{

                c = findBootstrapClassOrNull(name);

            }

        }catch(ClassNotFoundException e){

            //if throws the exception ,the father can not complete the load

        }

        if(c == null){

            c = findClass(name);

        }

    }

    if(resolve){

        resolveClass(c);

    }

    return c;

}

 

5. 静态分派和动态分派

首先明确一个概念,对于以下代码

Human man=new Man();

在这里对于创建的对象,Human是对象的静态类型,Man是对象的实际类型。静态类型是在编译器期间虚拟机就清楚的,而实际类型在编译期间虚拟机并不知道,只有在运行时候才决定

(1) 静态分派(针对重载)

对于重载的方法,编译器会根据参数的静态类型,在编译的时候就确定要调用哪一个方法。

(2) 动态分派(针对重写)

对于Human man=new Man(); man.say(),编译时的函数类型是Human的。当要执行这个方法的时候,用invokevirtual进行调用,会找到这个类的实际类型,即Man。由于动态分派是非常繁琐的动作,因此虚拟机基于性能的考虑,会建立一个虚方法表,来提高性能。

 


虚方法表中存放各个方法的实际入口地址,如果某个方法在子类没有重写,子类的虚方法表的入口和父类的入口一致,如果子类重写了这个方法,那么子类方法表中的地址会被替换成子类实现的入口地址。

 

6. JVM在编译期的优化

(1) 编译器早期优化

 <1>条件编译:例如根据布尔值把分支中不成立的部分去掉

 <2>泛型擦除

(2) 编译器晚期优化

 <1>热点代码优化

 <2>公共子表达式消除

 <3>方法内联:把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: