jvm内存模型,java类从编译到加载到执行的过程,jvm内存分配过程

Posted jet_qiu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jvm内存模型,java类从编译到加载到执行的过程,jvm内存分配过程相关的知识,希望对你有一定的参考价值。

一、jvm内存模型

JVM 内存模型主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈

1、堆

1.1、堆是 JVM 内存中最大的一块内存空间。

1.2、该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。

1.3、堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。

2、程序计数器(Program Counter Register)

程序计数器是一块很小的内存空间,用来记录下一条运行的指令(实际是记录各个线程执行的字节码的地址,由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。),例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。

3、方法区(Method Area)

3.1、方法区!=永久代,根据虚拟机类型而定;HotSpot 虚拟机使用永久代来实现方法区,但在其它虚拟机中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一说。

3.2、方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息(类的版本、字段、方法、接口和父类等信息)、运行时常量池、字符串常量。

ps1:JVM 在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。在加载类的时候,JVM 会先加载 class 文件,而在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用()。

ps2:字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量)。

ps3:符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、变量的名称和描述符以及方法的名称和描述符。

ps4:当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中。

ps5:在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。

3.3、方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。

3.2、在 Java6 版本中,永久代在非堆内存区;到了 Java7 版本,永久代的静态变量和运行时常量池被合并到了堆中;而到了 Java8,永久代被元空间取代了,并且元空间的存储位置是本地内存,永久代的静态变量(class static variables)以及运行时常量池(runtime constant pool)则跟 Java7 一样,转移到了堆中。如下图:

ps1:java8用直接内存元空间替代永久代的好处

1-1、移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。

1-2、永久代内存经常不够用或发生内存溢出,爆出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

4、虚拟机栈(VM stack)

4.1、Java 虚拟机栈是线程私有的内存空间,它和 Java 线程一起创建。

4.2、用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回。每一个方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出栈操作。

5、本地方法栈(Native Method Stack)

5.1、本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。

5.2、本地方法并不是用 Java 实现的,而是由 C 语言实现的。

 

二、java类从编译到加载到执行的过程

1、编译

通过jdk自带的javac工具完成

2、类加载

2.1、当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。

2.2、不同的实现类由不同的类加载器加载,JDK 中的本地方法类一般由根加载器(Bootstrp loader)加载进来,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现加载,而程序中的类文件则由系统加载器(AppClassLoader )实现加载。

2.3、在类加载后,class 类文件中的常量池信息以及其它数据会被保存到 JVM 内存的方法区中。

3、类连接

类在加载进来之后,会进行连接、初始化,最后才会被使用。在连接过程中,又包括验证、准备和解析三个部分。

3.1、验证

验证类符合 Java 规范和 JVM 规范,在保证符合规范的前提下,避免危害虚拟机安全。

3.2、准备

为类的静态变量分配内存,初始化为系统的初始值。对于 final static 修饰的变量,直接赋值为用户的定义值。例如,private final static int value=123,会在准备阶段分配内存,并初始化值为 123,而如果是 private static int value=123,这个阶段 value 的值仍然为 0。

3.3、解析

将符号引用转为直接引用的过程。我们知道,在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用,包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。如果要使用这些类和方法,就需要把它们转化为 JVM 可以直接获取的内存地址或指针,即直接引用。

3.4、类初始化

编译器会在将 .java 文件编译成 .class 文件时,收集所有类初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 <clinit>() 方法。在这个阶段中,JVM 首先将执行构造器 <clinit> 方法

初始化类的静态变量和静态代码块为用户自定义的值,初始化的顺序和 Java 源码从上到下的顺序一致。例如:

private static int i=1;
static{
  i=0;
}
public static void main(String [] args){
  System.out.println(i);
}

运行结果为:0

子类初始化时会首先调用父类的 <clinit>() 方法,再执行子类的 <clinit>() 方法,运行以下代码:public class Parent{

public static String parentStr= "parent static string";
  static{
    System.out.println("parent static fields");
    System.out.println(parentStr);
  }
  public Parent(){
    System.out.println("parent instance initialization");
 }
}

public class Sub extends Parent{
  public static String subStr= "sub static string";
  static{
    System.out.println("sub static fields");
    System.out.println(subStr);
  }

  public Sub(){
    System.out.println("sub instance initialization");
  }
 
  public static void main(String[] args){
    System.out.println("sub main");
    new Sub();
 }
}


运行结果为:

parent static fields
parent static string
sub static fields
sub static string
sub main
parent instance initialization
sub instance initialization

JVM 会保证 <clinit>() 方法的线程安全,保证同一时间只有一个线程执行。

JVM 在初始化执行代码时,如果实例化一个新对象,会调用 <init> 方法对实例变量进行初始化,并执行对应的构造方法内的代码。

类初始化完后就可以正式使用了,比如创建对象等。

4、运行时编译

见:https://www.cnblogs.com/jetqiu/p/11669168.html

 

三、jvm内存分配过程

1、JVM 根据配置配置或默认参数向操作系统申请内存空间

2、JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小

3、class 文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值

4、使用过程中内存分配

以上是关于jvm内存模型,java类从编译到加载到执行的过程,jvm内存分配过程的主要内容,如果未能解决你的问题,请参考以下文章

JVM内存模型

Java的类加载过程

Java内存模型及性能优化

jvm类加载

JVM类加载机制

Jvm类加载的过程