JVM原理

avatar
作者
猴君
阅读量:1

一、JVM类加载

1.1、jvm是

        Java Virtual Machine(Java虚拟机)。

1.2、类加载机制

        类加载机制是指将类的字节码文件所包含的数据读入内存,同时会生成数据的访问入口的一种特殊机制。

        虚拟机把Class文件加载到内存。
        并对数据进行校验,转换解析和初始化。
        形成可以虚拟机直接使用的Java类型,即java.lang.Class

1.2.1、装载

        查找和导入class文件。

        1、通过一个类的全限定名获取定义此类的二进制字节流。类加载器可以实现通过类全名来获取此类的二进制字节流这个动作,并且将这个动作放到放到java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类。
        2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
        3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

        在装载阶段完成之后,这个时候在内存当中,运行时数据区的方法区以及堆就已经有数据了。

  • 方法区:类信息,静态变量,常量
  • 堆:代表被加载类的java.lang.Class对象

1.2.2、链接

1.2.2.1、验证

        为了确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,并且还要求信息不会危害虚拟机自身的安全,导致虚拟机的崩溃。

        1、文件格式验证

        验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。这阶段的验证是基于二进制字节流进行的,只有经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面验证都是基于方法区的存储结构进行的。

        2、元数据验证

        对类的元数据信息进行语义校验(对Java语法校验),保证不存在不符合Java语法规范的元数据信息。

        3、字节码验证

        进行数据流和控制流分析,确定程序语义是合法的、符合逻辑的。对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。        

        4、符号引用验证

        这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。符号引用验证的目的是确保解析动作能正常执行。 

1.2.2.2、准备      

       为类的静态变量分配内存,并将其初始化为默认值。

       不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;

       不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

        进行分配内存的只是包括类变量(静态变量),而不包括实例变量,实例变量是在对象实例化时随着对象一起分配在java堆中的。通常情况下,初始值为零值,假设public static int a=1;那么a在准备阶段过后的初始值为0,不为1,这时候只是开辟了内存空间,并没有运行java代码,a赋值为1的指令是程序被编译后,存放于类构造器()方法之中,所以a被赋值为1是在初始化阶段才会执行。

1.2.2.3、解析

         解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
         解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。

        符号引用就是一组符号来描述目标,可以是任何字面量。 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

1.2.3、初始化

        初始化阶段是执行类构造器()方法的过程,通过程序制定的主观计划初始化类变量和其他资源,比如赋值。 

在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值
  • 使用静态代码块为类变量指定初始值

        必须把静态变量定义在静态代码块的前面。因为两个的执行是会根据代码编写的顺序来决定的。

JVM初始化步骤:

  • 假如这个类还没有被加载和连接,则程序先加载并链接该类
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句

1.2.4、使用

主动引用

只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种:

  • 创建类的实例,也就是new的方式

  • 访问某个类或接口的静态变量,或者对该静态变量赋值

  • 调用类的静态方法

  • 反射(如 Class.forName(“com.carl.Test”) )

  • 初始化某个类的子类,则其父类也会被初始化

  • Java虚拟机启动时被标明为启动类的类(JvmCaseApplication ),直接使用 java.exe 命令来运行某个主类

被动引用

  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化

  • 定义类数组,不会引起类的初始化

  • 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化的)

1.2.5、卸载

在类使用完之后,如果满足下面的情况,类就会被卸载:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

1.3、类加载器

1.3.1、概念

负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例的代码模块。

类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。

        一个类在同一个类加载器中具有唯一性(Uniqueness),而不同类加载器中是允许同名类存在的,这里的同名是指全限定名相同。但是在整个JVM里,纵然全限定名相同,若类加载器不同,则仍然不算作是同一个类,无法通过 instanceOf 、equals 等方式的校验。

1.3.2、分层

        用三种基础的类加载器做为我们的三种不同的信任级别。最可信的级别是java核心API类。然后是安装的拓展类,最后才是在类路径中的类(属于本机的类)

 1.3.3、类加载机制的三种特性

        ①全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

        ②父类委托,“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。    

“双亲委派”机制加载Class的具体过程是:

  1. ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器。

  2. 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给祖父类加载器。

  3. 依此类推,直到始祖类加载器(引用类加载器)。

  4. 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。

  5. 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。

  6. 依此类推,直到源ClassLoader。

  7. 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。

         ③缓存机制,将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

