Java 虚拟机

1、什么是Java虚拟机

 
我们常说的Java 虚拟机一般可以理解为以下三层意思:

  • 抽象的规范 .
  • 一个具体的实现 .
  • 一个运行中的虚拟机实例 .

        Java 虚拟机抽象规范仅仅是一个概念。该规范的具体实现,可能是来自多个提供商,并存于多个平台上。当运行一个Java程序的同时,也就是在运行一个Java虚拟机实例。

 

2、Java虚拟机的生命周期

 

        当启动一个Java程序时,一个Java虚拟机实例就产生了。当该程序关闭退出时,这个虚拟机实例就随之消亡。如果在同一台计算机上同时运行N个Java程序,将会产生N个Java虚拟机实例。每个Java程序都运行在自己的Java虚拟机实例中。

        一个运行时的Java虚拟机实例的作用就是,负责运行一个Java程序。

在这里插入图片描述

        Java虚拟机实例通过调用某个初始类的main() 方法来运行一个Java程序。并且这个main() 方法必须是公有的(public)、静态的(static)、返回值为void,并且接受一个字符串数组作为参数。任何拥有以上要求的main() 方法的类,都可以作为Java 程序运行的起点。


		public static void main(String[] args) {
		 // todo……
		}

 
在Java 虚拟机内部有两种线程:

  • 守护线程。
  • 非守护线程。

        守护线程通常是有虚拟机自己使用的,比如执行垃圾收集任务的线程。但是,Java程序也可以把它创建的任何线程标记为守护线程。由以上main() 方法开始的线程,是非守护线程。

        只要有任何一个守护线程在运行,那么该Java 程序就也在继续运行,Java 虚拟机也处于存活状态。当该程序中所有的非守护线程都终止时,虚拟机实例将自动推出。假若安全管理器允许,程序本身也能够调用Runtime 类或者 System 类的 exit() 方法来退出。

 

3、Java虚拟机的体系结构

 
        每一个Java 虚拟机都有一个类装载器子系统,它根据给定的全限定名来装入类型(类或接口)。同时,每一个Java 虚拟机也有一个执行引擎,它负责执行那些包含在被类装载的方法中的指令。

在这里插入图片描述

        每个Java虚拟机实例都有一个方法区和一个堆,它们是由该Java虚拟机实例中所有线程共享的。当虚拟机装载一个Class 文件时,它会从这个Class 文件包含的二进制数据中解析类型信息。然后,它把这些类型信息放到方法区中。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放在堆中。某些运行时数据区是由程序中所有的线程共享的,但是也有一部分是只能由一个线程拥有。

在这里插入图片描述

        当每一个新线程被创建时,它都会得到它自己的PC 寄存器(程序计数器),以及一个Java 栈。如果线程正在执行的是一个 Java 方法(非本地方法),那么PC 寄存器的值将总是指示下一条将被执行的指令,而它的 Java 栈则总是存储该线程中 Java 方法调用的状态 —— 包括它的局部变量,被调用时传进来的参数,它的返回值,以及运算的中间结果等等。而本地方法调用的状态,则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或者其他某些与特定实现相关的内存区中。

        Java 栈是由许多栈帧(帧)组成的。一个栈帧包含一个Java 方法调用的状态。当线程调用一个Java 方法时,虚拟机压入一个新的栈帧到该线程的Java 栈中;当该方法返回时,这个栈帧被从 Java 栈中弹出并抛弃。下图描述了Java 虚拟机为每个线程创建的内存区,这些内存区域是私有的,任何线程都不能访问另一个线程的PC寄存器或者 Java 栈。

在这里插入图片描述
 

3.1 关于类装载器

 
类加载器的作用:

        类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

类装载器

Java 虚拟机有两种类装载器,由不同的类装载器转载的类,将被放在虚拟机内部不同的命名空间中:

  • 启动类装载器,其是Java 虚拟机实现的一部分(系统提供)。
  • 用户自定义类装载器,其是Java 程序的一部分(开发人员编写)。

系统提供的类加载器主要有下面三个:
 
引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自java.lang.ClassLoader。
 
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
 
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

        用户自定义的类装载器是普通的Java对象,它的类必须派生自 java.lang.ClassLoader类。ClassLoader中定义的方法为程序提供了访问类装载器机制的接口。每一个被装载的类,Java虚拟机都会为它创建一个java.lang.Class 类的实例来代表该类。用户自定义的类装载器以及Class 类的实例都放在内存中的堆区,而装载的类信息则都位于方法区。

 
