zoukankan      html  css  js  c++  java
  • 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、使用过程中内存分配

  • 相关阅读:
    knox 编译 源码
    springboot 新建项目
    python 在工程中处理相对路径的思考
    Mac配置虚拟环境Virtualenv,安装Python科学计算包详解
    Python 日志文件处理
    Django与JS交互的示例代码-django js 获取 python 字典-Django 前后台的数据传递
    python for 循环
    Java Script 学习日志 Div
    如何制作windows live writer绿色便携版
    中国大数据企业排行榜V6.0- 5 年后再去看看中几个大数据公司的发展状况
  • 原文地址:https://www.cnblogs.com/jetqiu/p/11756378.html
Copyright © 2011-2022 走看看