深入解析Java内存区域:从程序计数器到本地方法栈的全面指南

一、Java内存区域 1、Java内存结构

深入解析Java内存区域:从程序计数器到本地方法栈的全面指南

内存结构

当前线程执行的字节码的行号指示器。如果当前方法是本机方法,则程序计数器的值未定义。

线程私有,Java内存区域中唯一不会发生OOM或StackOverflow的区域。

它通常被称为Java堆栈,它存储堆栈帧。局部变量表和其他信息存储在堆栈帧中。方法的执行到最后对应于栈帧的入栈到出栈。

线程私有,就会发生StackOverflow。

虚拟机栈的功能是一样的,只不过虚拟机栈是为Java方法服务的,而本地方法栈是为虚拟机调用Native方法服务的。

线程私有,就会发生StackOverflow。

Java虚拟机中最大的一块内存,几乎所有的对象实例都在这里分配内存。

它被所有线程共享,会发生OOM。

也称为非堆,用于存储虚拟机已加载的类信息、常量、静态变量和即时编译的代码等数据。

它被所有线程共享,会发生OOM。

它是方法区的一部分,存储常量(例如静态最终修饰,例如 String 字符串)和符号引用。

它被所有线程共享,会发生OOM。

2、创建对象时的堆内存分配算法

前提是堆内存必须绝对整齐。

所有已使用的内存放置在一侧,未使用的内存放置在另一侧,并且划分点的指示器放置在中间。当一个物体诞生时,它的大小就已经知道了。只需将指示器移至未使用的内存一侧即可。与对象大小相同的内存区域就足够了。

指针碰撞

如果堆内存不整齐,那么空闲列表是最合适的。

JVM 维护一个列表来记录哪些内存块可用。当一个对象被创建时,它会在链表中找到足够大的空间来分配给新对象,并将这块内存标记为已用内存。

空闲列表

3、对象在内存中的存储布局

分为三个部分:

包含两部分:它自己的运行时数据和类型指针。

自身的运行时数据包括:hashcode、gc分代年龄、锁状态标识符、线程持有的锁、偏向线程ID、偏向时间戳等。

对象指针是对象指向其类元数据的指针。虚拟机使用该指针来确定该对象是哪个类的实例。

用于存储对象的真实有效信息(包括从父类继承的和自定义的)

JVM要求对象的起始地址必须是8字节的整数倍(8字节对齐),所以如果不够的话就会用这部分来补充。

4. 如何定位物体

有以下两种类型。使用哪一种取决于 JVM。热点虚拟机采用直接指针的方式来定位对象。

堆栈上的引用直接指向堆中的对象。优点是速度快。没有额外费用。

Java堆会分离出一块独立的内存空间作为句柄池。这样,栈上的引用存储的是句柄地址而不是真正的对象地址,句柄中包含了对象的实例数据等信息。这样做的好处是,即使对象在堆中的位置发生了移动,堆栈上的引用也不需要改变。因为中间有一个把手。

5.判断对象是否可以回收的算法

向对象添加引用计数器。每当有对它的引用时,计数器的值就+1。当引用过期时,计数器的值为-1;当计数器的值为0时,jvm确定该对象。是一个垃圾对象。存在内存泄漏bug,比如循环引用,因此JVM虚拟机采用可达性分析方法。

有一些根节点GC Roots作为对象的起点。从这些节点开始向下搜索。搜索所经过的路径称为参考链。当一个对象没有任何引用链连接到GC Roots时,就证明该对象是垃圾。目的。

补充:哪些可以作为GC Root?

6. 如何判断一个对象是否可以被回收 7. Java堆内存的组成

堆组件

堆大小=年轻代+老年代。如果是Java8,就没有Permanent Generation。 Java8 用 Metaspace 取代了这个区域。

新生代(Young)分为Eden、S0(from)和S1(to)。

默认情况下 Edem : from : to = 8 : 1 : 1,这个比例可以通过 –XX:SurvivorRatio 设置

8. 什么时候抛出StackOverflowError?

当方法运行时栈深度超过虚拟机允许的最大深度时,不推荐递归的原因之一也是因为如此。效率低下,如果返回死了很容易导致StackOverflowError。

9. Java中是否存在内存泄漏?请简要描述。

