Java虚拟机(JVM)与Java内存模型(JMM)学习笔记

Java虚拟机【JVM】与Java内存模型【JMM】学习笔记

Java虚拟机(JVM)

三种JVM

  • Sun公司:HotSpot Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
  • BEA公司:JRockit
  • IBM公司:J9RM

我们学习都是:HotSpot

JVM 位置

image-20200523205542396

JVM的主要组成部分及其作用

image-20200706225247441

JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。

  • Execution engine(执行引擎):执行classes中的指令。

  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。

  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

作用 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

Java程序运行机制步骤

  • 首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
  • 再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
  • 运行字节码的工作是由解释器(java命令)来完成的。

image-20200706225030519

​ .java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

类加载器

​ 类加载器把class文件中的二进制文件数据读入到内存中,存放在方法区,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类加载的步骤如下:

image-20200707153928110

  • 加载:根据查找路径找到相应的class文件然后导入
  • **验证:检查加载的class文件的正确性。**目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
  • **准备:给类中的静态变量分配内存空间。**为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
  • **解析:虚拟机将常量池中的符号引用替换成直接引用的过程。**符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析。
  • 初始化:对静态变量和静态代码块执行初始化工作。类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

在Java虚拟机中提供了3种类加载器:

  • Bootstrap ClassLoader(启动/根类加载器)
    • 使用C++实现编写,是虚拟机自身的一部分,程序员无法在程序中获取该类
    • 负责加载虚拟机核心的类库(如java.lang.*等)
    • 构造ExtClassLoader和APPClassLoader
    • 没有继承ClassLoader类
  • ExtensionClassLoader(扩展类加载器)
    • Java编写的
    • 从指定目录加载类库,主要负责加载jre/lib/ext目录下的一下扩展的jar
    • 是ClassLoader的子类
    • 如果用户把创建的jar文件放到指定目录中,也会被扩展加载器加载
  • AppClassLoader/SyetemClassLoader(应用/系统类加载器)
    • Java编写的
    • 父加载器是扩展类加载器
    • 从环境变量或者classpath中加载类,主要负责加载应用程序的主函数类
    • 是用户自定义类加载器的默认父加载器
    • 是ClassLoader的子类
  • 用户还可以自定义类加载器,通过继承java.lang.ClassLoader类的方式实现。
    • 用户可以定制类的加载方式,父类加载器是系统加载器
    • 编写步骤:继承ClassLoader,重写findClass方法,从特定位置加载class文件,得到字节数组,然后利用defineClass把字节数组转化为Class对象
    • 为什么要使用自定义类加载器?
      • 可以从指定位置加载class文件,比如说从数据库、云端加载class文件
      • 加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行编译,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。

image-20200523212320583

双亲委派机制

双亲委派机制图解

image-20200526094508569

​ 当一个Hello.class文件要被加载时。不考虑我们自定义类加载器,首先会在应用类加载器(AppClassLoader)中检查是否加载过该Hello类,如果有就无需再加载。如果没有,就会来到父加载器(ExtClassLoader、BootstrapClassLoader),然后调用父加载器的loadClass方法。父类中同理会先检查自己是否已经加载过,如果没有再往上。注意这个过程,直到BootstrapClassLoader之前,都没有哪个加载器自己选择加载的。如果父加载器无法加载,就向下通知子加载器去加载,一直到最底层(AppClassLoader应用类加载器),如果没有任何加载器能加载,就会抛出异常(ClassNotFoundException)。

双亲委派机制优点:如果有人想替换系统级别的类:例如String.java,篡改它的实现,在这种机制下这些系统的类已经被Bootstrap ClassLoader加载过了,所以并不会再去加载,从一定程度上防止了危险代码的植入。

​ 例如:我们自定义了java.lang.String 类,但是在双亲委派机制下,类加载器加载我们自定义的 String 类的时候会首先使用启动类加载器(BootstrapClassLoader)加载,而启动类加载器在加载的过程中会先加载 JDK 自带的文件(rt.jar 包中的 java.lang.String.class),而我们自定义的 java.lang.String 类中含有 main 方法,JDK 中的 String 类没有 main 方法,所以运行时会报错说没有 main 方法,就是因为加载的不是自定义的 String 类,这样可以保证对 java 核心源代码的保护。

