Java -- 类的内存分析

本文详细介绍了Java中的JVM内存分布,包括栈、堆、方法区和运行时常量池。通过类的内存分析,阐述了对象创建过程及内存分配。特别提到了String类在内存中的处理方式,强调了构造器在对象初始化中的作用,并通过示例代码展示了内存分配错误可能导致的空指针异常。最后,文章总结了关键知识点,为进一步探讨Java内存管理奠定了基础。

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

前言

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




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

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、创建类

在这里插入图片描述


/**
 * 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);
    }
}

运行结果
在这里插入图片描述

但是在执行方法之前,当前的类已经放在方法区了。因此方法区是最先有数据的。
在这里插入图片描述
此时我们创建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已经初始化为“小明”
在这里插入图片描述
当我么在main方法中执行System.out.println(student.duration.couseName);的时候会报以下错误(空指针异常)
在这里插入图片描述
出现上面的情况是因为当前的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

内存图如下
在这里插入图片描述

String类

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

在这里插入图片描述

三、总结

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT自习小空间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值