JVM ---运行时数据区域

JVM涉及知识点众多,凡是自己做的项目涉及到的用到的就拿来,没涉及到也就没整理。里面也有借鉴他人博客的内容。

(图片来源深入理解Java虚拟机)

把程序分组代码抽象理解为三部成:数据,指令,控制流,

数据可以理解为:定义的成员变量,静态变量,常量;

指令理解为:在方法中执行的语句;

控制流理解为:分支、循环、跳转、异常处理、线程恢复。

【---------------------------------------------------------------------------------------------------------------

        程序计数器是一块较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效方式实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要以来这个计数器来完成。

            由于Java虚拟机的多线程是通过线程轮流切换分配处理器执行时间的方式来实现的,在任何一个确定时刻,一个处理器(对于处理器来说是一个内核)都只会执行一条线程种的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存

            如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址:如果正在执行的是Native方法,这个计数器值则为空(Undefined).此内存区域是唯一一个在java虚拟机种没有规定任何OutOfMemoryError的情况区域。

        Javaheap是java所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存的唯一目的就是存放对象实例,,几乎所有的对象实例都是在这里分配内存。这一点在java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术(什么是逃逸分析技术???)嘚吧嘚吧半天说的意思就是所有对象都分配的对上不“绝对”了.

    Java堆是垃圾收集器管理的主要区域,从内存回收角度来看,由现在收集器基本采用分代收集算法,所以java堆中还可以细分为:新生代和老年代,在细致一点的有Eden空间,FormSurvivor空间,ToSurvivor空间等。从内心的角度分析看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allcation Buffer TLAB).不过无论如何划分,都与存放的内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存

