Java虚拟机的运行机制,内存管理机制,垃圾回收机制和4种引用

本文章主要讲解的有以下几个点:

一、JVM是什么

二、JVM的运行机制

三、java的内存管理机制

四、java垃圾回收机制

五、java垃圾回收算法

六、java种4种引用

一、JVM是什么及作用

JVM是一个可以运行java代码的虚拟计算机。

Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。

java源代码首先经过编译后生成对应的.class字节码文件(每一个java源文件生成一个对应的.class字节码文件),生成的.class字节码文件由JVM中的解释器,也就是Java虚拟机中的字节码指令集….编译成特定机器上的机器码。

Java源文件—->编译器—->字节码文件—->Jvm—->机器码

所以,JVM最主要的作用是针对每个操作系统开发其对应的解释器,只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能够运行起来,这就是Java能一次编译,到处运行的原因。

JVM在Java程序开始执行的时候,它才运行,程序结束的时它就停止。

二、JVM的运行机制

1、JVM的结构体系

首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行(执行过程还包括将字节码编译成机器码),JVM执行引擎在执行字节码时首先会扫描四趟class文件来保证定义的类型的安全性,再检查空引用,数据越界,自动垃圾收集等。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存

类加载器分为启动类加载器(不继承classLoader,属于虚拟机的一部分;负责加载原生代码实现的Java核心库,包括加载JAVA_HOME中jre/lib/rt.jar里所有的 class);扩展类加载器(负责在JVM中扩展库目录中去寻找加载Java扩展库,包括JAVA_HOME中jre/lib/ext/xx.jar或-Djava.ext.dirs指定目录下的 jar 包);应用程序类加载器(ClassLoader.getSystemClassLoader()负责加载Java类路径classpath中的类)

类加载机制的流程:

1、加载:查找装载二进制文件,通过一个类的全限定名获取类的二进制字节流,并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口。

2、验证:为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。

3、准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配

4、解析:解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程

5、初始化:初始化阶段是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,也就是执行类构造器()方法的过程

三、java的内存管理机制

JVM将内存划分成了5个区,堆内存,栈内存,方法区,本地方法区,程序计数器

堆内存:

       1、存放对象本身,成员变量的内存

       2、内存是动态分配的(运行时分配内存)

       3、JVM只有一个堆内存,里面的数据时所有线程共享

       4、堆中的内存是由JVM GC回收的

       5、最容易产生OOM的

栈内存:

       1、存放方法中局部基础数据类型的变量和自定义对象的引用,记住是对象的引用,而对象本身是存放在堆内存的。

       2、一个线程对应一个栈,每个栈中的数据都是私有的,其它栈不能访问。

       3、一个栈可以有多个栈帧,一个栈帧对应一个方法的一次调用

       4、栈中的内存是自动释放的,方法结束后,里面创建的内存就会被自动回收

每调用一个方法时,都会为该方法分配一块栈帧内存,该栈内存主要用来存放此方法中定义的基本数据类型的变量和返回结果,如果自定义对象的话,该对象本身的内存肯定是在堆当中为其分配,只是该对象的引用是在这个栈内存中,方法执行完后,该栈内存也被回收了(出栈),定义的所有局部变量生命周期也就结束,内存直接自动释放,对象的引用也不存在了,所以,该方法中定义的引用类型的对象在堆内存就没有任何引用指向它,这样在GC扫描时,就会被当做垃圾回收处理掉,从而始放该对象所占用的内存

本地方法栈:

       本地方法栈的功能和JVM栈非常类似,用于存储本地方法(C/C++)的局部变量表,本地方法的操作数栈等信息。

方法区:

      方法区存储的是已经被虚拟机加载的数据:

     1、 线程共享

     2、存储的数据类型:类的信息,常量,静态变量,即时编译器编译后的代码,等。

程序计数器:

      在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

     JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了各条线程之间的切换后计数器能恢复到正确的执行位置,所以每条线程都会有一个独立的程序计数器。

java对象创建及初始化:

java对象创建之后,就会在堆内存拥有自己的一块区域,接着就是对象的初始化过程。对象一般通过构造器来进行初始化,构造器是一种与类名相同的没有返回值的特殊方法;如果一个类中没有定义构造函数,则系统会自动生成一个不接受任何参数的默认构造器;但是如果已经定义一个构造器(无论是否有参数),编译器就不会再自动创建默认构造器了;我们可以对构造函数进行多次重载(即传递不同数目或不同顺序的参数列表),也可以在一个构造器中调用另一个构造器,但是只能调用一次,并且必须将构造器放在最起始处,否则编译器会报错。

那么类成员初始化又是怎么做的呢?顺序是怎样的呢?java中所有变量在使用前都应该得到恰当的初始化,即使是方法的局部变量,如果不进行初始化就会发生编译错误;而如果是类的成员变量,即使你不进行初始化赋值,系统也是会给与其一个初始值的,例如char、int类型的初始值都是0,对象引用不进行初始化则默认为null。

类成员初始化顺序总结:先静态后普通再构造, 先父类后子类,同级看书写顺序

1.先执行父类静态变量和静态代码块,再执行子类静态变量和静态代码块
       2.先执行父类普通变量和代码块,再执行父类构造器(static方法) 
       3.先执行子类普通变量和代码块,再执行子类构造器(static方法) 
       4.static方法初始化先于普通方法,静态初始化只有在必要时刻才进行且只初始化一次。

注意:子类的构造方法,不管这个构造方法带不带参数,默认的它都会先去寻找父类的不带参数的构造方法。如果父类没有不带参数的构造方法,那么子类必须用supper关键子来调用父类带参数的构造方法,否则编译不能通过。

四、java垃圾回收机制

