从事Java开发的小伙伴都知道Java有八种基本数据类型,分别是byte
, boolean
,char
,short
,int
,float
,long
,double
。其中各个数据类型所占的字节数如下图所示:
在这里插入图片描述 基本数据类型的变量保存的是原始值,即:它代表的值是数值本身。
与基本数据类型不同的就是引用数据类型,Java中所有的对象都是引用类型,包括基本类型的包装类以及String类。引用类型的变量保存的是引用值,引用值代表了某个对象的引用,而不是对象本身,对象本身是放在引用值所代表的位置,对象是保存在堆上的,这个后面会详细说。
在这里插入图片描述 堆和栈是程序运行的关键,我们需要记住的是:栈是运行时的单位,解决的是程序运行的问题,即程序如何执行,或者如何处理数据,堆是存储单位,解决的是数据存储的问题,即数据怎么放,放哪儿。 在Java中一个线程就会有一个相应的线程栈与之对应,因为不同的线程执行逻辑不同,所以需要独立的线程栈。栈因为是运行单位,因此里面存储的信息都是当前线程(或程序)相关的信息。包括局部变量、程序运行状态、方法返回值等;而堆只负责存放对象信息。 我将通过如下这段代码,展示程序运行时栈的存储情况。
public static void main(String[] args) {
int a = 1;
int ret = 0;
int res = 0;
ret = add(3, 5);
res = a + ret;
printf("%d", res);
}
int add(int x, int y) {
int sum = 0;
sum = x + y;
return sum;
}
如上,这段代码 main()
方法调用了add()
方法,获取计算结果。并且与临时变量a相加,最后打印res的值。运行main()
方法之后,我们可以画出方法的调用栈,如下图所示:
在这里插入图片描述
对于有sum = x + y
这种有符号的运算,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。
堆中存的是对象,栈中存的是基本数据类型和堆中对象的引用,一个对象的大小不可估计或者说可以动态变化的,但是在栈中,一个对象只对应一个4byte的引用。 为啥不把基本类型放在堆中呢?因为其占用的空间一般是1~8个字节---需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况,长度固定,因此栈中存储就够了,如果把他存在堆中没有什么意义。可以说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据,下面是一个常见的问题是。
Java在方法调用传入参数时,因为没有指针,所以它都是进行传值调用,基本类型和引用类型的处理是一样的,都是传值。所以,如果是传引用的方法调用,可以理解为传引用值的传值调用,即引用的处理和基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序查找到堆中的对象,这个时候对应到真正的对象,如果此时进行修改,修改的就是引用对应的对象,而不是引用本身,即:修改的是堆中的数据,所以这个修改是可以保持的。 对象,,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性如果还是对象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。 程序参数传递时,被传递的值本身都是不能修改的,但是如果这个值是一个非叶子节点(即一个对象引用),则是可以修改这个节点下面的所有内容的。
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
在这里插入图片描述 这些组成部分一些是线程私有的,其他的则是线程共享的。 线程私有的有:1. 程序计数器;2.虚拟机栈;3.本地方法栈 线程共享的有:1. 堆;2:方法区;3.直接内存
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 程序计数器是唯一不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
Java虚拟机栈也是线程私有的,他的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 虚拟机栈中的局部变量表存放了编译器可知的各种Java虚拟机基本数据类型,对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用类型指针,也可能指向一个达标对象的句柄或者其他与此对象相关的位置)以及returnAddress类型(指向了一条字节码指令的地址)。Java虚拟机规范中,对Java虚拟机栈这个内存区域规定了两类异常状况,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
Java堆是虚拟机所管理的内存中最大的一块。Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放实例。Java堆是垃圾收集器管理的内存管理区域,因此也被称为GC堆, 从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:在细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在这里插入图片描述 在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
方法区(Method Area)和Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用) 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。 JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。 JDK1.4中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本文首先介绍Java中的数据类型,无非就是八种基本数据类型和引用类型,接着重点介绍了堆和栈的知识,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得Java的垃圾回收成为可能。接着就是详细介绍了JVM执行Java程序时将内存划分的区域。主要还是两类,一类是线程私有的,生命周期与线程的生命周期相同, 一类是线程共享的。主要是Java堆,这一块是垃圾收集器管理的主要区域。
《深入理解Java虚拟机》 第2章第2节