JVM初学JVM

Posted 六六学java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM初学JVM相关的知识,希望对你有一定的参考价值。

文章目录


一、JVM是什么

JVM是Java Virtrual Machine (Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机之后,java语言在不同的平台运行时不需要重新编译即可运行。java语言使用java虚拟机Java虚拟机可以屏蔽其具体平台的相关信息,使得java程序只需要通过编译成JVM能识别的字节码文件就可以实现在多个平台不需要重新编译即可运行,这也是java语言跨平台特点的原因

虽说JVM是java虚拟机,但JVM并不只是为java服务,JVM只识别字节码文件,只要是字节码文件,即可在JVM中运行

二、JVM架构

基于JVM架构的JVM程序执行流程:


编写好的java文件通过编译工具(IDEA)或者javac命令编译成class字节码文件,然后通过类装载器(类加载器)将字节码信息加载进内存,将其放在运行时数据区。但是字节码文件只是JVM的一套指令集规范(可以理解为JVM指定的一个规范,只有符合这个规范才能被JVM识别),并不能直接交给底层操作系统去执行,所以只能用过执行引擎将字节码编译成底层操作系统指令,再交给CPU去执行,而在这个过程中需要调用其他语言的本地库接口来实现整个程序的功能

JVM架构大致分为:类加载器,运行时数据区,执行引擎,本地库接口,本地方法库,接下来挨个分析

2.1、类加载器

对应到上述的JVM架构图中的这个位置:

作用:

类加载器的作用就是通过java类的全限定名将这个java文件的二进制字节流加载到JVM中,并将其中的静态数据转化成运行时数据结构,然后生成一个该类所对应的对象实例,作为方法区中类数据的访问入口

在虚拟机中提供了四种类加载器:

  • 启动类加载器(Bootsrap ClassLoader):C++编写的,加载java核心库java.*
    • 这个类加载器负责放在<JAVA_HOME>\\lib目录中,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用
    • 由于这个类加载器涉及到虚拟机本地实现的细节,所以不能直接进行操作
  • 拓展类加载器(Extension ClassLoader):java编写的,用来加载拓展库
    • 他负责<JAVA_HOME>\\lib\\ext目录下的jar包,或者是被-D java.ext.dirs系统变量指定目录下的jar包,是可以直接被开发者使用的
  • 应用程序类加载器(Application ClassLoader):java编写的,加载程序所在目录下的
    • 负责的是用户路劲下的(ClassPath)所指定的类库,也是可以直接使用的,如果开发者没有自己定义类加载器,那么默认的是使用的是这个类加载器
  • 自定义类加载器(User ClassLoader)
    • 用户自定的类加载器

原理:

类被类加载器加载到内存知道卸载出内存为止,其生命周期包括:

加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

其中验证、准备、解析俗称为连接

重点解释一下几个生命周期:

  1. 加载:

    加载是 类加载机制中的一个阶段,这个阶段也被称为装载,主要完成

    1. 通过全类名来获取到此类的二进制流
    2. 将字节流所代表的的静态存储结构转换为方法区的运行时数据结构
    3. 在java堆中生成一个代表这个类的对象,作为方法区这些数据的访问入口

    相对于类加载的其他阶段来说,加载阶段是开发期可控性最强的一个阶段了,因为加载阶段可以使用系统提供的类加载器来完成,也可以由用户自定义实现的类加载器来实现加载的这个阶段,这样用户就可以自定义字节流的获取方式

  2. 验证

    验证阶段主要的目的是确保该字节码文件符合当前虚拟机的要求,不会危害虚拟机自身的安全

    验证这个阶段主要包括四个验证步骤:

    1. 文件格式的验证
    2. 元数据的验证
    3. 字节码的验证
    4. 符号引用验证
  3. 准备

    准备阶段是开始为类变量分配内存以及设置初始值的阶段,这些内存都将在方法区中分配

    需要注意的是:

    1. 这个的分配内存仅仅只是为类变量分配内存,也就是被static修饰的变量,而不是实例变量,实例变量是在对象实例化的时候随着对象一起分配在堆内存中
    2. 然后就是设置初始值的问题,这里的初始值表示的是这个变量所对应的类型的默认值,比如public static int value = 12,在初始化的时候只会将该类变量的值设置为0,而不是12,因为int类型的数据的默认值是0,在对该变量的值设置为12,是在后面的初始化阶段。这种仅仅只是被static修饰的变量被赋值只能使用构造器赋值的方法。
    3. 在一些特殊情况下,类变量的值却并不是设置为该类型的默认值,而是直接设置其在被定义时的值,这种特殊情况就是这个变量同时被static以及final修饰符修饰的变量,这样的变量,在被编译的时候会生成ConstantValue属性 ,而有这个属性的变量的初始值就会是被定义的时候的值,而不是默认值,比如:public static final int value = 12 这个value就会被赋予ConstantValue属性,其初始值也就会是12
  4. 解析

    解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
    符号引用

    • 什么是符号引用?在说符号引用之前我们先来看看直接引用,直接引用是什么,比如就是你拥有你所需要数据的地址值,可以直接根据地址值获取到数据。但是 java语言是解释性的语言,然后由于总总原因(我也不知道对不对的原因)在某些时刻有些东西的直接地址还并不存在,是无法使用直接引用。这时候就可以用到符号引用了。
    • 符号引用:符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。这样我们就能根据符号引用锁定唯一的类,方法或字段了。
  5. 初始化

类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)

