常见对象&内存面试题

JVM内存结构中,那一部分不会发生内存泄漏?

核心知识 :JVM运行时内存分区,GC Root

答:我们知道发生内存泄漏的原因是本应该被释放的对象被生命周期大于它的对象持有,GC无法回收,从而导致内存泄漏(更多内容参见内存泄漏),也就意味着,对于存在GC Roots的运行时内存分区而言,其存在内容泄漏风险,从运行时内存分区及对象管理中,我们可以得到下表:

运行时内存分区 是否存在GC Roots 是否存在内存泄漏风险 是否存在OutOfMemoryError异常 是否存在StackOverflowError异常 备注
方法区 是,方法区中静态变量引用的对象以及常量引用的对象都是GC Roots /
是,new出来的部分系统类对象,回调等也是GC Roots /
虚拟机栈 是,虚拟机栈帧中,局部变量表中的对象是GC Roots, /
本地方法栈 是,本地方法栈中对象是GC Roots /
程序计数器 /

相关的还有JVM运行时内存分区是怎样的之类的问题,答题思路与上表一致。

GC时,新生代,老年代,永久代使用的算法是什么?

核心知识 :对象管理,内存回收算法

答:

对象分代 回收算法 备注
新生代 标记-复制算法 /
老年代 标记-清除算法 /
永久代 标记-整理算法 /

多线程的三个特性是什么?对应的解决方案是什么?

核心知识 :并发编程,内存模型

答:多线程并发需满足原子性,有序性,可见性,其对应的解决方案见下表:

特性 解决方案 备注
原子性 AtomaticInteger,AtomaticBoolean等原子操作类,synchronized等关键字 /
可见性 锁,读写屏障,互斥锁等 /
有序性 Lock锁,读写屏障,互斥锁等 /

可以自定义java.lang.String类吗?

核心知识 :类加载器

答:从类加载器一节我们了解到类加载器实现为双亲委托机制,对于A类而言,会首先将A类的加载请求传递给父类,进而对于我们自定义的java.lang.String类而言,依靠原始的类加载器并不能实现自定义String类替换String类的目的。

从自定义类加载器来看,我们可以获取固定路径的java.lang.String类文件,但获取到类文件使用defineClass将其定义成类对象时,会抛出异常,主要是因为类加载器底层实现不允许加载java.限定修饰的类,代码如下:

private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
        if (!this.checkName(name)) {
            throw new NoClassDefFoundError("IllegalName: " + name);
        } else if (name != null && name.startsWith("java.") && this != getBuiltinPlatformClassLoader()) {
            throw new SecurityException("Prohibited package name: " + name.substring(0, name.lastIndexOf(46)));
        } else {
            if (pd == null) {
                pd = this.defaultDomain;
            }

            if (name != null) {
                this.checkCerts(name, pd.getCodeSource());
            }

            return pd;
        }
    }

综上,不可以自定义java.lang.String类。

类加载使用的是什么机制?类加载过程是怎样的?

核心知识 :类加载

答:类是通过类加载器加载完成的,依赖双亲委托机制,双亲委托机制指的是当一个类加载器收到加载类的请求是,会将请求委托给付类,只有父类无法处理该请求,才会由当前类加载器处理,双亲委托机制的好处主要体现在:

  1. 1. 通过双亲委托机制,可以避免类重新加载

  2. 2. 通过双亲委托机制,父类已加载类,不可被子类重新加载,确保系统类的安全,避免被篡改

类加载过程主要由加载,验证,准备,解析,初始化五步组成,其中加载说的是将class字节码文件加载进内存,验证主要是进行字节码,元数据,符号引用等炎症,准备主要是为静态变量等分配内存空间,解析主要是将常量池中的符号引用替换为直接引用的过程,初始化指的是调用cinit方法,完成类对象的初始化(类对象(Class Object)和类的实例对象(Object)不同,类对象在卸载前只会被创建一次)。

Java内存模型是什么?并发环境中共享变量需要具有哪些特性?

核心知识 :类加载

答:Java内存模型是Java虚拟机中为了规避硬件差异,平台差异,内存访问等问题引起程序运行与设计不一致而定义的一套规范,在Java内存模型中,定义了在共享内存系统中,共享变量的读写操作规范,其是Java虚拟机的一部分。Java内存模型主要是借助限制处理器优化和内存屏障指令等方式来实现。

并发环境中共享变量相关的特性包括原子性,有序性,可见性,这里的原子性说的是针对一个或多个指令而言,其要么都执行,要么都不执行,不可被打断。有序性指的是程序执行顺序与代码设计顺序一致。可见性指的是当某线程对共享变量进行修改后,其修改后内容应该对其他同样持有该变量的线程可见。

Java代码中设计了很多并发工具以满足上述特性,具体如下表:

特性 关键字 备注
原子性 synchronized,原子变量等 /
可见性 volatile,synchronized,final等 /
有序性 Lock,RetreenLock,CountDownLatch等 /

在深入理解Java虚拟机中描述,对于final修饰的变量而言,其一旦在构造器中初始化完成,并且在构造器中没有将this引用向外传递,那么在其他线程中就能看见final字段对象的值。

也就是说final实现可见性的前提是在static代码块或构造函数中完成初始化且构造函数不会通过传递引用,将正在构建的对象提供给其他对象。如果存在将该对象引用传递的情况就有可能使得其他线程访问到初始化一半的对象从而造成异常。

Java基本类型内存占用,Java内存中字符以什么编码方式存在?

核心知识Java基础

Java中字符以UTF-16编码方式存储,各基础类型内存占用如下图所示:

