Skip to main content

Java深耕——初章

· 13 min read
CheverJohn

Java内存区域

Java虚拟机运行时数据区.jpg

程序计数器

  1. 作用:当前线程所执行的字节码的行号指示器。
  2. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  3. 存在的意义:Java虚拟机的多线程是通过线程轮流切换实现的,类似于操作系统的时间片算法来分配核心(处理器)的执行时间。所以,为了线程每一次切换后能够恢复到之前正确的执行位置,每个线程就需要各自独立的程序计数器。
  4. 如果线程此时正在执行的是一个Java方法,计数器记录的即是正在执行的虚拟机字节码指令的地址;如果正在执行的是Nativie方法,这个计数器即是空(Undefined)。

Java虚拟机栈(就是大家常说的栈内存)

虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。

每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

大部分程序员把Java的内存区域划分为堆内存(Heap)和栈内存(Stack),事实上Java内存区域的划分远比这复杂。其中所指的“堆”在后边会聊到,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

局部变量表

局部变量表存放了编译期可知的8种基本数据类型(int、char、byte、short、long、double、boolean、float)、对象引用和returnAddress类型

这边插一句关于对象引用类型的分析解释:字如其意,是一种引用,就是C中的指针嘛,我们来看一个例子

int[] arr = new int[4];

=左边的arr是变量部分,=右边的是一个对象。

右边只是一个指向对象起始地址的引用指针。

左边的arr是变量,所以被存放在栈内存中。右边的是对象, 所以被存放在堆内存中,两者靠指针来维系关系。上面这句话应该这么来理解。

continue!继续聊局部变量表,8种基本数据类型里64位长度的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。

局部变量表所需的内存空间在编译期间就完成了分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是早就确定好的,而且我们在方法运行期间不会改变 局部变量表的大小。

continue!继续聊虚拟机栈,我们聊报错

报错

有两种:

  1. StackOverflowError异常:如果线程请求的栈深度大于虚拟机所允许的深度
  2. OutOfMemoryError异常:当我们使用的虚拟机栈的内存不够时就会出现这个报错

本地方法栈

与虚拟机栈作用相似 ,区别在于虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用的Native方法服务。

甚至于有的虚拟机(比如Sun HotSpot虚拟机)直接将本地方法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地方法栈区域也会抛出两种报错StackOverflowError和OutOfMemoryError。

Java堆

是Java虚拟机所管理的内存中最大的一块。在虚拟机启动时就被创建了。

唯一目的就是存放对象实例,只要我们new的对象实例都在这边分配了内存。

Java对可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx:最大多少内存空间,-Xms:最小多少内存空间 来控制)

如果堆中内存不够的话,将会抛出OutOfMemoryError异常,我称之为爆内存,并且在下面给出爆内存的具体例子:

public class 爆内存 {
public static void main(String[] args) {
int it = 20;
long[] arr = new long[100];
arr[99] = 33;
System.out.println(arr[99]);
}
}
//教你如何爆内存哈,在终端输入Java -Xmx256 爆内存

当然爆内存知识OutOfMemoryError异常三种情况中的一种,之后我也会仔细围绕这个好好讲讲,https://blog.csdn.net/z453588/article/details/83743837,可以先看看这个博客解解馋

方法区

与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

可以和Java堆一样不需要连续的内存 和可以选择固定大小或者可扩展外

还可以选择不识闲垃圾回收。相对而言,垃圾收集行为在这个区域很少见。但是并非是说数据进入方法区就永久存在了。

这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

运行时常量池

是方法区的一部分。

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征就是具备动态性

动态性:Java语言并不要求常量一定只能在编译期产生,运行期间也可能将新的常量放入池中。这种特性被开发人员用的较多的就是String类的intern()方法。

既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当无法再申请到内存时会抛出OutOfMemoryError异常。

直接内存

直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用来进行操作。这样在一些场景中能显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

既然是直接内存,那么这一部分内存是不会受到Java堆大小的限制的咯,

但是既然是内存,则肯定还是会受到本机总共内存的大小和处理器寻址空间的限制。

服务器管理员配置虚拟机参数时,一般会根据实际内存设置 -Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的限制和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

对象访问

上面都是从Java虚拟机的运行时数据区来讨论的,这边在讨论一个话题:对象访问是如何进行的呢?

对象访问是Java中的常客,即使是最简答的访问,也会涉及到Java栈、Java堆、方法区这三个最重要内存区域之间的关联关系。举个例子说明:

Object obj = new Object();

等号左边的一部分会被保存在Java栈的本地变量表里,我之前也有所涉及,作为一个reference类型数据出现,一个引用类型嘛。

而右边的部分会被存储到Java堆中去。形成一块连在一起的结构化内存(类似于数组一样的结构)

另外在Java堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。

对象访问的两种方法:使用句柄和直接指针

如果使用句柄访问方式,Java堆中将会划分出一块内存来作为句柄池,栈内存中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

通过句柄访问对象.jpg

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

通过直接指针访问对象.jpg

各有优势:

使用句柄访问方式的最大好处就是堆内存中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针就行了。

使用直接指针访问方式的最大好处是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此此类开销积少成多后会非常可观。

各种异常实战

明儿个再补充