类装载器的内部机制:双亲委派机制(重点)

就是当某个类加载器需要加载一个字节码文件的时候,首先会将这个任务委托给当前类加载器的上级类加载器,然后上级委托它的上级,如此重复,如果上级类加载器都没有加载这个字节码,那么当前类加载器才会去加载这个字节码文件



双亲委派机制的作用

  • 防止重复的加载一个字节码文件:通过委托的方式询问上级类加载器是否加载过此类,如果加载过,那么自己就不需要去加载了,这样就保证了数据的安全
  • 保证核心的字节码文件不被修改:同样是通过委托的方式,不回去修改核心的字节码文件,即使有人修改了核心字节码文件,那因为委托的方式,也不会加载当前修改过的字节码文件,即使被加载了,也不会是同一个字节码的对象,不同的加载器去加载一个字节码文件,也不会是同一个字节码对象,这样就保证了class的执行安全

2.2、运行时数据区

  1. 方法区(Method Area):线程共享
  2. 堆(Heap):线程共享
  3. 虚拟机栈(VM Stack):线程私有
  4. 本地方法栈(Native Method Stack):线程私有
  5. 程序计数器(Program Count Register):线程私有
  • JVM运行时会分配好方法区和堆,而JVM每遇到一个线程,就为其分配一个程序计数器、虚拟机栈、本地方法栈,当线程终止时,三者(程序计数器、虚拟机栈、本地方法栈)所占用的内存空间也会释放掉。
  • 程序计数器、虚拟机栈、本地方法栈的生命周期与所属线程相同,而方法区和堆的生命周期与JAVA程序运行生命周期相同,所以GC(垃圾回收)只发生在线程共享的区域(大部分发生在Heap上)

相关定义解释:

  • 堆:

    • Java中的堆是用来存储对象实例以及数组(当然,数组引用是存放在Java栈中的)。堆是被所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。在JVM中只有一个堆。堆是Java垃圾收集器管理的主要区域,Java的垃圾回收机制会自动进行处理。

    • 堆空间分为老年代和年轻代。刚创建的对象存放在年轻代,而老年代中存放生命周期长久的实例对象。

    • 年轻代中又被分为Eden区(伊甸区)和两个Survivor区(幸存区:From区,To区)。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次GC仍然存活的,就会被转移到老年代。

      刚刚创建的对象会放在堆空间中新生代的伊甸区,可如果伊甸区空间已经满了,那么就会进行垃圾回收(Minor GC),将已经不被其他对象引用的对象进行销毁,而剩余依旧正在被引用的对象就会放在幸存0区(From),可如果From区也满了,就对From区进行垃圾回收(Minor GC),将From区中还幸存的对象移动到幸存1区(To),可如果To区也满了的话再移动带老年代,如果老年代也满了的话,就会执行垃圾回收(Major GC/Full GC),对老年代中的对象进行清理,可如果老年代清理之后还是满的,那么就会产生OOM(OutOfMemoryError)错误