沙箱安全机制

什么是沙箱?
 Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

所有的Java程序运行都可以指定沙箱,可以定制安全策略。

java中的安全模型:
 在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示

image-20200530174017851

JDK1.0安全模型

​ 但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示

image-20200530174205761

JDK1.1安全模型

​ 在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示

image-20200530175221973

JDK1.2安全模型

​ 当前最新的安全机制实现,则引入了**域 (Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。**虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示

image-20200530175859995

最新的安全模型

​ 以上提到的都是基本的 Java 安全模型概念,在应用开发中还有一些关于安全的复杂用法,其中最常用到的 API 就是 doPrivilegeddoPrivileged 方法能够使一段受信任代码获得更大的权限,甚至比调用它的应用程序还要多,可做到临时访问更多的资源。有时候这是非常必要的,可以应付一些特殊的应用场景。例如,应用程序可能无法直接访问某些系统资源,但这样的应用程序必须得到这些资源才能够完成功能。

Java本地接口(JNI)

JNI:Java平台提供给用户和本地C语言代码进行交互的接口,简称Java Native Interface(Java本地接口)

image-20200530181252792

​ **Java的方法上带native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由Java去调用。**这些函数的实现在DLL中,JDK的源代码中并不包含,JDK中是看不到的。对于不同的平台它们也是不同的。这是Java的底层机制,实际上Java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。

JNI作用:扩展Java的使用融合不同的编程语言为Java所用。Java在内存区域中专门开辟了一块标记区域:Native Method Stack用来登记native方法,在最终执行的时候,加载本地方法库中的方法,通过JNI调用。

native的意思就是通知操作系统,这个函数你必须给我实现,因为我要使用。所以native关键字的函数都是操作系统实现的,Java只能调用。Java是跨平台的语言,既然是跨了平台,所付出的代价就是牺牲一些对底层的控制,而Java要实现对底层的控制,就需要一些其他语言的帮助,这个就是native的作用了。

​ Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能。可以将native方法比作Java程序同C程序的接口,其实现步骤如下:
  1、在Java中声明native()方法,然后编译;
  2、用javah产生一个.h文件;
  3、写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件);
  4、将第三步的.cpp文件编译成动态链接库文件;
  5、在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。

JAVA本地方法适用的情况

  • 为了使用底层的主机平台的某个特性,而这个特性不能通过JAVA API访问
  • 为了访问一个老的系统或者使用一个已有的库,而这个系统或这个库不是用JAVA编写的
  • 为了加快程序的性能,而将一段时间敏感的代码作为本地方法实现。

JVM运行时数据区

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

image-20200707113017198

方法区

​ **方法区(Method Area)**是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间;静态变量(static)、常量(final)、类信息(Class)(构造方法、接口定义)、运行时的常量池、即时编译后的代码等数据存在方法区中,但是实例变量存在堆内存中,和方法区无关。

《Java虚拟机规范》只是规定了有⽅法区这么个概念和它的作⽤,并没有规定如何去实现它。那 么,在不同的 JVM 上⽅法区的实现肯定是不同的了。 ⽅法区和永久代的关系很像Java中接⼝和类 的关系,类实现了接⼝,⽽永久代就是HotSpot虚拟机对虚拟机规范中⽅法区的⼀种实现⽅式。 也 就是说,永久代是HotSpot的概念,⽅法区是Java虚拟机规范中的定义,是⼀种规范,⽽永久代是⼀ 种实现,⼀个是标准⼀个是实现,其他的虚拟机实现并没有永久代这⼀说法。

​ **堆(Java Heap)**是Java虚拟机中内存最大的一块,此区域的唯一目的就是存放对象实例,是被所有线程共享的,几乎所有的对象实例都在这里分配内存,存放new生成的对象和数组。它也是垃圾收集器管理的内存区域,因此很多时候称为“GC”堆。一个JVM只有一个堆内存,堆内存的大小是可以调节的。

clipboard.png