以下是 ClassLoader 中与加载类相关的方法 :

方法说明
getParent()返回该类加载器的父类加载器。
loadClass(String name)加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name)查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name)查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len)把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class<?> c)链接指定的 Java 类。

 

        装载、连接以及初始化,类装载器子系统除了要定位和导入二进制 class 文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号应用。这些动作必须严格按以下顺序进行:

在这里插入图片描述
 

3.2 方法区

 
        在Java虚拟机中,被装载类的信息存储在一个逻辑上被称为方法区的内存中,当虚拟机装载某个类时,它使用类装载器定位相应的 .class文件,然后读入这个.class文件(一个线下二进制数据流),接着将它传输到虚拟机中。紧接着虚拟机提取其中的类信息,并将这些信息存储到方法区。需要注意:该类中的类(静态)变量同样也是存储在方法区中。

当虚拟机运行Java程序时,它会查找使用存储在方法区中的类信息。
 
方法区的线程安全:

        由于所有的线程都共享方法区,故它们对方法区数据的访问必须被设计成线程安全的。比如:假设有两个线程都要访问一个名为 Lav 的类,但是这个类还没有被装入虚拟机,那么这时只应该有一个线程去装载它,而另一个线程则只能等待。

方法区的大小:

        方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同时,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。另外,虚拟机也可以允许开发者指定方法区的初始大小,以及最小和最大尺寸等。

方法区的垃圾收集:

        方法区可以被垃圾收集,因为虚拟机允许通过用户定义的类装载器来动态扩展 Java 程序,所以有些类也会成为程序“不再引用”的类。当某个类成为不再被引用时,Java 虚拟机可以卸载这个类(垃圾收集),从而使方法区占据的内存保持最小。

 

对每个装载的类,虚拟机都会在方法区中存储以下的类信息:

  • 这个类的全限定名。
  • 这个类的直接超类的全限定名(除非这个类是 java.lang.Object,它没有超类)。
  • 这个类是类类型还是接口类型。
  • 这个类的访问修饰符(public、abstract 或 final 的某个子集)。
  • 任何直接超接口的全限定名的有序列表。
     

        在Java class 文件和虚拟机中,类名总是以全限定名出现。在 Java 源代码中,全限定名由类所属包的名称加上一个“.”,再加上类名组成。在 .class 文件中,全限定名由源码中的“.”全部改变成“/”。

 

除了上面列出的基本类型信息外,虚拟机还得为每个被装载的类存储以下信息:

  • 该类的常量池。虚拟机必须为每个被装载的类维护一个常量池。常量池就是该类所有常量的一个有序集合,包括直接常量(string、integer、floating point常量)和对其他类型、字段和方法的符号引用。
     
  • 字段信息。包含字段名、字段的类型、字段的修饰符(public、private、protected、static、final、volatile、transient的某个子集)。
     
  • 方法信息。包含方法名、方法的返回类型(或void)、方法参数的数量和类型(按声明顺序)、方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)。
     
  • 除了常量以外的所有类(静态)变量。类变量是由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。这些变量只与类有关,而非类的实例,因此它们总是作为类信息的一部分而存储在方法区。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。
     
  • 一个到类 ClassLoader 的引用。每个类被装载时,虚拟机必须跟踪它是由启动类装载器,还是用户自定义类装载器装载的。
     
  • 一个到 Class 类的引用。对于每个被转载的类(不管是类还是接口),虚拟机都会相应的为它创建一个 java.lang.Class 类的实例。而且虚拟机还必须以某种方式,把这个实例和存储在方法区中的类型数据关联起来。

 

3.3 堆内存

 
什么是堆内存?

       堆内存是Java内存中的一种,它的作用是用于存储java中的对象和数组,当我们new一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放。Java 中的堆是 JVM 所管理的最大的一块内存空间。

 
