在学习Java的时候,我们会对 JVM 有这样的一些疑问。
Java为什么会用到 JVM?
JVM的作用又是什么?
Java程序在运行的时候 JVM 如何对内存进行分配?

前言

我们之前在学习C/C++的时候我们需要关注内存管理的问题,在运行程序的时候,稍不留神就会出现内存溢出、内存泄漏等问题。而Java语言对内存的操作很具有安全性,Java运行程序时的内存分配全部交给 JVM (Java Virtual Machine(Java虚拟机)),从而实现“一次编写,到处运行”。但是,Java程序员把内存管理这个任务交给Java虚拟机,一旦出现内存问题,如果不了解虚拟机是怎么使用内存的,那么会是一项异常艰难的工作。



文章目录

  • 前言
  • 一、JVM内存分布(部分主要的)
  • 二、类的内存分析
  • 1、创建类
  • 2、代码运行流程
  • 构造器
  • String类
  • 三、总结




一、JVM内存分布(部分主要的)

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。

java夸进程读写内存 java进程内存分析_后端

1、栈
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、 操作数栈、 动态链接、 方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean、 byte、 char、 short、 int、float、 long、 double)、 对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
2、堆
对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
3、方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据。类加载器,xxx.class字节码文件存放在方法区中,方法区中存放的是代码片段。因为类是需要加载的,所以方法区是最先有数据的
4、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。 Class文件中除了有类的版本、 字段、 方法、 接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

栈内存都是连续的空间,易于管理
堆内存空间可以是不连续的,为创建对象带来了很大的灵活性
方法区会存放只有一份的数据,减少内存空间的占用

二、类的内存分析

1、创建类

java夸进程读写内存 java进程内存分析_java夸进程读写内存_02

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: SweiJ
 * Date: 2021-10-24
 * Time: 22:14
 */
class Couse {
    String couseName;
    public void acKnow() {
        System.out.println("couse.acKnow");
    }
}

class Student {
    String name;
    double height;
    int age;
    Couse duration;

    public Student() {
        this.name = "小明";
        System.out.println(name);
    }

    public void eat() {
        System.out.println("eat<>");
    }

    public void study() {
        System.out.println("study<>");
    }
}

public class TestDemo {

    public static void main(String[] args) {
        Student student = new Student();

        Couse couse = new Couse();
        student.duration = couse;
        couse.couseName = "Java程序设计";

        System.out.println(student.name + " " + student.height + " " + student.age + " " + student.duration);
        System.out.println(student.duration.couseName);
    }
}

2、代码运行流程

和C语言一样,Java语言也是从main方法开始。

public class TestDemo {

    public static void main(String[] args) {
        Student student = new Student();

        System.out.println(student.name + " " + student.height + " " + student.age + " " + student.duration);
        System.out.println(student.duration);
    }
}

运行结果

java夸进程读写内存 java进程内存分析_java夸进程读写内存_03

但是在执行方法之前,当前的类已经放在方法区了。因此方法区是最先有数据的。

java夸进程读写内存 java进程内存分析_java_04


此时我们创建Student类的一个对象,Student student = new Student();此行代码首先会为对象分配内存,调用Student类中合适的构造方法。如果在编写一个类时没有编写构造器, 那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是, 实例域中的数值型数据设置为 0、 布尔型数据设置为 false、 所有对象变量将设置为 null。

构造器

我们自己在Student类中提供一个构造方法,在构造Student类的对象时,构造器会运行,以便将实例域初始化为所希望的状态。

public Student() {
        this.name = "小明";
        System.out.println(name);
}

注意:
1、它的方法名和类名是相同的,且没有返回值。
2、构造器与其他的方法不同,构造器总是伴随着new操作符的执行被调用,不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
3、每个类可以有一个以上的构造器
4、构造器可以有0个、1个或多个参数
此构造方法我们只把name属性初始化为“小明”


方法放在栈内存中,当前程序,我们只创建一个student的变量,对象会放在堆内存中。此时栈内存中的student变量引用的就是堆中该对象的地址。该对象的name已经初始化为“小明”

java夸进程读写内存 java进程内存分析_java_05


当我么在main方法中执行System.out.println(student.duration.couseName);的时候会报以下错误(空指针异常)

java夸进程读写内存 java进程内存分析_java_06


出现上面的情况是因为当前的duration没有被赋值,他指向的空间为空。我们该如何对其进行赋值呢。因为duration的变量类型是Couse类,因此我们需要对其new一个对象。

public class TestDemo {

    public static void main(String[] args) {
        Student student = new Student();

        Couse couse = new Couse();
        student.duration = couse;
        couse.couseName = "Java程序设计";

        System.out.println(student.name + " " + student.height + " " + student.age + " " + student.duration);
        System.out.println(student.duration.couseName);
    }
}

此时couse这个变量会存放在main方法的栈帧中。该变量引用一个对象,该对象同样存放在堆内存中。
student.duration = couse;该语句将couse赋值给duration(duration和couse都是Couse类型),也就是将地址赋值给duration

内存图如下

java夸进程读写内存 java进程内存分析_java夸进程读写内存_07

String类

我们会在内存发现一个问题,String也是一个类,Student类中的name属于引用类型。那么会不会像duration一样,在main中new一个对象,然后再堆中存放这个对象呢?
其实Java对于这些不变的类都会存放在常量池里面。在常量池中,会存放一些整形和String,在Student类中name的值就存放在常量池中,而此时的name会引用该常量池存放值的地址,在couse类中的couseName的值也同样存放常量池中,couseName引用的是该值的地址。

java夸进程读写内存 java进程内存分析_java_08

三、总结

这些知识对类的内存进行分析,还没涉及到垃圾回收,以后还会补充。