方法区(Method Area与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名Non-Heap(非堆)目的是与Java堆区分开来。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和引用符号,这部分内容将在加载后进入方法区的运行时常量池中存放。

    Java虚拟机堆Class文件每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可,装在和执行,但对于运行时常量池,java虚拟机规范没有做任何细节要求,不同的提供商实现的虚拟机可以按照自己的需求来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征时具备动态性,java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,利用较多的是Stringl类的intern()方法

    对象的创建在Java程序运行过程章无时无刻都有对象被创建出来。在语言层面上,创建对象(例如克隆,反序列化)通产仅仅是一个new 关键字,而在虚拟机中,对象(文中讨论仅限于普通Java对象,不必包括数组和Class对象等)创建过程。如下

        虚拟机遇到一条new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如果没有,那么必须限制性相应的类加载过程。

        在类加载检查通过后,接下来虚拟机将新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。Java


    

---------------------------------------------------------------------------------------------------------------】 

ProgramCount Register (程序计数器)

    

官方解释:程序计数器是一块较小的内存空间,指的是当前线所执行的字节码的行号指示器。

      通俗的讲解: 程序计数器是与线程绑定的,也就是每个线程在运行期都有独立的程序计数器。在上图可以看出,程序计数器都是属于线程隔离的数据区,我们在讲多线程是讲过,多线程是通过线程间抢夺CPU分配时间来竞争执行的(多核CPU来说指的是一个内核),一个CPU只会在同一时间内执行一条线程命令。

eg---》有A、B两个线程,当线程A获取到运行时间执行到一半时,

CPU分配的时间片用完了,此时A线程就要挂起,然后两个线程再次竞争下一次CPU时间片,因此A线程就需要一个计数器记录上次执行位置,好让下次在获取到CPU时间片时可以恢复到正确位置执行下去。各个线程之间的计数器是独立的,互不影响,独立存储,如果线程在执行一个方法时,此线程记录的是正在执行的字节码指令的地址,如果执行的是本地(Native)方法,则计数器的值为空(undefined),由于程序计数器的内存很小,所以JVM规范中没有规定此区域内存溢出的情况

 

Java虚拟机栈

         Java虚拟机栈也是线程独立的,多个线程有独立的Java虚拟机栈,栈的生命周期与线程相同,在线程启动时内创建,线程结束时被销毁,栈是用来存储Java方法运行时数据的,那栈中存储的数据是什么方式来组织的呢?其实在栈中存储的数据结构是一个数据单位来体现的,这个数据单位成为栈帧(Stack Frame)当程序执行一个方法时,会创建一个栈帧,我们成为入栈,当方法执行结束后,栈帧就会被销毁,我们称为出栈。在一个栈帧里,用具存储局部变量表、操作数栈、动态链接、方法出口和一些额外的附加信息。


      栈帧时虚拟机在方法调用调用执行时存储在虚拟机的数据结构,也可以称为栈元素,一个方法对应一个栈帧

栈的数据结构是先进后出,当前正在运行的线程所在的位置成为栈顶,多个线程拥有各自独立的虚拟机栈,在一个栈帧里面,接下来详细讲解一下栈帧的局部变量表、操作数栈、动态链接、返回地址等部分的作用和数据结构。

         局部变量表可以理解为是一组变量值的存储空间,目的是为了存放方法参数和方法内部定义的局部变量。当程序被编译成Class文件时,该方法会有一个Code属性的max_locals数据项来确定该方法所需要分配的局部变量表的最大容量,所以栈帧中需要多大的局部变量表,在编译后就已经确定了,并且在程序运行期变量表的容量不用改变,所以我们在基础入门时讲的,栈中存储的数据时确定大小的,就是这个原因

         操作数栈是一个先入后出,同样局部变量表一样,操作数栈的最大深度也在编译的时候写到方法的Code属性的max_stacks数据项中,操作数栈可以理解为正在操作中需要处理的数据和结果,eg:很简单的两个数相加操作,加法的字节码指令是iadd,在运行的时候作数栈中最接近栈顶的两个元素例如已经存入了2个init型的数值,执行iadd指令时,会将这两个值出栈相加,然后将相加的结果入栈。

            动态链接,每个栈帧都包含一个指向运行时常量池中该栈帧所述方法的引用,有这个引用是为了支持方法调用过程中的动态链接,因为Class文件的常量池有很多符号引用,这些符号有一部分将在每一次运行期转化为直接引用,称为动态链接,还有一部分是在类加载或第一次使用时转化为直接引用,成为静态引用。说白了,动态连接,就是通过符号的方式来引用常量池。

            方法返回地址,记录着该方法要返回到被调用的位置(通过地址来记录),我们知道方法结束有两种方式,一种是方法内部执行时遇到任意一个返回的字节码指令,这时候可能有返回值要传递给上层方法的调用者,就是调用当前方法的方法;另一种结束方式是在执行的过程中遇到了异常,并且没有在方法体内进行处理,也就是没有使用try...catch语句,此时在本方法中维护的异常表没有搜索到匹配的异常处理器,就会导致方法退出,而这种退出为异常完成出口,就不会给上层调用者返回任何值。所以方法返回地址就是用于记录调用者在哪里,以便于可以正常回到调用者的位置上

二、JAVA垃圾回收机制

       JAVA的垃圾回收主要涉及到确定对象是否存活、垃圾收集等算法,其中确定对象回收算法采用的是可达性分析算法,垃圾收集目前各JVM厂商广泛采用的是分代收集算法。这里面主要描述下分代收集算法的过程。

       分代收集算法的核心思想是将内存区域按照对象的生存周期阶段进行划分,其中将堆区划分为新生代(young generation)和老年代(old generation)。将非堆区(一般指方法区)划分为持久代(permanent generation)。

       1.新生代

       新生代又可再分为Eden区和两个Survivor区(两个Survivor区的大小是一样的,便于交换)。新生成的对象都会先在新生代的Eden区进行保存。新生代的特点是每次垃圾回收都会有大量的内存被回收,而且收集比较频繁,所以新生代适合如下的收集算法:

       首先,新生成的对象分配到Eden区,如果eden区满了,则将可达性的对象复制到survivor1区,后清空eden区。

       然后,如果survivor1区满了,则将eden区与survivor1区的可达性对象复制到survivor2区,后清空eden区和survivor1区,清空完后将survivor2区与survivor1区交换,即保持survivor2是空的。

       再次,如果survivor2区也满了,则将eden区、survivor1区、survivor2区的可达性对象复制到老年代中,并清空新生代中。

       最后,如果老年代也满了,就触发full gc了。

       2.老年代

       老年代的内存比新生代大的多,这个区域执行垃圾回收的频度不高。当老年代满时,会触发full gc。

       3.持久代

       持久代一般指方法区,该区需要回收的有废弃的常量和类。对于常量可用可达性分析的方法进行判断回收,对于类则需要同时满足以下条件才会被回收:

       首先,该类的所有实例对象都已被回收;

       其次,该类的类加载器也已被回收;

       再次,该类的Class方法没有在任何地方被引用,即无法通过在任何地方通过反射访问到该类的方法。

        4.什么时候会解决垃圾回收?

       综上所述,当eden满时,就会触发scavenge gc,当出现以下情况时会触发full gc:

       老年代已满;

       持久代已满;

       调用System.gc()方法;

三、JAVA类加载过程

        JVM类加载过程具体装载、验证、准备、解析、初始化这五个部分。

       1.装载

       在装载过程中,需要完成以下事情:

       1)通过类的全限定名获取类的二进制字节流;

       2)将类的二进制字节流转换为方法区的运行时数据结构;

       3)生成一个代表此类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

       2.验证

       验证、解析和初始化又称为是连接阶段,在验证验证主要是确保二进制字节流符合JVM的规范,不会危害计算机的安全。具体验证阶段需要做的事情如下:

       1)文件格式验证,验证字节流是否符合Class文件格式规范;

       2)元数据验证,对字节码进行语义验证,以保证其描述信息符合JAVA语言规范;

       3)字节码验证,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;

       4)符号引用验证,对常量池中的各种符号引用的信息进行匹配性验证。

       3.准备

       准备的过程其实是分配内存的过程。在这个阶段有两个容易产生混淆的概念:一是此阶段分配内存的只是类变量(static变量),不包含实例变量,实例变量的内存分配是在对象实例化时随对象一起分配在堆中;二是该阶段分配内存中保存的值只是数据类型的零值,具体值需要在初始化阶段进行赋值。也有特殊情况,就是对于静态常量(final修饰)会在准备阶段将值赋值为真实值。

       4.解析

       解析阶段就是将常量池内折符号引用转换为直接引用的过程,具体包括类和接口的解析、字段的解析、方法的解析、接口方法和解析。

       5.初始化

       初始化阶段其实就是执行类构造函数(clinit)的阶段。对于clinit()需要说明以下几点:

       1)clinit()中的程序是自动收集类中static变量及static块产生的,执行顺序与代码中的顺序一致。静态语句块中只能访问在其之前声明的static变量,在其之后声明的static变量只能赋值,不能访问。

       2)执行clinit()方法前,JVM会自动调用父类的clinit()方法;

       3)虚拟机会保证一个类的clinit()在多线程环境中,自动加锁、同步。

