Java虚拟机详细解析--JVM类加载过程+内存分配+GC算法+垃圾回收器分类

看了很多人的帖子,写的都很好,但是不够详细,从京东面试出来之后,本学渣突然把之前看过的梳理一下。希望能帮到有需要的朋友....

一、 JVM的类加载过程

        

1、类的加载

1)通过一个类的全限定名获取此类的Class文件。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。

2ClassLoader将这个class文件所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。

2、类的连接

类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的class文件合并入JREJava运行时环境)中。类的连接大致分三个阶段:

1)验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。

格式检查---------->魔数检查、版本检查、长度检查              

语义检查---------->是否继承final、是否有父类、抽象方法是否有实现

字节码检查------->跳转指令是否指向正确位置、操作数类型是否合理

符号引用验证---->符号引用的直接引用是否存在

2)准备:当一个类验证通过后,虚拟机就会进入准备阶段,在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。

                     

3)解析:将类的class文件中的符号引用换为直接引用。

3、类的初始化

类初始化是类加载的最后一步,除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码。

类的初始化的主要工作是为静态变量赋程序设定的初值:

static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100

Java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:

1)使用new字节码指令创建类的实例,或者使用getstaticputstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。

2)通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。

3)当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。

4)当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。

5)使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticRE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

注意,虚拟机规范使用了“有且只有”这个词描述,这五种情况被称为“主动引用”,除了这五种情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”。

 

二、JVM类加载器分类详解:

1Bootstrap ClassLoader:启动类加载器,也叫根类加载器,它负责加载Java的核心类库,加载如(%JAVA_HOME%/lib)目录下的rt.jar(包含SystemString这样的核心类)这样的核心类库。根类加载器非常特殊,它不是java.lang.ClassLoader的子类,它是JVM自身内部由C/C++实现的,并不是Java实现的。

2Extension ClassLoader:扩展类加载器,它负责加载扩展目录(%JAVA_HOME%/jre/lib/ext)下的jar包,用户可以把自己开发的类打包成jar包放在这个目录下即可扩展核心类以外的新功能。

3System ClassLoader\APP ClassLoader:系统类加载器或称为应用程序类加载器,是加载CLASSPATH环境变量所指定的jar包与类路径。一般来说,用户自定义的类就是由APP ClassLoader加载的。

4、类加载器的双亲委派加载机制(重点):当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

 

双亲委派模型的源码实现:

主要体现在ClassLoaderloadClass()方法中,思路很简单:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,调用自己的findClass()方法进行加载。

 public Class loadClass(String name) throws ClassNotFoundException {  

            return loadClass(name, false);  

    }  

    protected synchronized Class loadClass(String name, boolean resolve)  

        throws ClassNotFoundException {  

        // First, check if the class has already been loaded  

        Class c = findLoadedClass(name);  

        if (c == null) {  

            try {  

if (parent != null) {  

c = parent.loadClass(name, false);  

} else {  

c = findBootstrapClassOrNull(name);  

}  

            } catch (ClassNotFoundException e) {  

                    // ClassNotFoundException thrown if class not found  

                    // from the non-null parent class loader  

            }  

            if (c == null) {  

                // If still not found, then invoke findClass in order  

                // to find the class.  

                c = findClass(name);  

            }  

        }  

        if (resolve) {  

            resolveClass(c);  

        }  

        return c;  

}  

三、JVM的内存分配

          

