深入了解jvm之运行时数据区(一)

  1. 什么是运行时数据区
    运行时数据区是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。

至于垃圾收集器是如何判断对象可不可用,
然后又是使用什么方法进行回收的。这是垃圾收集器的范畴的知识了。后续再讲。




 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值