Java堆内存划分如图所示,分别为新生代、老年代(Old Memory)、永久代(Perm)。**在JDK1.8中,永久代被移除,使用元空间代替(MetaSpace)。**GC 垃圾回收主要在新生代和老年代。如果内存满了,就会报OOM(Java.lang.OutOfMemoryError:Java heap space),即堆内存不够。

  • 新生代

    • 使用复制清除算法(Copying算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。

    • 分为Eden(伊甸园区)、Survivor From、Survivor To,默认比例为8:1:1

    • 内存不足时发生Minor GC(轻GC)

    • 类诞生和成长的地方,甚至死亡。

    • 伊甸园区:所有的对象都是在伊甸园区new出来的。伊甸园区满了会触发一次轻GC(垃圾回收),还在使用的对象就不回收,存到幸存0区,不再使用的,就会被当作垃圾清理。伊甸园区和幸存区即新生代满了,就会触发一次重GC,把新生代还在使用的对象存到老年代,不再使用的对象当作垃圾清理掉。新生代和老年代都满了,会报OOM错误。

      真理:经过研究,99%的对象都是临时对象。

  • 老年代

    • 采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。
  • 永久代:用来存储类的元数据,也就是方法区。

    • 永久代的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。

    • MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中永久代的实现。不过元空间与永久代之间的最大区别在于:元空间并不在虚拟机中,而是使用本地内存。

    • 这个区域常驻内存的。用来存放JDK自身携带的Class对象、Interface元数据、Java运行时的一些环境或类信息等,这个区域不存在垃圾回收!关闭VM虚拟机就会释放这个区域的内存。

    • 例如:一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载,直到内存满,就会出现OOM。

    • jdk1.6之前:永久代,常量池在方法区;

    • jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中

    image-20200528192554705

    • jdk1.8之后:无永久代,常量池在元空间。

    为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?

    ​ 整个永久代有⼀个 JVM 本身设置固定⼤⼩上线,⽆法进⾏调整,⽽元空间使⽤的是直接内存,受本机 可⽤内存的限制,并且永远不会得到java.lang.OutOfMemoryError。你可以使⽤ -XX: MaxMetaspaceSize 标志设置最⼤元空间⼤⼩,默认值为 unlimited,这意味着它只受系统内存的限 制。 -XX:MetaspaceSize 调整标志定义元空间的初始⼤⼩如果未指定此标志,则 Metaspace 将根 据运⾏时的应⽤程序需求动态地重新调整⼤⼩。 当然这只是其中⼀个原因,还有很多底层的原因。

    运⾏时常量池

    ​ 运⾏时常量池是⽅法区的⼀部分。Class ⽂件中除了有类的版本、字段、⽅法、接⼝等描述信息外,还 有常量池信息(⽤于存放编译期⽣成的各种字⾯量和符号引⽤) 既然运⾏时常量池时⽅法区的⼀部分,⾃然受到⽅法区内存的限制,当常量池⽆法再申请到内存时会抛 出 OutOfMemoryError 异常。 JDK1.7及之后版本的 JVM 已经将运⾏时常量池从⽅法区中移了出来,在 Java 堆(Heap)中开辟了⼀ 块区域存放运⾏时常量池。

    image-20200528192935691

    image-20200715093731275

  • 堆内存的划分在JVM里面的示意图:

image-20200707145250300

程序计数器

​ **程序计数器:Program Counter Register:**每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,即将要执行的指令代码),在执行引擎读取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成。它是一个非常小的内存空间,几乎可以忽略不计。

虚拟机栈

虚拟机栈(Virtual Machine Stacks):虚拟机栈是Java执行方法的内存模型,每个方法被执行的时候,都会创建一个栈帧,把栈帧压入栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。

  • 栈帧:栈帧存储方法的相关信息,包含局部变量数表、返回值、操作数栈、动态链接
    • 局部变量表:包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。
    • 返回值:如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向方法调用指令后面的一条指令地址。
    • 操作数栈:操作变量的内存模型。操作数栈的最大深度编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的元素可以是任意的Java类型,包括long和double,32为数据占用帧空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。
    • 动态链接:每个栈帧都都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
  • 线程私有

本地方法栈

本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈式服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的。

堆栈的区别

物理地址
  • 堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-清除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记-压缩算法)

  • 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。

内存分别
  • 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。

  • 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

存放的内容
  • 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储

  • 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

静态变量放在方法区
静态的对象还是放在堆。
程序的可见度

堆对于整个应用程序都是共享、可见的。

栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。

深拷贝和浅拷贝

  • 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,

  • 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,

    使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。

  • 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。

  • 深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

Java对象创建过程

Java 中提供的几种对象创建方式:

Header 解释
使用new关键字 调用了构造函数
使用Class的newInstance方法 调用了构造函数
使用Constructor类的newInstance方法 调用了构造函数
使用clone方法 没有调用构造函数
使用反序列化 没有调用构造函数

下面是对象创建的主要流程:

image-20200708213904827

  1. 当虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经加载、连接和初始化。如果没有,就执行该类的加载过程。
  2. 为该对象分配内存。
    • 假设Java堆是规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器。那分配内存只是把指针向空闲空间那边挪动与对象大小相等的距离,这种分配称为”指针碰撞“。
    • 假设Java堆不是规整的,用过的内存和空闲的内存相互交错,那就没办法进行“指针碰撞”。虚拟机通过维护一个列表,记录哪些内存块是可用的,在分配的时候找出一块足够大的空间分配给对象实例,并更新表上的记录。这种分配方式称为“空闲列表”。
    • 使用哪种分配方式由Java堆是否规整决定。Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
    • 分配对象保证线程安全的做法:虚拟机使用CAS失败重试的方式保证更新操作的原子性。(实际上还有另外一种方案:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,TLAB。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完分配新的TLAB时,才进行同步锁定。虚拟机是否使用TLAB,由-XX:+/-UserTLAB参数决定)
  3. 虚拟机为分配的内存空间初始化为零值(默认值)
  4. 虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到对象的元数据信息、对象的Hash码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。
  5. 执行(init)方法,把对象按照程序员的意愿进行初始化

Java对象定位访问的方式

Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有句柄直接指针两种方式。

  • 指针: 指向对象,代表一个对象在内存中的起始地址。

  • 句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。

句柄访问

Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据对象类型数据各自的具体地址信息,具体构造如下图所示:

image-20200708212735143

直接指针

如果使用直接指针访问,引用 中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。

image-20200708212910219

  • 句柄访问优点:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
  • 直接指针优点:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot虚拟机中采用的就是这种方式。

Java实例化对象过程中的内存分配

问题引入

这里先定义一个很不标准的“书”类,这里为了方便演示就不对类的属性进行封装了。

class Book{
   
    String name;    //书名
    double price;   //价格
    public void getInfo(){
   
        System.out.println("name:"+name+";price:"+price);
    }
}

在这个类中定义了两个属性和一个方法,当然也是可以定义多个类和多个方法的。 类现在虽然已经定义好了,但是一个类要使用它必须要实例化对象,那么对象的定义格式有一下两种格式:

//声明并实例化对象: 类名称 对象名称 = new 类名称()
Book book = new Book();
//分步完成声明和实例操作: 
// |- 声明对象: 类名称 对象名称 = null;
Book book = null;
// |- 实例化对象: 对象名称 = new 类名称();
book = new Book();

对象属于引用数据类型,其和基本数据类型最大的不同在于引用数据类型需要进行内存分配,而关键字new主要的功能就是开辟内存空间,也就是说只要是使用引用数据类型就必须使用关键字new来开辟空间。有些时候我们需要对对象属性进行操作,那么其中的堆栈内存空间又是如何分配的呢?接下来我们来分析一下其中的过程。

堆内存与栈内存

如果想对对象操作的过程进行内存分析,首先要了解两块内存空间的概念:

  • 堆内存:保存每一个对象的属性内容,堆内存需要用关键字new才能开辟。
  • 栈内存:保存的是一块堆内存的地址。

堆内存很好理解,可能有人会有疑问为什么会有栈内存,举个例子,好比学校有很多教室,每个教室有一个门牌号,教室内放了很多的桌椅等等,这个编号就好比地址,老师叫小明去一个教室拿东西,老师必须把房间号告诉小明才能拿到,也就是为什么地址必须存放在一个地方,而这个地方在计算机中就是栈内存。

对象空属性

我们先实例化一个对象,并对其的属性不设置任何值

public class Test{
   
    public static void main(String args[]){
   
         Book book = new Book();
         book.
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

T Head

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

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

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

打赏作者

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

抵扣说明:

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

余额充值