方法区:

  • 方法区只是JVM的一种规范,在java中JDK7之前方法区的实现叫做永久代,而在JDK8之后,元空间就提带了永久代

  • 永久代和元空间到区别

    • 存储位置:永久代是在 Java 堆中的一个特殊区域,而元空间是在本地内存中的。

    • 大小调整:永久代的大小是有限制的,并且必须在启动时指定,而元空间可以根据需要自动调整大小。

    • 垃圾收集:永久代使用 Java 堆的垃圾收集器进行垃圾回收,而元空间使用本地内存的垃圾收集器。

    • 存储内容:永久代主要存储类的信息(如类名、方法名、字段名等),而元空间存储的是类的元数据(如类的结构、方法表、字段表等)。

    • 类信息的存储方式:永久代中的类信息是使用永久代专用的类加载器加载和卸载的,而元空间中的类信息是使用与应用程序类加载器相同的类加载器加载和卸载的。

  • 在方法区中存储了每个类的信息(包括类的名称,修饰符,方法信息,字段信息),类中静态变量,类中定义为final类型的常量、类中的Field信息、类中的方法信息以及编译器编译后的代码等

  • 方法区是线程共享的,在永久代中也会跟堆一样会被GC,但是在JVM中规定了方法区可以不被GC,但是由于方法区的回收效率跟堆相比实在太差,所以方法区中的部分区域是可以被GC的,就比如常量池的垃圾回收,以及一些没有被引用的类的卸载等,当然,一定情况下,永久代还是会出现OOM错误,因为永久代是堆空间的一个特殊区域。

  • 而元空间使用的是本地内存存储类的元数据,是根据计算机的内存大小而定的,可以有效的避免永久代内存大小得到限制,以及永久代垃圾回收器效率低的限制

  • 虚拟机栈:

    • 虚拟机栈是线程私有的,每一个线程创建的同时都会创建自己的虚拟机栈,互不干扰

    • 虚拟机栈中存放的是一个个栈帧,每一个栈帧代表的是被调用的方法。在栈帧中包括局部变量表、操作数栈、指向当前方法所属类的运行时常量池的引用、方法的返回地址和一些额外的信息

    • 局部变量表(LV):用来存储方法中的局部变量,对于引用类型的变量,则存的是指向对象的引用,对于基本数据类型的变量,直接存储的是它的值

    • 操作数栈(OS):栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归跟到低就是进行计算的过程。因此可以说,程序的所有计算过程都是在借助于操作数栈来完成的

    • 指向运行时常量池的引用(DL):因为在方法执行的过程中有可能需要用到类中的常量,所以需要有一个引用指向运行时常量

    • 方法返回地址(RS):当一个方法执行完毕之后,要返回之前调用它的地方,因此需要在栈帧中必须保存一个方法的返回地址

  • 当一个线程执行一个方法的时候,就会创建一个对应的栈帧,并将创建的栈帧放入栈中,由于栈是先进后出的数据结构,所以当前正在执行的方法一定是放在栈顶的,当当前方法被调用完毕之后就会出栈。

  • 程序计数器:

    • 也被称为PC寄存器

    • 在JVM中,多线程是通过线程轮流切换获得CPU的执行时间,所以,在任意时刻,一个CPU的内核只会执行一条线程中的指令,因此为了能够使每一个线程在线程切换后,下一次获得CPU执行时间的时候能够恢复到上一次执行的位置,每一个线程都需要有一个属于自己的程序计数器来保存上一次执行的位置,同时每一个线程的程序计数器不能互相被干扰,否则会影响到其他线程执正常执行的次序,所以说程序计数器是线程私有的

    • 程序计数器中存储的数据所占的内存空间的大小不会随着程序的执行而发生改变,所以程序计数器是不会发生OOM内存溢出错误

  • 本地方法栈:

    • JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。
    • 区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
    • Native Method(本地方法)的定义

创建对象内存分析

2.3、执行引擎

在之前先了解各种语言的发展:

