首页 > 其他 > 详细

简单认识JVM

时间:2019-10-29 20:21:34      阅读:94      评论:0      收藏:0      [点我收藏+]

准备:

  在具体聊JVM之前,我们先看两张图,通过分析图,咱们慢慢来聊聊JVM。

技术分享图片

JVM内存结构图

 

技术分享图片

JVM内存结构脑图

  上面两张图中,第二张图相对来说比较直观,就是JVM内存结构都划分成了哪些模块,各个模块各有什么特点。其实这些我们都可以在第一张图中,找到答案,不着急咱们先慢慢的一点点的分析一下第一张图。  

  我们先来看一下JVM整体的体系结构。

JVM体系结构:

  类加载器(ClassLoader):负责加载.class文件。我们编译期通过javac将.java文件编译成.class文件。在程序运行的时候则就是根据.class文件特有的标示,由类装载器(ClassLoader)来加载.class文件。加载成功后,至于它是否可以运行,就要交给后面的执行引擎(ExecutionEngine)来判断了。

  运行时数据区:这也是我们今天主要分析的部分,也就是JVM的内存结构。它主要分为方法区、堆、Java栈、程序计数器、本地方法栈 五个部分。

  执行引擎:上面我们说一个.class文件是否可以运行,要看执行引擎,很显然执行引擎负责的最主要的工作就是执行字节码(即.class文件)以及本地方法。

  

  接下来,我想和大家再说说JVM整体工作的流程,或者说JVM从创建到消亡都经历了什么。

JVM工作流程:

  我们知道JVM主要的工作就是将编译成.class文件的Java代码解释翻译成平台所能识别的机器码,并且再通过特定平台运行代码。那它又主要经历了哪些事情呢?

  其实一个JVM是伴随着一个程序启动和退出来诞生和消亡的(一个计算机上如果运行多少个Java程序那么就会有多少个JVM)。

  当我们执行main函数启动程序的时候,相应的JVM就被创建了。

  我们知道,一个独立运行的程序一般都是有着自己独有的进程的,而程序中不同的操作却可能会开启不同的线程。在Java中存在两种线程一种是守护线程(也叫后台线程,Java代码中可以将一个线程指定为守护线程,但一般不使用,通常都是JVM中使用)一种是非守护线程(也叫前台线程),简单理解就是守护线程做的事情是看不见的,默默的来守护着,例如JVM中的垃圾回收所在的线程就是一个守护线程。而非守护线程则一般做的都是与用户交互的事情。作为程序的入口的main函数所在线程就是一个非守护线程,其他线程则都是在该线程中启动的。而JVM退出的条件就是所有的非守护线程都终止了才会退出,条件允许的话,我们也可以通过代码来进行强制退出的(java.lang.System.exit())。

  那JVM创建之后大概都做了什么呢:

  1、首先加载.class文件,这一步是通过类装载器(ClassLoader)完成的。

  2、其次是为字节码分配并管理内存。

  3、对无效的内存进行回收整理。

  接下来我们一步步的来解析一下。

类加载器:

  类加载器的主要任务是根据一个类的全限定名(包括包名和类名)来读取该类的二进制字节流到JVM中,然后为其生成一个对应的java.lang.Class实例对象,一个类只会被载入JVM中一次。    

  在JVM确认一个类是否被载入过,是通过该类的全限定名与该类的类加载器来确定的(PS:Java中只通过全限定名便可确认一个类的唯一性)。因为JVM预定了三种类加载器,用户有时也可自定义专门的类加载器,他们具体有哪些?如下:

  根类加载器(BootstrapClassLoader):用来加载Java的核心类,用户调用不到。

  扩展类加载器(ExtensionsClassLoader):它负责加载JRE的扩展目录下的类库,用户可以直接使用。

  系统类加载器(SystemClassLoader):也称为应用程序类加载器,它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。没有特殊情况用户自定义的类加载器就继承自该类加载器。

  大体介绍了类加载器,那么一个类被加载都经历了什么过程呢?咱们接着看...

类的加载过程:

  当程序主动使用某个类时,如果该类还未被加载到内存中的话,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化(有时将3个步骤会统称为初始化)。

  咱们看一张图:

技术分享图片

