JVM知识梳理与总结上

1、编写Java代码到运行它

JVM知识梳理与总结上

      如上图XXX.java是源文件,然后使用命令javac  XXX.java将源文件编译成XXX.class文件,然后再使用命令java XXX字节码文件(class文件)。当然实际工作中一般项目中是使用mvn相关命令将java系统打包成jar/war包,然后使用java xxx.jar或者部署到tomcat等相关web服务器运行。JVM在执行字节码文件的时候首先类加载器要先将字节码文件加载到JVM中,然后再由JVM来执行我们的代码。

     class字节码文件从加载到执行结束大概经历了如下几个阶段:

   JVM知识梳理与总结上

1.1、加载阶段

    通过一个全限定类名来获取定义此类的二进制字节流;将这个字节流的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表该类的Class对象,作为方法区这个类的各种数据的访问入口。存放的内存有的叫方法区、有的也叫永久代区(JDK8后叫Metaspace元数据空间)。

1.2、验证阶段

   根据Java虚拟机规范,来校验你加载进来的“.class”文件中的内容是否符合指定的规范。以确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全。主要包括文件格式验证、元数据验证、字节码验证、符号引用验证。

1.3、准备阶段

    给类分配一定的内存空间,然后给他里面的类变量(也就是static修饰的变量)分配内存空间,并指定一个默认的初始值。

1.4、解析阶段

    虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用类似于别名,直接引用类似于实际存储地址)。主要包括类或接口的解析、字段解析、类方法解析、接口方法解析。

1.5、初始化阶段

    执行类的初始化代码,如果初始化的时候发现其父类还没被初始化那么会先初始化其父类。

 

2、重点关注类的加载

2.1、类加载器

2.1.1、启动类加载器(Bootstrap ClassLoader)

    加载<JAVA_HOME>\lib目录中的类库(或被-Xbootclasspath参数所指定的路径中)


2.1.2、扩展类加载器(Extension ClassLoader)

   加载<JAVA_HOME>\lib\ext目录中的类库(或被java.ext.dirs系统变量指定的路径中的类库)


2.1.3、应用程序类加载器(Application ClassLoader)

    加载用户类路径(ClassPath)上所指定的类库。


2.1.4、用户自定义加载器

    继承ClassLoader  ;重写findClass()方法 ;调用defineClass()方法

 

2.2、双亲委派机制

JVM知识梳理与总结上

    你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载,但是如果父类加载器在自己负责加载的范围内没找到这个类,那么就会下推加载权利给自己的子类加载器。

 

3、JVM内存区域划分

3.1、方法区(JDK8以后改为Metaspace)

    存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。常见的class字节码文件加载到JVM后就是存放在方法区的。


3.2、程序计数器

    简单说就是我们写好的java源文件被编译成class后缀的字节码文件,而class字节码文件就对应计算机能识别的一条条指令。所以当JVM加载.class文件到内存后,就会使用字节码执行殷勤去执行我们写好的代码编译成字节码对应的一条条指令。
所以程序计数器就是用来记录当前执行的字节码指令的位置的,也就是记录目前执行到了哪一条字节码指令。


3.3、java虚拟机栈

    虚拟机栈中存放每个方法执行时创建的栈帧,对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,
这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧用于存放局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入了方法表的 Code 属性之中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
    每个线程都有自己的Java虚拟机栈,比如在执行main方法的main线程就会有自己的一个Java虚拟机栈,用来存放自己执行的那些方法的局部变量。如果线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧。栈帧里就有这个方法的局部变量表 、操作数栈、动态链接、方法出口等东西。

3.4、本地方法区

    本地方法栈则为使用到的本地操作系统(Native)方法服务。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以*实现它。

 

3.5、堆

   Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。

    从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
 

3.6、堆外内存

  在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。通过NIO中的allocateDirect这种API,可以在Java堆外分配内存空间。然后,通过Java虚拟机里的DirectByteBuffer来引用和操作堆外内存空间。

 显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
 

4、JVM各个内存区域协作流程图

JVM知识梳理与总结上