Jvm原理剖析与调优之内存结构

一些不得不说的概念

 

JVM

JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。是运行java应用最底层部分。

JDK(Java Development kit)

整个Java的核心,包括了Java运行环境(Java Runtime Envirnment),一堆Java工具(编译,debug等)和Java基础的类库(rt.jar)。是开发java应用的基础。

JRE(Java Runtime Environment,Java运行环境)

运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。运行java应用的基础。

 

J2SE(Java 2 Platform,Standard Edition)。包含那些构成Java语言核心的类。比如:数据库连接、接口定义、输入/输出、网络编程

J2EE(Java 2 Platform,Enterprise Edition)。Enterprise Edition(企业版) J2EE 包含J2SE 中的类,并且还包含用于开发企业级应用的类。比如:EJB、servlet、JSP、XML、事务控制

J2ME(Java 2 Platform,Micro Editon)

 

 

主要JVM

首先,JVM是一套规范。很多公司均实现了各自的虚拟机。常见的有

HotSpot JVM(sun)
Jrockit JVM(BEA公司的JVM,应用于weblogic)
IBM JVM

Apache Harmony

其中,我们常用的是HotSpot JVM.

 

JVM结构

《Jvm原理剖析与调优之内存结构》《Jvm原理剖析与调优之内存结构》

 

第 一步(编译): 

创建完源文件之后,程序会先被编译为.class文件。Java编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后 引用,这个有点象make。如果java编译器在指定目录下找不到该类所其依赖的类的.class文件或者.java源文件的话,编译器话 报“cant find symbol”的错误。

第二步(运行):

java类运行的过程大概可分为两个过程:1、类的加载  2、类的执行。

需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

  1. 在 编译好java程序得到MainApp.class文件后,在命令行上敲java AppMain。系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为AppMain.class的二进制文件,将 MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。

  2. (java命令)然后JVM找到AppMain的主函数入口,开始执行main函数

  3. (类加载器)执行过程中,会创建对象。JVM会首先从方法区加载类信息和相关常量,class加载完毕之后,在堆上为对象分配内存,然后调用初始化实例,当然这时候实例保持指向class类型信息,这个信息保存在方法区中。

  4. (执行引擎)调用实例方法时,会根据引用找到对象信息,进而可定位对应的class类型信息,和方法表。

  5. (执行引擎)执行方法时,在虚拟机栈中进行,分配栈帧,随着入栈出栈,完成方法调用操作。

执行引擎

运行Java的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,他要么在执行字节码,要么在执行本地方法。一个线程可能通过解释或者使用芯片级指令直接执行字节码,或者间接通过JIT执行编译过的本地代码。我们上文讲到的main函数,也就是执行引擎的操作入口。

Class文件 

实际上,Class文件中方法的字节码流就是有JVM的指令序列构成的。每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。

iload_0    // 把存储在局部变量区中索引为0的整数压入操作数栈。

iload_1    // 把存储在局部变量区中索引为1的整数压入操作数栈。

iadd         // 从操作数栈中弹出两个整数相加,在将结果压入操作数栈。

istore_2   // 从操作数栈中弹出结果

JVM运行时数据区

1)程序计数器(线程私有)

当前线程所执行的字节码的行号指示器,通过改变这个计数器的值,确定下一条要执行的命令。分支,循环,跳转都需要它的支持。

它是线程私有的,每个线程都有专属于自己的程序记数器,线程之间互不影响,独立存储,保证了线程切换后,可以恢复到原先执行位置。

2)Java虚拟机栈(线程私有)

每个方法的执行,同时都会在虚拟机栈上创建一个栈帧。用于存储局部变量表,操作数栈,方法出口,动态链接等。一个方法的执行周期,同时也就对应着栈帧的出栈入栈操作。有时候方法的递归,会造成大量的栈帧,达到一定的深度,会报StackOverflowError异常。有一点需要说明:在编译器编译Java代码时,就已经在字节码中为每个方法都设置好了局部变量区和操作数栈的数据和大小。并在JVM首次加载方法所属的Class文件时, 就将这些数据放进了方法区。因此在线程调用方法时,只需要根据方法区中的局部变量区和操作数栈的大小来分配一个新的栈帧的内存大小,并堆入Java栈。

局部变量区: 用来存放方法中的所有局部变量值,包括传递的参数。这些数据会被组织成以一个字长(32bit或64bit)为单位的数组结构(以索引0开始)中。其中类 型为int, float, reference(引用类型,记录对象在堆中地址)和returnAddress(一种JVM内部使用的基本类型)的值占用1个字长,而byte, char和shot会扩大成1个字长存储,long,double则使用2个字长。

 操作数栈: 用来在执行指令的时候存储和使用中间结果数据。

帧数据区: 常量池的解析,正常方法返回以及异常派发机制的信息数据都存储在其中。

3)本地方法栈(线程私有)

与Java虚拟机栈类似,只不过该区域是为native方法提供服务。

4)方法区(Perm)(线程共享)

    存储已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。包含运行时常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容是在类加载后进入方法区运行时常量池中。

5)堆

《Jvm原理剖析与调优之内存结构》

 