虽然Java会自动GC,但是如果使用不当,仍然可能会发生内存泄漏,比如ThreadLocal忘记remove时。 (ThreadLocal太长,不适合放在这里,懂的人都懂,不懂的谷歌)

10.什么是栈帧?包含什么

栈帧存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧中的局部变量表存储的是基本类型+对象引用+返回地址。局部变量所需的内存空间是在编译期间分配的。因为基本类型和对象引用可以决定占用多少个槽,并且这个大小在运行时无法更改。

11.简述一个方法的执行过程

方法的执行到最后其实就是从栈帧到栈的过程。方法的局部变量将存储在栈帧的局部变量表中。如果是递归的,总是会被压入栈中。执行后会弹出。所以效率低,因为总是压栈,而且栈很深。

12、方法区会被回收吗?

方法区的回收价值很低,主要是回收废弃的常量和无用的类。

如何识别无用的类:

13. 一个对象包含多少个字节?

将占用16个字节。例如

对象 obj = 新对象();

因为obj引用在栈上占用4个字节,新对象在堆上占用8个字节,4+8=12,但对象要求都是8的倍数,所以对象的字节对齐(Padding)部分会补全4个字节,占用16个字节。

另一个例子:

public class NewObj {
    int count;
    boolean flag;
    Object obj;
}
NewObj obj = new NewObj();

这个对象的大小为:空对象8个字节+int类型4个字节+boolean类型1个字节+对象引用4个字节=17个字节,需要8的倍数,所以byte需要加上7个字节结盟。也就是说这个程序占用了24个字节。

14.为什么栈被分成两部分? 15、栈的起点在哪里?

main函数也是程序的起点。

16.为什么基本类型不放在堆中?

因为基本类型占用的空间一般为1-8个字节(需要的空间很少),而且由于是基本类型,不会有动态增长(长度是固定的),所以存放在栈上比较多合适的。相反,将其存储在动态增长的堆上没有什么意义。

17. Java参数是按值传递还是按引用传递?

按值传递。

当基本类型作为参数传递时,必须按值传递;当引用类型作为参数传递时,它也是按值传递,但“值”是相应的引用。假设方法参数是对象引用。当进入被调用的方法时,传递的引用的值将被程序解释为堆中的对象。只有这样,它才与真实的物体相对应。如果此时修改,修改的是Reference对应的对象,而不是引用本身,也就是说修改的是堆中的数据,而不是栈中的引用。

18. 为什么不推荐递归?

因为递归总是压入栈,短时间内无法弹出,所以栈的压力会很大。栈也深,容易爆炸,效率低。

19. 为什么大于2的参数要放在对象中?

因为除了double和long类型在局部变量表中占用2个槽位外,其他类型都占用1个槽位大小。如果参数太多,栈帧就会变大。因为槽很大,放一个对象的引用只会占用1个槽,增加了堆的压力,减少了栈的压力。堆有自己的GC,所以这个压力可以忽略不计。

20. 常见笔试题

问题:输出是什么?

答案:aaa、aaa、abc

原因:其实也是一个按值传递还是按引用传递的问题。具体核心原因:main函数的str引用和zcd都在栈上,其对应的值在方法区或者堆中。 test1、test2、test3的参数也在栈中。该空间与 main 上的空间不同,而是不同的堆栈框架。所以你在测试方法中修改的数据实际上对于main函数来说是不可见的。但对象引用修改的是堆内存中的对象属性值,所以是感性的。那为什么test2输出的是aaa而不是abc呢?因为test2已经替换了堆中的所有对象,重新生成了一个新的对象,所以看不到main上的引用,如下三图所示:

(1)aaa

问题1

(2)aaa

问题2

(3)ABC

问题3

public class TestChuandi {
    public static void main(String[] args) {
        String str = "aaa";
        test1(str);
        // aaa
        System.out.println(str);
        Zhichuandi zcd = new Zhichuandi();
        zcd.setName("aaa");
        // aaa
        test2(zcd);
        System.out.println(zcd.getName());
        // abc
        test3(zcd);
        System.out.println(zcd.getName());
    }
    private static void test1(String s) {
        s = "abc";
    }
    private static void test2(Zhichuandi zcd) {
        zcd = new Zhichuandi();
        zcd.setName("abc");
    }
    private static void test3(Zhichuandi zcd) {
        zcd.setName("abc");
    }
}
class Zhichuandi {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读