线程共享区域为:(1java堆 (2)方法区

线程私有区域为:(1JVM栈 (2)本地方法栈 (3)程序计数器

 

1java堆:

堆是jvm内存管理中最大的一块,线程共享。在jvm启动的时候创建。此区域唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,这个区域垃圾回收频繁。

Java---------->新生代----------->Eden空间、From Survivor空间、To Survivor空间

 ----------->老年代

新生代中垃圾回收算法为复制算法:(小对象、minor GC

复制算法是先将内存分为连个部分,一部分用来放入对象,而另一部分暂时不用,当使用的一部分内存要进行垃圾回收的时候会将不需要回收的对象复制保存在另一个空间中,然后再对使用过的那部分区域进行垃圾回收。

优点:效率很高    

缺点:很浪费空间

解决方式:一般将新生代分为Eden空间和两个Survivor空间,其大小在HotSpot虚拟机中默认比例为811,这样在新生代中采用复制算法回收垃圾效率就很高了。

具体回收过程是将Eden区域和From Survivor区域作为对象的存储空间,当要进行垃圾回收的时候先将这两个区域中不需要回收的对象复制保存在To Survivor区域中,然后再进行垃圾回收。

另外有一点是当一个对象在Eden区域和From Survivor区域中存储的时候发现内存不足,这时会进行内存分配担保,就是将此对象直接存入在老年代中。

老年代中采用的GC算法为标记-清除算法或者标记-整理算法:(大对象、Full GC

标记-清除算法为:首先标记出要进行GC的对象,标记完成后再进行GC

缺点:这种算法效率不高,并且会产生很多内存碎片。

标记-整理算法:同样是先对要进行GC的对象进行标记,但是不同的是在标记完成后不是立刻执行GC,而是先将不需要GC的对象移动到一端,然后在边界外再对要回收的对象进行GC

关于对象的分配:对象优先在Eden区域分配,大对象会直接进入老年代,长期存活的对象会进入老年代,这里的长期存活是根据新生代中的对象年龄阈值来定义的,对象刚分配到新生代的时候年龄为1,每进行一次GC对象的年龄会加1HotSpot中默认的阈值是15,也就是说对象年龄达到15岁的时候会被分配到老年区,这个值是可以通过参数配置的。

在进行垃圾回收的时候新生代GC又叫minor GC,老年代GC可以设置内存容量达到百分比的多少的时候进行GCminor GC时间短,频率高;老年代的GC又叫Full GC,而Full GC时间长,频率低。

2、方法区---永生代

方法区又被称为永久区,线程共享,是用来存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。这个区域很少进行垃圾回收,回收目标主要是针对常量池的回收和对类型的卸载。

3JVM

    JVM栈是线程私有的,它的生命周期与线程相同。JVM栈描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表中存放了编译期可知的各种基本数据类型、对象的引用类型。局部变量表中需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

3、本地方法栈

本地方法栈和JVM栈非常相似,它们之间的区别不过是jvm栈是为执行java方法服务,而本地方法栈是为jvm使用到对的本地方法服务。

5、程序计数器

    程序计数器是一块较小的内存空间,线程私有。它可以看作是当前线程所执行的字节码的行号指示器。在jvm的概念模型里,字节码解释器工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的jvm字节码指令的地址;如果正在执行的是本地方法,这个计数器值则为空。

 

四、GC算法

jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的.

对象存活判断:

1)引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。

优点:此方法简单,执行效率高。

缺点:无法解决对象相互循环引用的问题。

2)可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

3)在Java语言中,GC Roots包括:

虚拟机栈中引用的对象。

方法区中类静态属性实体引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI引用的对象。

1、标记 -清除算法

“标记-清除”算法分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

缺点:

1)首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲,尤其对于交互式的应用程序来说简直是无法接受。试想一下,如果你玩一个网站,这个网站一个小时就挂五分钟,你还玩吗?

2)其次,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2、复制算法--------->主要适用于堆区中的新生代、老年代

复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的,当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。

缺点:

1)它浪费了一半的内存。

2)如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。

3、标记/整理算法

标记/整理算法与标记/清除算法非常相似,它也是分为两个阶段:标记和整理。

1)标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。

2)整理:移动所有存活的对象,且按照内存地址次序依次排列,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。

优点:

(1)当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

(2)标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。

缺点:

标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。

4、总结以上三种算法:

(1)三个算法都基于根搜索算法去判断一个对象是否应该被回收,而支撑根搜索算法可以正常工作的理论依据,就是语法中变量作用域的相关内容。因此,要想防止内存泄露,最根本的办法就是掌握好变量作用域。

(2)GC线程开启时,或者说GC过程开始时,它们都要暂停应用程序。

执行效率:复制算法>标记/整理算法>标记/清除算法(只是简单的对比时间复杂度)。

