- 什么是运行时数据区
运行时数据区是jvm在执行java程序过程中,将它所管理的内存空间进行了不同区域的划分。
运行时数据区一共分为了五大块。
分别是 程序计数器、虚拟机栈、本地方法栈、方法区、堆。
其中:程序计数器、虚拟机栈、本地方法栈是线程私有的。
方法区、堆 是线程共享的。
1.1.程序计数器
程序计数器是一块较小的内存空间。它主要是用来记录当前线程所执行的字节码的行号(行号指示器),因为jvm中,多线程是通过线程轮流切换+分配执行时间的方式来实现的。
程序计数器的存在,可以保证线程切换回来后,还能正确地往后面继续去执行。
所以程序计数器是线程私有的、每个线程都有自己的计数器。
举个栗子:你正在房间看书、看到了某一页(当前线程在执行字节码),这时候来电话了,需要出门一趟(线程切换、暂停等),这时候你为了记住看到了哪里、就在书的当前页放入一个书签(程序计数器,记录了当前的进度),当你处理完事情回来后(线程切换回来),就可以根据书签的位置(程序计数器记录的行号),接着往下看。
1.2.java虚拟机栈
每当有办法执行的时候,就会在虚拟机栈中创建一个栈帧。栈帧的主要结构如下。
1.2.1.局部变量表
主要是用来存储在方法中定义的局部变量、包括基本类型、引用类型、方法的入参等。
public class VariableExample {
public void method() {
// 这是局部变量
int localVariable = 20;
Object ob = new Object();
System.out.println("Local variable: " + localVariable);
System.out.println("ob: " + ob);
}
public static void main(String[] args) {
VariableExample example = new VariableExample();
example.method();
}
}
示例代码中,method 办法中的 localVariable,ob 都会放在局部变量表中。
1.2.2.操作数栈
可以理解为一个临时的数据存储区域,用于存放计算过程中的中间数据,直到最终计算完成得到最终结果。它的类型是栈、所有会有压栈、弹栈的操作、并且有后进先出的特征。
.支持算术和逻辑运算。加减乘除、比较等
.方法调用时传递的参数
.类型转换
public class OperandStackDemo {
public static int add(int num1, int num2) {
int a = 10;
int b = 20;
int c =(num1+num2)*(a+b);
return c;
}
public static void main(String[] args) {
// 方法调用时传递参数
int result = add(8, 2);
System.out.println("Result of method call: " + result);
}
}
以上代码中,当执行 add方法时。入参 num1,num2放入了局部变量表。
int a = 10;
int b = 20;
也是直接存在局部变量表中。
程序判断到需要进行计算操作时,从操作数栈弹出num1,num2进行计算。得出的中间结果
result1再压入操作数栈。
然后直接去局部变量表取a,b的值做计算。得出的中间结果
result2再压入操作数栈。
最后再把result1,result2 弹出操作数栈。得到最终结果。然后赋值给c、放到局部变量表
然后返回。
1.2.3.动态链接
主要是JVM在运行时将符号引用(在编译阶段只知道要调用的方法的名称、所属的类等信息,并不知道这个办法在内存中的实际地址,因为实际地址只有在运行的时候才能确定)转换为直接引用。
class B {
public void methodInB() {
System.out.println("This is the method in B");
}
}
class A {
public void callMethodFromB() {
B bObj = new B();
bObj.methodInB();
}
public static void main(String[] args) {
A aObj = new A();
aObj.callMethodFromB();
}
}
以上代码中。A 的callMethodFromB 办法中,调用了B的methodInB办法。
编译的阶段、A 的callMethodFromB 办法中。只知道调用的是B的methodInB()。
但B的methodInB()实际的内存地址目前还是未知的。
程序运行的时候,通过动态链接的机制。可以找到methodInB()实际的内存地址,完成了从符号引用到直接引用的过程。
1.2.4.方法返回
指的是所执行方法的结束位置,方法执行完毕后,会把控制权交换给调用者,调用者可以在调用完方法后接着往下执行。保证了程序执行的逻辑性、正确性。
import java.util.Scanner;
public class DivisionDemo {
public static void main(String[] args) {
double result = divide(10, 0); // 调用除法方法
if (result != Double.NaN) {
System.out.println("Result: " + result); // 输出结果
}
scanner.close();
}
// 除法方法
public static double divide(double dividend, double divisor) {
// 被除数为 0 的情况
if (dividend == 0) {
System.out.println("被除数为 0,无法进行除法运算。");
return Double.NaN; // 方法出口 1: 返回 NaN 表示无效结果
}
// 除数为 0 的情况
if (divisor == 0) {
System.out.println("除数为 0,无法进行除法运算。");
return Double.NaN; // 方法出口 2: 返回 NaN 表示无效结果
}
// 正常的除法运算
double result = dividend / divisor;
return result; // 方法出口 3: 返回有效结果
}
}
在以上demo中,return Double.NaN; 是方法出口、return result; 也是方法出口。
方法出口分为两种:
1.正常出口:方法正常执行结束、交还控制权给调用者。
2.异常出口:方法运行出现异常后,没有被捕捉处理、就会一直往上抛、直至程序运行终止。
1.3.本地方法栈
虚拟机栈是为java虚拟机执行方法服务的、而本地方法栈是为虚拟机调用本地(native)方法服务的。这些本地方法一般是使用C,C++语言实现的。其结构划分与虚拟机栈类似、但是不受java虚拟机的约束管理、拥有自己的数据管理机制。在java代码中,一般会在办法中使用native 关键字来标识其为本地方法。注意:例如在hotspot虚拟机中,直接将虚拟机栈与本地方法栈合二为一。
public class NativeMethodExample {
// 声明一个本地方法
public native void nativeMethod();
static {
// 加载本地库,假设本地库名为 "nativeLibrary"
System.loadLibrary("nativeLibrary");
}
public static void main(String[] args) {
NativeMethodExample example = new NativeMethodExample();
// 调用本地方法
example.nativeMethod();
}
}
1.4.java堆
java堆是虚拟机所管理的内存空间中最大的一块,主要目的是用来存储对象与数组。在Hotspot(注意,不同虚拟机,堆的结构划分是不同的。只不过Hotspot是主流)虚拟机中,堆又划分为了新生代(划分比例大约为1/3)、老年代(大约为2/3)。
新生代中又划分了
Eden区:新对象创建的地方,大多数新创建的对象都会放在Eden区,当Eden空间满了的时候,会触发一 次MinorGc(新生代垃圾回收)
Survivor From区:又称S0区,当触发了MinorGc后,在Eden区仍存活的对象,会被复制到这里。
Survivor To 区:又称S1区,用于在进行垃圾回收时,与S0区来配合使用,用于对象的复制和交换,确保 在多次的垃圾回收过程中,存活较久的对象可以从新生代晋升为老年代。
老年代:用于存放存活较久的对象、以及大对象。一些大对象,在创建的时候就直接存放在这里。当老年 代的空间满了的时候,会触发一次FullGC(老年代垃圾回收)。
从以上可以看出,java堆实际上是由虚拟机的垃圾回收器管理的。
在java中,频繁的对象创建是十分常见的、且新创建的对象一般都是朝生夕灭的、存活时间短。所以MinorGc出现的频率较多,但由于处理的内存空间小,效率较快。
老年代中的对象要么大对象、要么是存活时间久的对象。相对来说,比较稳定、所以FullGc出现频率较少,但由于处理的内存空间大,所以处理时间是比MinorGc长,效率低。频繁的FullGC会影响系统的稳定性。
启动项目时,可以通过参数指定 堆的大小、新生代的大小等、
java -Xms1024m -Xmx2048m -Xmn512m -XX:NewRatio=2 -XX:MaxPermSize=256m xxx.jar
- -Xmx 和 -Xms 是最基本的堆大小设置参数,它们提供了堆的整体范围。
- -Xmn 可以精确设置新生代的大小,然后通过
-Xmx
和-Xms
计算老年代的范围。 - -XX:NewRatio 直接定义了老年代和新生代的比例,当堆大小固定时,能精确确定老年代和新生代的具体大小。
- -XX:MaxPermSize 主要影响 Java 7 及之前的永久代和 Java 8 及以后的元空间,在调整这个参数时,会对堆内存分配产生一定的间接影响,因为 Java 虚拟机会根据堆的大小和永久代 / 元空间的需求来平衡内存分配。
1.5.方法区
是一种规范、HotSpot虚拟机在java7及之前使用永久代来实现、java8后使用元空间来实现。
方法区存储的数据有:运行时常量池、类的元数据(类的全名、类的修饰符、父类、字段信息、方法信息等)、静态变量、类的加载信息等。
永久代的实现方式、因为内存空间较小、容易出现oom(内存溢出)。
所以hotspot 在java8后移除了永久代、使用元空间来代替、元空间使用的是本地内存,由操作系统管理内存、理论上来说、只会受到物理内存大小的限制。
关于内存溢出以及栈溢出
内存异常:当堆可用的内存空间无法满足新创建对象所需要的内存大小时,会抛出Out Of Memory Exception
内存泄露:不再使用的对象没有被垃圾回收器正确回收,导致可用内存越来越少,当可用内存用完的时 候,就会抛出Out Of Memory Exception。
当永久代/元空间的内存空间用完后,也会出现OOM。
栈溢出:由于虚拟机栈跟本地方法栈都是栈、所以当线程所需要的栈深度超过了虚拟机所允许的最大的栈 深度时,会抛出 Stack OverFlow Exception。
至于垃圾收集器是如何判断对象可不可用,
然后又是使用什么方法进行回收的。这是垃圾收集器的范畴的知识了。后续再讲。