类的生命周期

  加载:这一步也往往被称为“装载”,它是类加载机制中的第一步,它主要完成了:

     1、通过该类的全限定名来获取该类的二进制字节流(具体的获取方式用户可以自定义类加载器控制)。

     2、将字节流所代表的静态存储结构转换为方法区的运行时数据结构。

     3、在java堆中生成一个代表该类的java.lang.Class对象,作为方法区该类的访问入口。

  链接:连接阶段负责把类的二进制数据合并到JRE中。它包含了三个过程:

     1、验证:通过文件格式验证、元数据验证、字节码验证和符号引用验证,确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

     2、准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值

     3、解析:将类的二进制数据中的符号引用替换成直接引用(PS:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在)。

  初始化:如果该类具有父类,则对父类进行初始化,执行静态初始化器和静态初始化成员变量(准备阶段的静态变量将会在这个阶段赋值,成员变量也将被初始化)。

  上面我们了解了类加载器与类的加载过程的大概,那么一个类在什么时候会被加载呢?来看...

类何时加载:

  一个类被加载一般有如下几种情况:

  1、new一个对象的时候。

  2、调用一个类的静态方法的时候。

  3、初始化某个子类的时候(初始化子类会先初始化父类)。

  4、JVM启动时标明的启动类(即与文件名相同类名的类 )。 

  5、使用反射时(Class.forName("xxx"))。 

  6、访问某个类或接口的静态变量,或者对该静态变量赋值时。

  7、对于 static final 修饰的变量,声明时就已经将值确定下来了,那么就相当于静态常量,它会被放在“常量池”(PS:关于常量池,可以看我另一篇文章《常量池---知识网》)中的,再次使用时是不会初始化该类的。但static final声明Field类时并不能确定值,只有在运行时给该变量赋值,此时获取此静态变量就要初始化该类了。(PS:Field是java.lang.reflect包下一个类,我们使用反射时会用到它获取当前对象的成员变量的类型或对成员变量重新设值)

  我们了解了类的加载这个过程,我们继续看图,接下来就是我们经常见到的JVM内存结构了,也就是运行时数据区。

JVM内存结构:

  我们从图中可以看到,运行时数据区分为了五个部分,而在图中将方法区、堆划为了线程共享区,将Java栈、程序计数器、本地方法栈划为了线程独占区,这是什么意思呢?

  其实所谓的线程独占区其实就是它们存在与否是和线程绑定在一起的,当创建一个线程的时候就会为其分配Java栈、程序计数器、本地方法栈,当线程终止时分配的这三样就会被回收,也就是它们所占用的内存会被释放掉。而方法区、堆则是与程序绑在一起的,也就是进程级别的,和哪个线程无关,所有线程都可以访问。

  那这五个部分具体都是什么样子的呢?来看...      

  方法区:在这里,存储了每个类的信息(包括类的名称、修饰符、方法信息、字段信息)、类中静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息以及编译器编译后的代码等。当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,在这里进行的GC主要是方法区里的常量池和类型的卸载。当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。在方法区中有一个特别重要的东西叫做“运行时常量池”(PS:关于常量池,可以看我另一篇文章《常量池---知识网》,这里就不详细描述了)。

  堆:堆是用来存储对象实例以及数组的(当然,数组引用是存放在Java栈中的)。堆是被所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。JVM中只有一个堆。堆是Java垃圾收集器管理的主要区域,Java的垃圾回收机制会自动进行处理。堆空间分为老年代和年轻代。刚创建的对象存放在年轻代,而老年代中存放生命周期长久的实例对象。年轻代中又被分为Eden区和两个Survivor区(From Space和To Space)。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次GC仍然存活的,就会被转移到老年代。 当一个对象大于eden区而小于old区(老年代)的时候会直接扔到old区。 而当对象大于old区时,会直接抛出OutOfMemoryError(OOM)。

  java栈:Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈。JVM栈是线程私有的,每个线程创建的同时都会创建自己的JVM栈,互不干扰。Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。

  程序计数器:也称作PC寄存器,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且互不干扰。(PS:在JVM规范中规定,如果线程执行的是非native(本地)方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined)。程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

  本地方法栈:JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。本地方法栈与Java栈的作用和原理非常相似。在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它(在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一)。

  上面我们大概了解了运行时数据区的各个区域的作用,我们接着看图。

执行引擎:

  类装载器装载负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area),然后执行引擎执行会执行这些字节码。通过输入的字节码,进行字节码解析,输出执行后的结果。执行引擎会把字节码转换成可以直接被机器执行的机器码,而执行引擎一般都是通过解释器(边解释边执行,逐条来所以解释的快,但执行的就慢了)、即时编译器(合适的时候解释,放在缓存里,执行的就快了)来解析字节码的。

  好了,以上就是对JVM的一个大概的整理吧,如果你真的通读了下来,那么就看看下面这张图吧,试着自己回忆一下吧。

技术分享图片

JVM体系图

参考:《深入详细讲解JVM原理

   《JVM之内存结构详解

   《jvm之java类加载机制和类加载器(ClassLoader)的详解

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

简单认识JVM

原文:https://www.cnblogs.com/HelloHai/p/11761035.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!