机器码:用二进制编码方式表示的指令,叫做机器指令码,也叫硬编码。最初的人们就是采用它来编写程序,这就是机器语言

  • 机器码虽能被计算机理解和接受,但不易理解和记忆,编码难度也较大;
  • 机器码的程序,CPU能直接读取运行,因此和其他语言编的程序相比,执行速度最快;
  • 机器指令与CPU硬件紧密相关,所以不同种类的CPU所对应的机器指令也就不同。

指令:

  • 由于机器码是由0和1组成的二进制序列,可读性实在太差,于是发明了指令;
  • 指令就是把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写,如mov,inc等),可读性稍好;
  • 但不同的硬件平台,执行同一个操作,对应的机器码可能不同;不同的硬件平台的同一种指令,对应的机器码也可能不同。

指令集:

不同的硬件平台,各自支持的指令是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。

  • x86指令集,对应的是x86架构的平台
  • ARM指令集,对应的是ARM架构的平台

汇编语言(硬件级别的语言,不能跨平台):

  • 由于指令的可读性还是太差,于是又发明了汇编语言;
  • 汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址;
  • 不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
  • 由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行。

高级语言:

  • 为了让用户编程更容易些,后来就出现了各种高级计算机语言,高级语言比机器语言、汇编语言更接近人的语言;
  • 当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。

JVM执行引擎:

JVM主要负责装载字节码文件进内存,但是字节码并不能直接运行到操作系统上,因为字节码指令并非机器码,且只能被JVM识别,所以,为了让java程序在操作系统上面跑起来,JVM中执行引擎就负责将高级语言(字节码指令)翻译为机器码被操作系统识别,以及对字节码的优化和执行GC垃圾回收器

JVM中主要包含:两种解释器,JIT即时编译器,GC垃圾回收器

  • 执行引擎执行哪条字节码指令完全依赖于PC寄存器,每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
  • 执行方法过程中,有可能会通过存储在局部变量表(栈)中的对象引用准确定位到存储在Java堆区中的对象实例信息;
  • 通过对象头(堆)中的元数据指针,定位到目标对象的类型信息(方法区);

2.3.1、解释器(interpreter):

JVM设计初期,仅仅只是为了实现java的跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而实现了再运行时采用逐行解释字节码到机器码执行程序的方案

JVM发展史中,共有两套解释执行器:

  • 古老的字节码解释器:在执行时通过纯软件代码翻译字节码的执行,效率非常低下。

  • 现在普遍使用的模板解释器:将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,提高了解释器的性能。

在HotSpot VM中,解释器主要由Interpreter模块和Code模块构成。

  • Interpreter模块:实现了解释器的核心功能;
  • Code模块:用于管理HotSpot VM在运行时生成的本地机器指令;

2.3.2、JIT即时编译器(Just In Time Compiler)

因为解释器在设计和实现上非常简单,导致编译的时候效率极低。为了解决这个问题,JVM就提供了JIT即时编译器即时编译可以将整个函数体编译成机器码,避免了函数体被解释执行,在重复执行该函数体的时候直接执行编译后的机器码即可,大大的提高了执行效率

HotSpot VM中采用的是解释器与即时编译器并存的架构,解释器和编译器相互协作,取长补短,选择最合适的方式来权衡编译本地代码和直接解释本地代码的时间

HotSpot VM默认的程序执行方式:

  • 在虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,在启动时省去许多不必要的编译时间。
  • 随程序运行时间推移,即时编译器逐渐发挥作用,根据热点探测功能,将满足触发JIT条件的字节码编译为本地机器指令,以换取更高的程序执行效率。

JIT即时编译器的分类:

在HotSpot VM中内嵌两个JIT编译器:Client Compiler(C1编译器)Server Compiler(C2编译器)

为什么说Java是半解释半编译型语言?

因为java程序在刚运行的时候,可以先使用解释器逐行编译,然后等后面发现热点代码后,就可以使用JIT即时编译,这样的话,再重复运行热点代码的时候,就不需要再去编译一次,直接可以使用JIT即时编译器编译好的,节约了大量的时间。我的理解就是可以一边解释,让程序快速的跑起来,然后一边做编译,这样下一次再运行这部分代码的时候就可以直接执行编译后的文件

