万万没想到,JVM内存区域的面试题也可以问的这么难?

二、Java内存区域

1、Java内存结构

万万没想到,JVM内存区域的面试题也可以问的这么难?

内存结构

  • 程序计数器

当前线程所执行字节码的行号指示器。若当前方法是native的,那么程序计数器的值就是undefined。

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

  • 虚拟机栈

就是常说的Java栈,存放栈帧,栈帧里存放局部变量表等信息,方法执行到结束对应着一个栈帧的入栈到出栈。

线程私有,会发生StackOverflow。

  • 本地方法栈

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

线程私有,会发生StackOverflow。

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

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

  • 方法区

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

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

  • 运行时常量

是方法区的一部分,存常量(比如static final修饰的,比如String 一个字符串)和符号引用。

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

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

  • 指针碰撞

前提要求堆内存的绝对工整的。

所有用过的内存放一边,没用过的放另一边,中间放一个分界点的指示器,当有对象新生时就已经知道大小了,指示器只需要像没用过的内存那边移动与对象等大小的内存区域即可。

万万没想到,JVM内存区域的面试题也可以问的这么难?

指针碰撞

  • 空闲列表

假设堆内存并不工整,那么空闲列表最合适。

JVM维护一个列表 ,记录哪些内存块是可用的,当对象创建时从列表中找到一块足够大的空间划分给新生对象,并将这块内存标记为已用内存。

万万没想到,JVM内存区域的面试题也可以问的这么难?

空闲列表

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

分为三部分:

  • 对象头

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

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

对象指针就是对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例

  • 实例数据

用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)

  • 对齐填充

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

4、对象怎么定位

如下两种,具体用哪种有JVM来选择,hotspot虚拟机采取的直接指针方式来定位对象。

  • 直接指针

栈上的引用直接指向堆中的对象。好处就是速度快。没额外开销。

  • 句柄

Java堆中会单独划分出一块内存空间作为句柄池,这么一来栈上的引用存储的就是句柄地址,而不是真实对象地址,而句柄中包含了对象的实例数据等信息。好处就是即使对象在堆中的位置发生移动,栈上的引用也无需变化。因为中间有个句柄。

5、判断对象是否能被回收的算法

  • 引用计数法

给对象添加一个引用计数器,每当有一个地方引用他的时候该计数器的值就+1,当引用失效的时候该计数器的值就-1;当计数器的值为0的时候,jvm判定此对象为垃圾对象。存在内存泄漏的bug,比如循环引用的时候,所以jvm虚拟机采取的是可达性分析法。

  • 可达性分析法

有一些根节点GC Roots作为对象起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,则证明此对象为垃圾对象。

补充:哪些可作为GC Roots?

  • 虚拟机栈中的引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(native方法)引用的对象

6、如何判断对象是否能被回收

  • 该对象没有与GC Roots相连

  • 该对象没有重写finalize()方法或finalize()已经被执行过则直接回收(第一次标记)、否则将对象加入到F-Queue队列中(优先级很低的队列)在这里finalize()方法被执行,之后进行第二次标记,如果对象仍然应该被GC则GC,否则移除队列。(在finalize方法中,对象很可能和其他 GC Roots中的某一个对象建立了关联,那就自救了,就不会被GC掉了,finalize方法只会被调用一次,且不推荐使用finalize方法)

7、Java堆内存组成部分

万万没想到,JVM内存区域的面试题也可以问的这么难?

堆组成部分

堆大小 = 新生代 + 老年代。如果是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篇幅过长,不适合放到这里,懂者自懂,不懂Google)

10、栈帧是什么?包含哪些东西

栈帧中存放的是局部变量、操作数栈、动态链接、方法出口等信息,栈帧中的局部变量表存放基本类型+对象引用+returnAddress,局部变量所需的内存空间在编译期间就完成分配了,因为基本类型和对象引用等都能确定占用多少slot,在运行期间也是无法改变这个大小的。

11、简述一个方法的执行流程

方法的执行到结束其实就是栈帧的入栈到出栈的过程,方法的局部变量会存到栈帧中的局部变量表里,递归的话会一直压栈压栈,执行完后进行出栈,所以效率较低,因为一直在压栈,栈是有深度的。

12、方法区会被回收吗

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

如何判断无用的类:

  • 该类所有实例都被回收(Java堆中没有该类的对象)

  • 加载该类的ClassLoader已经被回收

  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方利用反射访问该类

13、一个对象包含多少个字节

会占用16个字节。比如

Object obj = new Object();

因为obj引用占用栈的4个字节,new出来的对象占用堆中的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的倍数,所以字节对齐需要补充7个字节,也就是这段程序占用24字节。

14、为什么把堆栈分成两个

  • 栈代表了处理逻辑,堆代表了存储数据,分开后逻辑更清晰,面向对象模块化思想。

    栈是线程私有,堆是线程共享区,这样分开也节省了空间,比如多个栈中的地址指向同一块堆内存中的对象。

  • 栈是运行时的需要,比如方法执行到结束,栈只能向上增长,因此会限制住栈存储内容的能力,而堆中的对象是可以根据需要动态增长的。

15、栈的起始点是哪

main函数,也是程序的起始点。

16、为什么基本类型不放在堆里

因为基本类型占用的空间一般都是1-8个字节(所需空间很少),而且因为是基本类型,所以不会出现动态增长的情况(长度是固定的),所以存到栈上是比较合适的。反而存到可动态增长的堆上意义不大。

17、Java参数传递是值传递还是引用传递

值传递。

基本类型作为参数被传递时肯定是值传递;引用类型作为参数被传递时也是值传递,只不过“值”为对应的引用。假设方法参数是个对象引用,当进入被调用方法的时候,被传递的这个引用的值会被程序解释到堆中的对象,这个时候才对应到真正的对象,若此时进行修改,修改的是引用对应的对象,而不是引用本身,也就是说修改的是堆中的数据,而不是栈中的引用。

18、为什么不推荐递归

因为递归一直在入栈入栈,短时间无法出栈,导致栈的压力会很大,栈也有深度的,容易爆掉,所以效率低下。

19、为什么参数大于2个要放到对象里

因为除了double和long类型占用局部变量表2个slot外,其他类型都占用1个slot大小,如果参数太多的话会导致这个栈帧变大,因为slot大,放个对象的引用上去的话只会占用1个slot,增加堆的压力减少栈的压力,堆自带GC,所以这点压力可以忽略。

20、常见笔试题

问题:输出结果是什么?

答案:aaa、aaa、abc

原因:其实也是值传递还是引用传递的问题。具体核心原因:main函数的str引用和zcd在栈上,而其对应的值在方法区或堆上。test1、test2、test3的参数也在栈上,这个空间和main上的不是同一块,是不同的栈帧。所以你修改test方法的数据对于main函数其实是无感知的。但是对象的引用的话修改的是堆内存中的对象属性值,所以有感知,那为什么test2输出的是aaa而不是abc呢?因为test2把堆中的对象都给换了,重新生成一个全新对象,对main上的引用来讲是看不到的,具体如下三幅图:

(1)aaa

万万没想到,JVM内存区域的面试题也可以问的这么难?

题1

(2)aaa

万万没想到,JVM内存区域的面试题也可以问的这么难?

题2

(3)abc

万万没想到,JVM内存区域的面试题也可以问的这么难?

题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;
    }
}

 

END

推荐好文

强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!

能挣钱的,开源 SpringBoot 商城系统,功能超全,超漂亮!

posted on 2021-01-13 10:12  Java知音号  阅读(613)  评论(0编辑  收藏  举报

导航