堆内存的特点:

  • 先进先出,后进后出。堆其实可以类似的看做是管道,或者说是平时去排队买票的情况差不多,也就是你先排队好,你就先买票。
     
  • 堆可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,但缺点是,由于要在运行时动态分配内存,存取速度较慢。
     

 
       new对象在堆中,由Java虚拟机的自动垃圾收集器来管理。

       垃圾收集器的主要工作就是,自动回收不再被运行中的程序引用的对象所占用的内存。此外,垃圾收集器也可能去移动那些还在使用的对象,以此减少堆碎片。
 
       Java 虚拟机规范并没有强制规定垃圾收集器,它只是要求悉尼及实现必须“以某种方式”管理自己的堆空间。比如:当某个实现可能只有固定大小的堆内存可用了,当堆内存不足时,它就简单的抛出 OutOfMemory 异常,根本不考虑回收垃圾对象的问题。
 

在这里插入图片描述
       Java 程序在运行的时候,创建的所有类实例或数组都放在同一个堆中。一个Java 虚拟机实例,只存在一个堆空间,因此所有线程都将共享这个堆。一个Java 程序独占一个Java 虚拟机实例,因此每个 Java 程序都有它自己的堆空间。(它们不会彼此干扰)。同一个Java 程序的多个线程,共享同一个堆空间,所以需要考虑多线程访问对象(堆空间)的同步问题。
 

Java中变量在内存中的分配 :

  • 类变量(static修饰的变量):在程序加载时系统就为它在堆中开辟了内存,堆中的内存地址 存放于栈以便高速访问。静态变量的生命周期—一直持续到整个“系统”关闭。
     
  • 实例变量:当你使用java关键字new的时候,系统在堆中开辟并不一定是连续的空间分配给变量(比如说类实例),然后根据零散的堆内存地址,通过哈希算法换算为一长串数字以表征这个变量在堆中的“物理位置”。实例变量的生命周期–当实例变量的引用丢失后,将被GC(垃圾回收器)列如可回收“名单”中,但并不马上就释放堆中内存。
     
  • 局部变量:局部变量,由声明在某方法,或某代码段里(比如for循环),执行到它的时候在堆中开辟内存,当局变量一旦脱离作用域,内存立即释放。

 

3.4 PC寄存器(程序计数器)

 
在一个运行的Java程序中,每一个线程都有它自己的PC寄存器(程序计数器)。它是在该线程启动时创建的。

       PC 寄存器的大小是一个字长,它既能持有一个本地指针,也能够持有一个 returnAddress. 当线程执行某个 Java 方法时,PC 寄存器的内容,总是下一条将被执行指令的“地址”,该“地址”可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时PC寄存器的值是“undefined”.

 

3.5 Java 栈内存

 

什么是栈内存?

栈内存是Java的另一种内存,主要是用来执行程序用的,比如:基本类型的变量和对象的引用变量。

 
栈内存的特点

  • 先进后出,后进先出。栈内存就好像一个矿泉水瓶,往里面放入东西,那马先放入的沉入底部。
  • 存取速度比堆要快,仅次于寄存器,栈数据可以共享,但缺点是,存在栈中的数据大小与生存必须是确定的,缺乏灵活性

 
栈内存分配机制:栈内存可以称为一级缓存,由垃圾回收器自动回收管理。

 
       每当启动一个新线程时,Java 虚拟机都会为它分配一个 Java 栈,并且 Java 栈是以帧为单位保存线程的运行状态。虚拟机只会直接对 Java 栈执行两种操作:以帧为单位的压栈或出栈。
 

  • 压栈:

       每当线程调用一个Java 方法时,虚拟机都会在该线程的Java 栈中压入一个新帧。而这个新帧就成为当前帧。在执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等数据。

  • 出栈:

       Java 方法可以以两种方式完成。一种通过 return 返回的,称为正常返回;另一种通过抛出异常而中止的。不管以哪种方式完成,虚拟机都会将当前帧弹出 Java 栈然后释放掉,这样上一个方法的帧就成为当前帧了。

 
Java 栈上的所有数据都是此线程私有的,任何线程都不能访问另一个线程的栈数据。Java 栈和帧在内存中不必是连续的。

 

3.6 栈和堆的区别

 
差异:

  1. 堆内存用来存放由new创建的对象和数组。
  2. 栈内存用来存放方法或者局部变量等。
  3. 堆是先进先出,后进后出。
  4. 栈是先进后出,后进先出。
  5. 共享性的不同:
       - 栈内存是线程私有的 。
       - 堆内存是所有线程共有的。
  6. 空间大小:栈的空间大小远远小于堆的。

 
 
 
 
 
 
 
 
 
.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值