内存整齐度:复制算法=标记/整理算法>标记/清除算法。

内存利用率:标记/整理算法=标记/清除算法>复制算法。

5GC算法中的神级算法-----分代搜集算法

分代搜集算法是针对对象的不同特性,而选择适合的算法,这里面并没有实际上的新算法产生,它是对前三个算法的实际应用。

对象分类:

1)夭折对象:朝生夕灭的对象,通俗点讲就是活不了多久就得死的对象。例子:某一个方法的局域变量、循环内的临时变量等等。

2)老不死对象:这类对象一般活的比较久,岁数很大还不死,但归根结底,老不死对象也几乎早晚要死的,但也只是几乎而已。例子:缓存对象、数据库连接对象、单例对象(单例模式)等等。

3)不灭对象:此类对象一般一旦出生就几乎不死了,它们几乎会一直永生不灭,记得,只是几乎不灭而已。例子:String池中的对象(享元模式)、加载过的类信息等等。

对象对应的内存区域:

4)夭折对象和老不死对象都在JAVA堆,而不灭对象在方法区,因此分代搜集算法就是针对的JAVA堆而设计的,也就是针对夭折对象和老不死对象。

1-1)新生代------->夭折对象+比较适合复制算法      内存分配比-------->8:1:1

在新生代里的每一个对象,都会有一个年龄,当这些对象的年龄到达一定程度时(年龄就是熬过的GC次数,每次GC如果对象存活下来,则年龄加1),则会被转到年老代,而这个转入年老代的年龄值,一般在JVM中是可以设置的。

在新生代存活对象占用的内存超过10%时,则多余的对象会放入年老代。这种时候,年老代就是新生代的“备用仓库”。

2-2)老年代------->老不死对象+采用标记/整理或者标记/清除算法。

针对老不死对象的特性,显然不再适合使用复制算法,因为它的存活率太高,而且不要忘了,如果年老代再使用复制算法,它可是没有备用仓库的。

3-3)永久代-------->不灭对象(方法区的对象回收)

因为JAVA堆是GC的主要关注对象,而以上也已经包含了分代搜集算法的全部内容,接下来对于不灭对象的回收,已经不属于分代搜集算法的内容。不灭对象存在于方法区。这一部分区域的GC与年老代采用相似的方法,由于都没有“备用仓库”,二者都是只能使用标记/清除和标记/整理算法。

6、回收的时机

JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。

因此GC按照回收的区域又分了两种类型,一种是普通GCminor GC),一种是全局GCFull GC),它们所针对的区域如下。普通GCminor GC):只针对新生代区域的GC。全局GCFull GC):针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC

7、垃圾收集器

串行:Serial收集器------>新生代

1Serial收集器曾是一个单线程的收集器,在进行收集垃圾时,必须stop the world,它是虚拟机运行在Client模式下的默认新生代收集器。

2Serial OldSerial收集器的老年代版本,同样是单线程收集器,使用标记整理算法。

并行:ParNew收集器、Parallel Scavenge收集器

3ParNew收集器是Serial收集器的多线程版本,许多运行在Server模式下的虚拟机中首选的新生代收集器,除Serial外,只有它能与CMS收集器配合工作。

4Parallel Scavenge收集器也是新生代收集器,使用复制算法又是并行的多线程收集器,它的目标是达到一个可控制的运行用户代码跟(运行用户代码+垃圾收集时间)的百分比值。

5Parallel Old收集器是Parallel Scavenge收集器的老年代版本,用多线程和标记整理算法。

6Concurrent Mark Sweep收集器是一种以获得最短回收停顿时间为目标的收集器,基于标记清除算法。

过程:初始标记---->并发标记---->重新标记---->并发清除---->并发重置

优点:并发收集,低停顿

缺点:对CPU资源非常敏感,无法处理浮动垃圾,收集结束会产生大量空间碎片。

(7)G1收集器是基于标记整理算法实现的,不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First)。

7、垃圾回收事件与组合关系

 如果有时间会继续更新的。。。。。。

 

你可能感兴趣的:(Java虚拟机详细解析--JVM类加载过程+内存分配+GC算法+垃圾回收器分类)