为什么解释器依然存在?

  • 在虚拟机启动的时候,可以先试用解释器来进行编译执行,这样就省去了编译的时间,达到立即执行的效果
  • 采用解释器与即时编译器共存的架构来换取一个平衡点
  • 解释执行在编译器进行激进优化不成立的时候,作为编译器的备选方案。

2.3.3、JIT的热点探测功能

JVM根据代码被调用执行的频率,通过指定的条件判断是否需要通过JIT即时编译器将字节码直接编译成对应平台的机器码

热点代码

一个被调用执行很多次的方法,或者是一个方法体内循环了很多次的循环体都可以被称之为“热点代码”,需要被编译成本地代码的字节码也可以被称为热点代码,所以热点代码都可以通过JIT即时编译器编译成本地机器码。由于这种编译方式是在代码执行的时候执行的,因此也被称为栈上替换,或简称为OSR(On Stack Replacement)编译

热点探测的方式

HotSpot VM目前采用的热点探测方式是基于计数器的热点探测:

  • 为每个方法建立2个不同类型的计数器:方法调用计数器(Invocation Counter) 和回边计数器(Back Edge Counter);
    • 方法调用计数器:用于统计方法的调用次数;
      • 在Client模式下默认阈值是1500次,在Server模式下是10000次,超过阈值就会触发JIT编译
    • 回边计数器:用于统计循环体执行的循环次数;(方法体内有多个循环体怎么处理?)
  • 通过计算方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果已超过阈值,将会向即时编译器提交一个该方法代码编译请求。

热度衰减

  • 方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数;
  • 当超过一定的时间限度, 方法的调用次数不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay) ,而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)
  • 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的。
  • 虚拟机参数 -XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样只要系统运行时间足够长,绝大部分方法都会被编译成本地代码;
  • 虚拟机参数 -XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。

2.3.4、GC 垃圾回收器

直接看CSDN上面的一篇文章—> 图文详解JVM中的垃圾回收机制(GC)

看完总结(夹杂个人理解)

基于引用计数中循环引用的问题:

代码举例:

public class Test 
    // 成员变量
    Test t1 = null;


// 场景:
Test t2 = new Test();
Test t3 = new Test();
t2.t1 = t3; // 将t3对象的引用传递给t2对象中的成员变量t1
t3.t1 = t2; // 将t2对象的引用传递给t3对象中的成员变量t1

// 将t2 , t3设置为null
t2 = null;
t3 = null;

创建t2、t3对象,根据引用计数的原则,这两个对象的引用计数加一

因为t2中的成员变量跟t3的成员变量都引用了创建的两个对象,那么这两个对象的引用计数都变成2


此时,t2和t3都设置为null,取消了对象的引用,所以在堆中这两个对象实例的引用次数都减一。但是此时的两个对象还在被t2、t3的成员变量所引用,但是由于引用长在彼此的身上,外界的代码也无法访问到这两个对象,此时此刻,这俩对象,就不能使用,又不能释放,就出现了“内存泄露”的问题。


JVM常见的三种GC

  • Minor GC (新生代GC):只对新生代进行垃圾回收

    • 当新生代空间不足的时候就会触发Minor GC,这里的新生代空间不足是指新生代中的Eden区,Survivor区满不会触发GC
    • Minor GC采用的垃圾回收算法是标记-复制算法
    • Minor GC会引发STW(stop-the-world),暂停其他用户的线程,等垃圾回收完毕之后,用户的线程才正常运行
  • Major GC(老年代GC):只对老年代进行垃圾回收

    • 触发机制:当老年代空间不足的时候,会尝试先触发Minor GC , 如果还是空间不足,那就触发Major GC,Major GC后,空间如果还是不足那就抛出OOM错误
  • Full GC(整堆收集):收集整个java堆和方法区的垃圾收集

    • 调用System.gc(),系统建议执行Full GC,但不是必然执行。
    • 老年代空间不足。
    • 方法区空间不足。
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
    • from向to复制的时候,对象大小大于to的内存,则把对象转移到老年代,且老年代的的可用内存小于对象大小。

    在开发中尽量减少 Full GC , 减少Major GC 和 Full GC的发

参考资料:

资料一:关于JVM(基本常识)