类型 字节数 取值范围 备注
byte 1字节 -128~127 /
short 2字节 -32768~32767 /
int 4字节 -2147483648~2147483647 /
long 8字节 -2的63次方~2的63次方-1 /
float 4字节 3.402823e+38 ~ 1.401298e-45(e+38表示是乘以10的38次方,同样,e-45表示乘以10的负45次方) /
double 8字节 1.797693e+308~ 4.9000000e-324 /
char 2字节 0~65535 /
boolean 4字节 true 或 false /

Object obj = new Object(),obj对象占用内存多大?

核心知识:Java对象

从Java对象一节可知,一个Java对象由对象头,实例数据和对齐填充三部分组成,其中对象头又包含MarkWord和类指针。以64位操作系统为例,MarkWord占据64位,即8字节,类指针为指针类型数据,在64位系统中,占8字节,由于new Object中无实例数据,故实例数据内存占0字节,对于对齐填充而言,其要求对象内存占用必须是8字节的整数倍,64位满足该要求,故对齐填充占0字节,故占用内存16字节。

obj对象在32/64位系统中内存占用如下表:

系统类型 MarkWord 开启指针压缩 类指针 实例数据 对齐填充 总内存
32位 4字节(32位) / 4字节 0 0 8字节
64位 8字节(64位) 4字节 0 4字节 16字节
64位 8字节(64位) 8字节 0 0 16字节

64位代码验证,开启指针压缩的内存占用如下图:

常见对象&内存面试题
1-6-1-1

关闭指针压缩的内存占用如下:

常见对象&内存面试题
1-6-1-2

字符串”abcde”占多大内存?不考虑OOM的情况下,Java内存中字符串最大长度是多少?

核心知识:Java基础

“abcde”的内存占用=5*(2字节)=10字节

1.如果以String s = “abcde”这种方式声明字符串,则该字符串会被加入常量池中,常量池最大长度不超过65535

字节码分析如下:

常见对象&内存面试题
1-6-1-3
常见对象&内存面试题
1-6-1-5

从上面两张图片中可以看出,字符串Hello被定义在常量池中,类型为CONSTANT_Utf8_info,其定义如下:

CONSTANT_Utf8_info {    
       u1 tag;    
       u2 length;    
       u1 bytes[length]; 
}

可以看出u2 代表的是长度,而u2是无符号的16位整数,其理论最大取值为65535

2.如果以String s = new String(“abcde”)这种方式声明字符串,则会在堆内存生存String类的实例对象,而String类是通过char数组(JDK 1.8及以前)存储字符串,故其最大长度为数组大小 = Integer.MAX_VALUE = 2147483647

字节码分析如下:

常见对象&内存面试题
1-6-1-6
常见对象&内存面试题
1-6-1-7

注意:JDK 1.8以后,String底层使用byte数组实现,这样的话英文字母就可以用一个字节来存储,节约内存

常见对象&内存面试题
1-6-1-9

JVM给对象分配内存一定在堆上吗?

核心知识:Java基础

一般情况下使用new关键词创建的对象分配在堆上,但是在一些场景下,new指令创建的对象有可能创建在栈上,那么什么时候会发生这种情况呢?就要说到逃逸分析,当一个对象不发生内存逃逸时,那么该对象就可能在栈上分配。

一个对象是否逃逸,主要看该对象作用域,以下述代码为例:

public Person testPerson() {
    Person person = new Person("Hello");
    return person;
}

上述代码中,testPerson方法中创建的person对象,就会发生逃逸,主要分为两种情况:

  1. 1. 方法逃逸:person对象有可能被外部方法或其他对象引用,进而导致逃逸,其作用域大于该函数域

  2. 2. 线程逃逸:person对象有可能被外部其他线程访问到,例如被其他线程访问的共享变量引用等

如果一个对象不会发生逃逸,则该对象有可能在栈上分配,进而减少GC频率,下述代码的person对象就可能分配在栈上:

public String testPerson() {
    Person person = new Person("Hello");
    return person.toString();
}

老年代中有可能存在几类对象?老年代空间不足会发生什么?

核心知识:Java基础

老年代中存在两类对象:

  • • 大对象:为提高GC效率,大内存对象可以直接进入老年代,比如特别长的字符串,大数组等等

  • • 长期存活的对象:对于长期存活的对象而言,其在S0,S1区多次转换后,年龄升高,自动移入老年代

当大对象进入老年代,老年代内存不足时,会触发Full GC,以获取更多可用内存空间

tlab是什么?

核心知识:Java基础

tlab完整英文为Thread Local Allocation Buffer,译为线程本地缓存区,是Java内存分配中的一个概念,其是在Java堆中划分出来的一块内存区域,该区域指定线程独享,用于完成线程对象的创建。使用其的主要的地是减少并发环境下对内存分配区域的竞争,加速内存分配,因为tlab本质还在堆上,属共享内存空间,所以tlab区域的内存对象仍然可以被其他线程访问。

tlab区带来的只是针对这片内存区域而言,其内存的消耗是私有的。

volatile关键字具有什么特性?实现原理是怎样的?

核心知识:并发编程

volatile保证了共享变量在并发环境中的可见性和有序性,根据内存一致性协议,针对共享变量插入内存屏障指令,以确保各线程获取共享变量时获取到的是最新的值,被volatile修饰后并不代表共享变量具有原子性,故在共享变量操作时如需保证原子性,应注意加锁或使用原子操作类,如AtomicBoolean,AtomicInteger等原子操作类。

原文始发于微信公众号(小海编码日记):常见对象&内存面试题

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/67833.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!