【七】GC、类加载机制,以及内存(内存部分)

内存部分LZ认为还是要以记忆为主。

1、首先会涉及到的问题一般是内存分为哪几个部分?

内存根据区域是否是线程安全会分为线程共享区域和非线程共享区域。线程共享区域下有JAVA堆以及方法区。而非线程共享区有虚拟机栈、本地方法栈和程序计数器。

我们都知道一个Java程序会被编译成为一个字节码文件,而每个字节码文件都需在JVM上运行,然后告知一个JVM入口,随后JVM会将通过字节码解释器加载运行。那么接下来LZ便通过程序运行过程中的涉及的各个内存区域简要概括一下这几个区域。

首先JVM在运行时会先分配好Java堆和方法区,而JVM每遇到一个线程便会为其分配一个虚拟机栈、本地方法栈以及程序计数器。当程序终止之后,非线程共享区便会被释放掉。故而Java堆和方法区的生命周期和JAVA程序生命周期一致,而其余三个则和其所属线程的生命周期相同。

而这也是GC也只发生在线程共享的区域。

程序计数器

这部分的内存空间较小,可以看作是当前线程所执行的字节码的行号指示器,通过改变这个计数器来选取下一条需要执行的字节码指令。其为线程私有,故而每个线程都有其特定的程序计数器。

当线程执行的是Java方法时,计数器中记录的是正在执行的虚拟机字节码指令地址;当线程执行的是Native方法,则计数器为空。

这个区域是在JVM中没有规定过OutOfMemoryError的区域。

Java虚拟机栈

每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链表和方法出口等等。一个方法从调用到结束就代表着一个帧栈到入栈和出栈的过程。这部分为线程私有,生命周期和线程生命周期一致,故而不存在GC机制,在线程结束便会释放线程。

在局部变量表中存放着编译期可知的基本数据类型、引用对象以及returnAddress(下一条字节码指令的地址)。而局部变量表的所需内存空间在编译期便会完成分配。即进入这个方法,为其分配多大的局部变量空间是完全确定,在方法运行期间不会改变。

当方法被调用时便产生一个帧栈,然后被压入栈中。此过程秉承FILO原则。故而方法只有被调用时才会占用内存空间。

(图片引用自https://www.cnblogs.com/wangjzh/p/5258254.html

该部分区域会出现StackOverFlowError(当线程请求的栈深度大于虚拟机所允许的深度时)和OutOfMemoryError(虚拟机栈可动态扩展,但扩展无法申请到足够的内存)

本地方法栈

该区域与虚拟栈作用类似,唯一的区别在于虚拟机栈服务于执行的Java方法,而本地方法栈服务于Native方法。

说完线程私有的区域,下面便是线程共享的Java堆和方法区

Java堆

这一部分是JVM所管理的内存中最大的一块区域。该区域在虚拟机启动时创建,目的是存放对象的实例,该区域是垃圾收集器管理的主要区域。也被称为GC堆,处理逻辑连续的内存空间,可扩展。

而当堆中内存没有完成实例分配,且堆也无法再扩展时,会出现OutOfMemoryError。

方法区(又被称为永久代)

该区域用于存储已被虚拟机加载的类信息、常量、静态变量。无需连续的内存空间,可拓展,也可不实现垃圾回收。而内存回收的目标主要是针对常量和对类的卸载。

该部分区域当内存分配无法满足需求时出现OutOfMemoryError。

该区域中有一块很重要的部分:运行时常量池,Java语言不要求常量仅在编译期产生,也可在运行时产生,例如String的intern方法。


接下来我们会通过一个例子来分析JVM的如何运行的(来自https://www.cnblogs.com/wangjzh/p/5258254.html

public class JVMShowcase {
    //静态类常量,
    public final static String ClASS_CONST "I'm a Const";
    //私有实例变量
    private int instanceVar=15;
    public static void main(String[] args) {
//调用静态方法
        runStaticMethod();
//调用非静态方法
        JVMShowcase showcase=new JVMShowcase();
        showcase.runNonStaticMethod(100);
    }
    //常规静态方法
    public static String runStaticMethod(){
        return ClASS_CONST;
    }
    //非静态方法
    public int runNonStaticMethod(int parameter){
        int methodVar=this.instanceVar * parameter;
        return methodVar;
    }
}

上述该例子来自上述链接博主。下述阐述也大体来自于该博主

JVM会先向操作系统申请内存,申请到后对自身堆和栈内存进行分配,分配好之后会检查class文件是否存在错误,如若存在则返回错误信息。随后JVM通过类加载器将类信息加载到方法区中。方法区中此时除了类信息之外,还有静态方法main方法和runStaticMethod方法。随后执行main方法,创建线程。Java堆中出现object和showcase两个对象。(object时showcase父类


随后便是对象创建的过程:

对象的创建存在多种方法:在【六】中介绍过。例如通过new关键字,调用类的构造函数显式创建;或利用反射机制,class类的newInstance或者construtor类的newInstance方法;还有类实现了Cloneable接口则可以使用clone方法。

当对象被创建之后,虚拟机会为其分配内存存放对象自身的实例变量以及其从父类继承过来的实例变量。在这些实例变量分配内存的同时,这些实例变量会被赋予零值对象头会在对象初始化之前设置在分配内存完成之后,虚拟机便开始为对象初始化。

对象头分为两部分:存储对象自身运行时数据,例如GC分代年龄和hashcode等等;另一部分时类型指针,对象指向其类元数据的指针,通过这个指针确定该类属于哪个类的实例,如果是数组的话则还有一块记录数组长度的数据。

对象初始化主要设计三种执行对象初始化的结构:

【1】首先是实例话变量初始化与实例代码块初始化

在定义(声明)实例变量的同时,还可以直接对实例变量进行赋值或使用实例代码块对其进行赋值。若使用这两种方式为实例变量初始化,那他们将在构造函数执行之前完成这些初始化操作。

事实上,使用上述两种方式进行赋值,编译器是将其中的代码放到类的构造函数中去。且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身执行之前。(代码块中的变量要先被定义)

【2】构造函数初始化

java要求在实例化类之前,必须先实例化其超类,以保证所创建的实例的完整性。

该过程是一个典型的递归过程:

父类实例化--->各子类依次实例化

实例变量初始化-->实例代码块初始化-->构造函数初始化

package com.vsw;

public class Bar extends  Foo{

    int j1;

    Bar(){
        2;
    }

    {j=3;}

    @Override
    protected int getValue() {
        return j;
    }

    public  static  void main(String[] args){
        Bar bar = new Bar();

        System.out.println(bar.getValue());
    }

}
class Foo{
    int 1;

    public Foo() {
        System.out.println(i);

        int x = getValue();

        System.out.println(x);
    }

    {
        i=2;
    }

    protected int getValue(){

        return  i;
    }

}

上面这个例子是《深入浅出JAVA虚拟机》的例子。输出的结果为2、0、2.

这个例子非常清晰的体现出了对象初始化的过程。

对象在创建之后,我们需要通过栈上的reference数据来操作堆上的具体对象。reference类型可通过使用句柄或者直接指针的方法定位访问堆中的对象的具体位置。

使用句柄访问:java堆中将会划分出一块内存来存放句柄池,reference中存储的便是对象的句柄地址,句柄中包含了对象的实例数据与类型数据各自的具体地址信息。

直接指针访问则reference中存储的便是对象的地址。该种方法定位的速度更快,节省了一次指针定位定位的时间开销。

猜你喜欢

转载自blog.csdn.net/qq_32302897/article/details/81299338