二、运行时数据区

2.1、常量池分类

静态常量池

        静态常量池是相对于运行时常量池来说的,属于描述class文件结构的一部分。

字面量符号引用组成,在类被加载后会将静态常量池加载到内存中也就是运行时常量池

        字面量 :文本,字符串以及Final修饰的内容

        符号引用 :类,接口,方法,字段等相关的描述信息。

运行时常量池 

当静态常量池被加载到内存后就会变成运行时常量池。

字符串常量池 

        字符串作为最常用的数据类型,为减小内存的开销,专门为其开辟了一块内存区域(字符串常量池)用以存放。

2.2、运行时数据区

2.2.1、方法区

        方法区是各个线程共享的内存区域,在虚拟机启动时创建。

        用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

        当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

2.2.2、堆

        Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。

        Java对象实例以及数组都在堆上分配。

2.2.3、虚拟机栈

        虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。     

        每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

        栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。

        每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。

2.2.4、程序计数器

        一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,这就需要在线程中维护一个变量,记录线程执行到的位置。

2.2.5、本地方法栈

        如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。

栈指向堆

        如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),就是典型的栈中元素指向堆中的对象。

方法区指向堆

         方法区中会存放静态变量,常量等数据。private static Object obj=new Object(),就是典型的方法区中元素指向堆中的对象。

三、对象的生命周期

创建阶段

        为对象分配存储空间

        开始构造对象

        从超类到子类对static成员进行初始化

        超类成员变量按顺序初始化,递归调用超类的构造方法

        子类成员变量按顺序初始化,子类构造方法调用,并且一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段。

应用阶段

        系统至少维护着对象的一个强引用(Strong Reference)

        所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))

引用的定义:

1.我们的数据类型必须是引用类型

2.我们这个类型的数据所存储的数据必须是另外一块内存的起始地址

1.强引用

        JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用

2.软引用

        软引用是用来描述一些还有用但是非必须的对象。对于软引用关联的对象,在系统将于发生内存溢出异常之前,将会把这些对象列进回收范围中进行二次回收。(当你去处理占用内存较大的对象 并且生命周期比较长的,不是频繁使用的)。

3.弱引用

        弱引用(Weak Reference)对象与软引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于Weak引用对象, GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收。

4.虚引用

        为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。也就是说,如果一个对象被设置上了一个虚引用,实际上跟没有设置引用没有任何的区别。

不可见阶段

        不可见阶段的对象在虚拟机的对象根引用集合中再也找不到直接或者间接的强引用,最常见的就是线程或者函数中的临时变量。程序不在持有对象的强引用。 (但是某些类的静态变量或者JNI是有可能持有的 )。

不可达阶段

        指对象不再被任何强引用持有,GC发现该对象已经不可达。

收集阶段(Collected)

        对象的finalize()函数执行完成后,对象仍处于不可达状态,该对象进程终结阶段。

对象内存空间重新分配阶段(Deallocaled)

        GC对该对象占用的内存空间进行回收或者再分配,该对象彻底消失。

如何确定一个对象是垃圾?

        1、引用计数法

        对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。

        2、可达性分析

        通过GC Root的对象,开始向下寻找,看某个对象是否可达

        

四、垃圾回收算法

4.1、标记清除

        标记:找出内存中需要回收的对象,并且把它们标记出来。此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时。

        清除:清除掉被标记需要回收的对象,释放出对应的内存空间。

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

4.2、标记复制

        将内存划分为两块相等的区域,每次只使用其中一块。 当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。空间利用率低。

4.3、标记整理

        标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。相对"复制算法"来讲,少了一个"保留区"。

        复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况,所以老年代一般不能直接选用这种算法。

        随机整理:对象的移动方式和它们初始的对象排列及引用关系无关。也就是说,不会根据他的引用关系来进行判定他的对象排列,不管引用关系靠的有多近。

        线性整理: 将具有关联关系的对象排列在一起比如A跟B有关系,就认为他们应该排列在一起,至于整体内存什么样子,不关注,有没有内存碎片也不关注。

        滑动整理: 对象“滑动”到堆的一端,从而“挤出”垃圾,可以保持对象在堆中原有的顺序。

4.4、分代收集

Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)。

Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)。

        

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!