资料二:类加载器的作用

资料三:JVM原理-超详细总结

JVM内存初学 堆栈方法区

转自:

http://www.open-open.com/lib/view/open1432200119489.html

这两天看了一下深入浅出JVM这本书,推荐给高级的java程序员去看,对你了解JAVA的底层和运行机制有
比较大的帮助。
废话不想讲了.入主题:
先了解具体的概念:
JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)

堆区:
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
方法区:
1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
为了更清楚地搞明白发生在运行时数据区里的黑幕,我们来准备2个小道具(2个非常简单的小程序)。
AppMain.java

 public   class  AppMain                

//运行时, jvm 把appmain的信息都放入方法区

{

public   static   void  main(String[] args)  //main 方法本身放入方法区。

{

Sample test1 = new  Sample( " 测试1 " );   //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面

Sample test2 = new  Sample( " 测试2 " );

test1.printName();

test2.printName();

}


Sample.java

 public   class  Sample        //运行时, jvm 把appmain的信息都放入方法区

{

/** 范例名称 */

private  name;      //new Sample实例后, name 引用放入栈区里,  name 对象放入堆里

/** 构造方法 */

public  Sample(String name)

{

this .name = name;

}

/** 输出 */

public   void  printName()   //print方法本身放入 方法区里。

{

System.out.println(name);

}


OK,让我们开始行动吧,出发指令就是:“java AppMain”,包包里带好我们的行动向导图,Let’s GO!
技术分享图片
系统收到了我们发出的指令,启动了一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取这个文件中的二进制数据,然后把Appmain类的类信息存放到运行时数据区的方法区中。这一过程称为AppMain类的加载过程。
接着,Java虚拟机定位到方法区中AppMain类的Main()方法的字节码,开始执行它的指令。这个main()方法的第一条语句就是:
Sample test1=new Sample("测试1");
语句很简单啦,就是让java虚拟机创建一个Sample实例,并且呢,使引用变量test1引用这个实例。貌似小case一桩哦,就让我们来跟踪一下Java虚拟机,看看它究竟是怎么来执行这个任务的:
1、 Java虚拟机一看,不就是建立一个Sample实例吗,简单,于是就直奔方法区而去,先找到Sample类的类型信息再说。结果呢,嘿嘿,没找到@@, 这会儿的方法区里还没有Sample类呢。可Java虚拟机也不是一根筋的笨蛋,于是,它发扬“自己动手,丰衣足食”的作风,立马加载了Sample类, 把Sample类的类型信息存放在方法区里。
2、 好啦,资料找到了,下面就开始干活啦。Java虚拟机做的第一件事情就是在堆区中为一个新的Sample实例分配内存, 这个Sample实例持有着指向方法区的Sample类的类型信息的引用。这里所说的引用,实际上指的是Sample类的类型信息在方法区中的内存地址, 其实,就是有点类似于C语言里的指针啦~~,而这个地址呢,就存放了在Sample实例的数据区里。
3、 在JAVA虚拟机进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个方 法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据。OK,原理讲完了,就让我们来继续我们的跟踪行动!位 于“=”前的Test1是一个在main()方法中定义的变量,可见,它是一个局部变量,因此,它被会添加到了执行main()方法的主线程的JAVA方 法调用栈中。而“=”将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用。
OK,到这里为止呢,JAVA虚拟机就完成了这个简单语句的执行任务。参考我们的行动向导图,我们终于初步摸清了JAVA虚拟机的一点点底细了,COOL!
接下来,JAVA虚拟机将继续执行后续指令,在堆区里继续创建另一个Sample实例,然后依次执行它们的printName()方法。当JAVA虚拟机 执行test1.printName()方法时,JAVA虚拟机根据局部变量test1持有的引用,定位到堆区中的Sample实例,再根据Sample 实例持有的引用,定位到方法去中Sample类的类型信息,从而获得printName()方法的字节码,接着执行printName()方法包含的指 令。

以上是关于JVM初学JVM的主要内容,如果未能解决你的问题,请参考以下文章

JVMJVM系列之JVM体系

JVM 基本概念

Java初识

JVM体系结构及其细节

JVM体系结构及其细节

JVMJVM内存结构