堆是整个内存数据区最负责的部分,负责对象的创建。同时,垃圾回收的主要工作也在于此。堆又进一步进行细分,主要是为了满足垃圾回收。

堆的组成

Eden(伊甸园):对象创建的入口。

Survivor Space:用于保存在eden space内存池中经过垃圾回收后没有被回收的对象,也就是“幸存还活着”的对象。

幸存者0区(Survivor 0 space)和幸存者1区(Survivor1 space):当伊甸园的空间用完时,程序又需要创建对象;此时JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象 进行销毁工作。同时将伊甸园中的还有其他对象引用的对象移动到幸存者0区。幸存者0区就是用于存放伊甸园垃圾回收时所幸存下来的JAVA对象。

当将伊甸园中的还有其他对象引用的对象移动到幸存者0区时,如果幸存者0区也没有空间来存放这些对象时,JVM的垃圾回收器将对幸存者0区进行垃圾 回收处理,将幸存者0区中不在有其他对象引用的JAVA对象进行销毁,将幸存者0区中还有其他对象引用的对象移动到幸存者1区。幸存者1区的作用就是用于 存放幸存者0区垃圾回收处理所幸存下来的JAVA对象。

 

Tenured :对象经过survivor 1 space内存池,每经历过一次垃圾回收,年龄就增加1,超过设定阀值后,被移入终身代,当然也包括由于担保机制移入的对象。

对于新生代和老年代,垃圾回收器对其态度不同。

发生在新生代的回收频率频繁,大部分对象是“朝生夕死”,收集算法一般采用高效简单的复制算法,也就是上文描述的对象转移操作(Eden->survivor 0,survivor 0->survivor 1)。发生在该区域的垃圾回收为Young GC.

对于老年代,由于大部分对象主要为存活率高的对象,垃圾回收器采用”标记-整理“算法。发生在该区域的垃圾回收为FULL GC.

 

堆相关参数

(影响堆空间划分,进而会影响GC发生频率)JVM调优工作,主要是基于这些参数,进行适当调整管理,达到调整堆内存大小及比例大小,以满足实际业务需求。另外还包括方法区。

-Xms:设置 Java 应用程序启动时的初始堆大小;

-Xmx:设置 Java 应用程序能获得的最大堆大小;

-Xss:设置线程栈的大小;

-XX:MinHeapFreeRatio:设置堆空间最小空闲比例。当堆空间的空闲内存小于这个数值时,JVM 便会扩展堆空间;

-XX:MaxHeapFreeRatio:设置堆空间的最大空闲比例。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆;

-XX:NewSize:设置新生代的大小;

-XX:NewRatio:设置老年代与新生代的比例,它等于老年代大小除以新生代大小;

-XX:SurvivorRatio:新生代中 eden 区与 survivor 区的比例;

-XX:MaxPermSize:设置最大的持久区大小;

-XX:TargetSurvivorRatio: 设置 survivor 区的可使用率。当 survivor 区的空间使用率达到这个数值时,会将对象送入老年代。

 

对象的生命周期

  创建阶段

1)检查指令的参数,是否能在常量池中定位到一个类的符号引用,如果是引用,判断代表的类是否加载,解析和初始化过

2)如果没有加载,则必须进行加载,解析和初始化

3)类加载检查,这时候已经知道所需内存的大小。

4)分配内存。从java堆中划分一块大小确定的内存。支持2种方式,至于选择哪种方式分配内存,与java堆是否规整有关(也就是是否空间空间和使用空间相互交错情况)。1.指针碰撞(分界点的指示器移动);2.空闲列表方式。然而,java堆是否规整,则取决于垃圾收集器的工作方式。此外,在分配内存时还要考虑多线程情况,保证原子性。分配内存的原子性有2种方式进行保证(CAS 和 本地线程分配缓冲-XX +/- UseTLAB)。

5) 分配内存完成后,初始化内存空间(初始化为0)

6)维护对象的对象头信息。如元数据信息,哈希码,GC分代年龄,锁信息,类元指针。

7)调用init方法,按照程序员意愿进行初始化。

     <7.1> 从超类到子类对static成员进行初始化; 
     <7.2> 超类成员变量按顺序初始化,递归调用超类的构造方法; 
       <7.3> 子类成员变量按顺序初始化,子类构造方法调用。

应用阶段

分为强引用、软引用、虚引用、若引用

 

   不可视阶段; 

当一个对象处于不可视阶段,说明我们在其他区域的代码中已经不可以在引用它,其强引用已经消失,例如,本地变量超出了其可视的范围。 

   不可到达阶段; 

处于JVM对象生命周期不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量, 所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其 实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。

   可收集阶段、终结阶段、释放阶段 ; 

当一个对象处于可收集阶段、终结阶段与释放阶段时

     <1> 回收器发现该对象已经不可达。 
     <2> finalize方法已经被执行。 
     <3> 对象空间已被重用。

 

from: https://yq.aliyun.com/articles/52426?spm=5176.100239.blogcont27348.13.v0xllM

    原文作者:不会就问咯
    原文地址: https://www.cnblogs.com/KingIceMou/p/6967504.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