java采用自动管理内存,主要自动化管理了两个方面的内容:一个是自动给对象分配内存,一个是自动回收对象的内存,而这两个问题所涉及的内存区域就是Java内存模型中的区域。我们知道垃圾回收机制是Java语言一个显著的特点,其可以有效的防止内存泄露、保证内存的有效使用,从而使得Java程序员在编写程序的时候不再需要考虑内存管理问题

java的垃圾回收主要流程:当GC触发时(随时都可能触发),垃圾回收器会扫描整个内存区域,如果发现某一个对象没有任何引用指向它时,那GC就会将该对象所占用的内存回收掉,在GC触发期间,除了GC以外的线程都阻塞了,只有GC触发完成后,其他线程才会继续。

从这个流程中可以发现三个问题:

1、什么样的内存会被回收(对象是否可以被回收的两种经典算法: 引用计数法 和 可达性分析算法)

2、内存什么时候被回收    (在GC扫描时)

3、内存怎么被回收      (采用垃圾回收算法---下面会讲到)

方法区的回收:

方法区的内存回收目标主要是针对 常量池的回收 和 对类型的卸载。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;

  • 加载该类的ClassLoader已经被回收;

  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

如何确定一个对象是否可以被回收?

1、引用计数法:判断对象的引用数量

引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。

引用计数算法是垃圾收集器中的早期策略。在这种方法中,堆中的每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为 1。当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数加 1(a = b,则b引用的对象实例的计数器加 1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减 1。特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减 1。任何引用计数为0的对象实例可以被当作垃圾收集。

引用计数收集器可以很快的执行,并且交织在程序运行中,对程序需要不被长时间打断的实时环境比较有利,但其很难解决对象之间相互循环引用的问题。如下面的程序和示意图所示,对象objA和objB之间的引用计数永远不可能为 0,那么这两个对象就永远不能被回收。

public class ReferenceCountingGC {
  
        public Object instance = null;
 
        public static void testGC(){
 
            ReferenceCountingGC objA = new ReferenceCountingGC ();
            ReferenceCountingGC objB = new ReferenceCountingGC ();
 
            // 对象之间相互循环引用,对象objA和objB之间的引用计数永远不可能为 0
            objB.instance = objA;
            objA.instance = objB;
 
            objA = null;
            objB = null;
 
            System.gc();
    }
}

上述代码最后面两句将objA和objB赋值为null,也就是说objA和objB指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为 0,那么垃圾收集器就永远不会回收它们。

2、 可达性分析算法:判断对象的引用链是否可达

可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的,如下图所示。在Java中,可作为 GC Root 的对象包括以下几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象;

  • 方法区中类静态属性引用的对象;

  • 方法区中常量引用的对象;

  • 本地方法栈中Native方法引用的对象;

五、java垃圾回收算法

1、标记清除算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

标记-清除算法的主要不足有两个:

  • 效率问题:标记和清除两个过程的效率都不高;

  • 空间问题:标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2、复制算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

复制算法的优点和缺点:

优点:运行高效且不容易产生内存碎片

缺点:对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

3、标记整理算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

4、分代收集算法

对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对象也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久代三个模块,如下图所示:

1). 新生代(Young Generation)

  新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。新生代内存按照 8:1:1 的比例分为一个eden区和两个survivor(survivor0,survivor1)区,大部分对象在Eden区中生成。在进行垃圾回收时,先将eden区存活对象复制到survivor0区,然后清空eden区,当这个survivor0区也满了时,则将eden区和survivor0区存活对象复制到survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后交换survivor0区和survivor1区的角色(即下次垃圾回收时会扫描Eden区和survivor1区),即保持survivor0区为空,如此往复。特别地,当survivor1区也不足以存放eden区和survivor0区的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。


2). 老年代(Old Generation)

  老年代存放的都是一些生命周期较长的对象,就像上面所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。此外,老年代的内存也比新生代大很多(大概比例是1:2),当老年代满时会触发Major GC(Full GC),老年代对象存活时间比较长,因此FullGC发生的频率比较低。


3). 永久代(Permanent Generation)

  永久代主要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。

六、java中的4中引用类型

1、强引用

强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类引用。 只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。如果内存不足时,JVM宁愿抛出OutOfMemoryError内存溢出错误也不会回收强引用,如果想要JVM回收强引用类型的对象,将其引用更改为null,JVM会在合适的时间回收这个null引用队形。

2、软引用

软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在GC扫描过程中,只有当内存空间不足时,才会将软引用的对象回收掉。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

软引用适合做缓存,在内存足够是,直接通过软引用取值,无需从真实的来源中查询数据,可以显著的提升网站性能,当内存不足时,能让JVM进行回收,从而删除缓存,这时候只能从真实来源查询数据

3、弱引用

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

可以看出,被弱引用关联的对象,在调用垃圾回收以后就会被回收,

弱引用可以在回调函数上防止内存泄漏,因为回调函数是匿名内部类,一个非静态的内部类会默认地持有外部类的一个强引用,当JVM在回收外部类的时候,此时回调函数在某个线程里被回调的时候,JVM就无法回收外部类,造成内存泄漏。

4、虚引用

虚引用是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,若某个对象与虚引用关联,那么任何时候都可能被虚拟机回收,虚引用不能单独使用,必须配合引用队列一起使用​​​​​​​。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

当垃圾回收器准备回收一个对象时,如果发现它与虚引用关联.就会在他回收以前将这个虚引用加入到引用队列中,程序可以判断引用队列中是否加入了虚引用,来了解被引用的对象是否将要被回收,如果确实要被回收,就可以做一些回收之前的收尾工作。

发布了23 篇原创文章 · 获赞 19 · 访问量 2134

猜你喜欢

转载自blog.csdn.net/huyinda/article/details/104769688