四、JVM的类加载器

       JVM的类加载是通过类加载器实现的,常用的类加载器包括下面三种:

       1.启动类加载器(bootstrap classloader):加载{JDK_HOME}/lib下的类

       2.扩展类加载器(extension classloader):加载{JDK_HOME}/lib/ext下的类

       3.应用程序类加载器(application classloader):加载classpath指定的类

       对于不同类加载器以及他们之间的协作可以参考下面的双亲委派模型。

       双亲委派模型的工作过程是:如果一个类加载器收到了类的加载请求,会首先把请求委派给自己的父类,每个层次的类加载器都会如此,因为所有的加载请求最终都会发送到bootstarp加载器中,只有当父加载器确实无法自己完成加载请求时,子加载器才会尝试自己加载。

       双亲委派模型使得JAVA类能够按层次进行加载,不会造成混乱。

五、JVM的相关工具

       JDK中有很多强大的监控工具,可以直接在命令行运行。这对于在生产环境进行监控是非常有用的。例如SUN JDK中就包含了以下监控和故障处理工具。

       jps: jvm process status tool,显示指定系统内所有的hotspot虚拟机进程
       jstat: jvm statistics monitoring tool,用于收集hotspot虚拟机各方面的运行数据
       jinfo: configuration info for java,显示虚拟机配置信息
       jmap: memory map for java,生成虚拟机的内存转储快照(heapdump文件)
       jhat: jvm heap dump browser,用于分析heapmap文件,它会建立一个http/html服务器,让用户可以在浏览器上查看分析结果
       jstack: stack trace for java ,显示虚拟机的线程快照

还有java并发。在



猜你喜欢

转载自blog.csdn.net/qq_35361859/article/details/80804229