(七)java面向对象面试题

1. 变量存放位置

  • 类变量(静态成员变量):java7之前把静态变量存放在方法区,在Java7之后存放在堆中
  • 成员变量(类中声明的变量):随着类的产生和销毁,在类初始化时,从运行常量池取出引用,存放在堆中
  • 局部变量:定义在类的方法中的变量,存放在栈中

1.1 案例

public class StaticObjTest {
    static class Test{
        // 静态变量
        // 一个java.lang.Class类型的对象实例引用了此变量
        static ObjectHolder staticObj = new ObjectHolder();
        // 实例变量
        ObjectHolder instanceObj = new ObjectHolder();
        void foo() {
            // 局部变量
            ObjectHolder localObj = new ObjectHolder()();
            System.out.println("done");
        }
    } 
    private static class ObjectHolder{
    } 
    public static void main(String[] args) {
        Test test = new StaticObjTest.Test();
        test.foo();
    }
}

JVM状态图如下:
请添加图片描述
分析

  • 以上代码中,静态变量staticObj随着Test的类型信息存放在方法区
  • 实例变量instanceObj随着Test对象存放在堆中
  • 局部变量localObj则是存放在foo()方法栈帧的局部变量表中
  • 三个变量引用对应的对象实体都是在堆空间。

2. HotSpot方法区变迁

版本演讲细节
JDK6及之前方法区的实现为永久代,静态变量存放在永久代中,字符串常量池(StringTable)位于运行时常量池中
JDK7方法区的实现为永久代,但已经逐步“去永久代”,静态变量、字符串常量池移除,保存在堆中
JDK8方法区的实现为本地内存的元空间 ,字符串常量池、静态变量仍在堆中

2.1 JDK1.2~JDK6

在JDK1.2~JDK6的实现中,HotSpot使用永久代实现方法区;HotSpot使用GC分代实现方法区带来了很大便利;
在这里插入图片描述

2.2 JDK7

由于将JRockit许多优秀特性移植到HotSpot时,GC分代技术遇到了种种困难,JDK7 中符号表被移动到Heap中, 字符串常量静态变量被移动到heap中。
在这里插入图片描述

2.3 JDK8

在 JDK8 中,元空间(Meatspace)完全取代永久代
在这里插入图片描述

3. 为什么调整字符串常量池和静态变量的位置

JDK7中将字符串常量池放到了堆空间中:因为永久代的回收效率很低,在Full GC时才会触发,而Full GC在老年代的空间不足、永久代不足时才会触发,这就导致字符串常量池回收效率不高;
而我们开发中会有大量的字符串被创建,回收效率低会导致永久代内存不足。将字符串常量池放到堆里,能及时回收内存。

4. 为什么要用元空间替换永久代

  • 因为永久代设置最大空间大小是难以确定的。
  • 如果动态加载类过多,容易产生Perm区的OOM,当要不断动态加载很多类,会经常出现致命错误;而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的最大大小仅受本地内存限制。
  • 对永久代进行调优是很困难的。

5. JDK1.8元空间会产生内存溢出吗?什么情况先会产生溢出

会,加载到内存中的 class 数量太多或者体积太大。超出我们设定的最大值时会溢出。
通过增加Metaspace的大小-XX:MaxMetaspaceSize=512m

5.1 演示

  • 查看元空间大小java -XX:+PrintFlagsInitial
    在这里插入图片描述

  • 新建工程
    在这里插入图片描述
    一路next即可

  • 测试代码

public class MetaspaceDemo {
    static class OOM{}
    public static void main(String[] args) {
        int i = 0;//模拟计数多少次以后发生异常
        try {
            while (true){
                i++;
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOM.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects,
                                            MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invokeSuper(o,args);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable e) {
            System.out.println("=================多少次后发生异常:"+i);
            e.printStackTrace();
        }
    }
}
  • 调整大小,默认为2080M,这里设置为10M方便演示效果
    -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
    在这里插入图片描述
  • 结果展示为:
    在这里插入图片描述

6. JVM8的内存结构

在这里插入图片描述

6.1 程序计数器

  • 程序计数器就是当前线程所执行的字节码的行号指示器
  • 程序计数器是线程私有的,每个线程都已自己的程序计数器。

6.2 虚拟机栈

  • 方法被执行时入栈,执行完后出栈
  • 每一个栈帧包含如下内容
    • 局部变量表:存放着方法里的Java基本数据类型
    • 操作数栈
    • 动态链接
    • 方法返回地址
  • 虚拟机栈可能会抛出两种异常:
    • 如果线程请求的栈深度大于虚拟机所规定的栈深度,则会抛出StackOverFlowError即栈溢出
    • 如果虚拟机的栈容量可以动态扩展,那么当虚拟机栈申请不到内存时会抛出OutOfMemoryError即OOM内存溢出
  • 产生StackOverFlowError的原因是:
    • 无限递归循环调用(最常见)。
    • 执行了大量方法,导致线程栈空间耗尽。
    • 方法内声明了海量的局部变量。

6.3 本地方法栈

  • 本地方法栈与虚拟机栈的作用是相似的,都会抛出OutOfMemoryError和StackOverFlowError,都是线程私有的,主要的区别在于:
    • 虚拟机栈执行的是java方法
    • 本地方法栈执行的是native方法

6.4 java堆

java堆是JVM内存中最大的一块,由所有线程共享, 是由垃圾收集器管理的内存区域,主要存放对象实例,当然由于java虚拟机的发展,堆中也多了许多东西,现在主要有:

  • 对象实例
    • 类初始化生成的对象
    • 基本数据类型的数组也是对象实例
  • 字符串常量池
    • 字符串常量池原本存放于方法区,从jdk7开始放置于堆中。
    • 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
  • 静态变量
    • 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
  • 线程分配缓冲区(Thread Local Allocation Buffer)
    • 线程私有,但是不影响java堆的共性
    • 增加线程分配缓冲区是为了提升对象分配时的效率

java堆既可以是固定大小的,也可以是可扩展的(通过参数-Xmx和-Xms设定),如果堆无法扩展或者无法分配内存时也会报OOM。

6.5 方法区

它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

  • 类型信息:对每个加载的 类型 (类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
    • 这个类型的完整有效名称(全名=包名.类名)
    • 这个类型直接父类的完整有效名(对于interface或是java.lang. Object,都没有父类)
    • 这个类型的修饰符( public, abstract,final的某个子集)
    • 这个类型实现接口的一个有序列表。
  • 域(Field)信息:JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。域的相关信息包括如下内容:
    • 域名称
    • 域类型
    • 域修饰符(public,private,protected,static,final, volatile,transient的某个子集)
  • 方法(Method)信息:JVM必须在方法区中保存类型的所有方法的相关信息以及方法的声明顺序。方法的相关信息包括:
    • 方法名称
    • 方法的返回类型(或void)
    • 方法参数的数量和类型(按顺序)
    • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
    • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)
    • 异常表(abstract和 native方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
  • 静态变量(non-final的)
    • 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
    • 补充说明:被声明为final的静态变量的处理方法则不同,被static和final修饰的变量也称为全局变量,每个全局常量在编译的时候就会被赋值了。
public class Order {
    public static int num = 10;
    public static final int COUNT = 20;
}

通过反编译的class文件
请添加图片描述

  • 运行时常量池:理解运行时常量池,需要了解字节码文件(ClassFile)中的常量池;方法区内部包含运行时常量池,字节码文件内部包含了常量池。
    • 常量池:一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息,那就是常量池( Constant Pool Table),包括各种字面量(数量值和字符串值)和对类型、域和方法的符号引用。常量池可以看做是一张表,虚拟机指令根据这张常量表,找到要执行的字面量、类名、方法名、参数类型等。
    • 运行时常量池
      • 运行时常量池(Runtime Constant pool)是方法区的一部分。
      • 常量池(Constant Pool Table)是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
      • 在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
      • JVM为每个已加载的类型(类或接口)都维护一个运行时常量池,池中的数据项像数组项一样,是通过索引访问的。
      • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,会转换为真实地址。运行时常量池,相对于Class文件中的常量池的另一重要特征是:具备动态性。比如String.intern()方法会动态地向池中增加内容
      • 运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据比符号表要更加丰富。
      • 如果构造类或接口的运行时常量池时,所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemoryError异常。

6.6 直接内存

jdk1.4中加入了NIO(New Input/Putput)类,引入了一种基于通道(channel)与缓冲区(buffer)的新IO方式,它可以使用native函数直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样可以在一些场景下大大提高IO性能,避免了在java堆和native堆来回复制数据。
java 的 NIO 库允许 java 程序使用直接内存。直接内存是在 java 堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于 java 堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在 java 堆外,因此它的大小不会直接受限于 Xmx (虚拟机参数)指定的最大堆大小,但是系统内存是有限的, java 堆和直接内存的总和依然受限于操作系统能给出的最大内存。直接内存位于本地内存,不属于JVM内存,不受GC管理,但是也会在物理内存耗尽的时候报OOM。
注意:direct buffer不受GC影响,但是direct buffer归属的JAVA对象是在堆上且能够被GC回收的,一旦它被回收,JVM将释放direct buffer的堆外空间
直接内存(Direct Memory)的特点:

  • 直接内存并非 JVMS 定义的标准 Java 运行时内存。
  • JDK1.4 加入了新的 NIO 机制,目的是防止 Java 堆 和 Native 堆之间往复的数据复制带来的性能损耗,此后 NIO 可以使用 Native 的方式直接在 Native 堆分配内存。
  • 直接内存区域是全局共享的内存区域。
  • 直接内存区域可以进行自动内存管理(GC),但机制并不完善。
  • 本机的 Native 堆(直接内存) 不受 JVM 堆内存大小限制。可能出现 OutOfMemoryError 异常。

7. 方法区和永久代

  • (永久代)PermGen space 则是 HotSpot 虚拟机基于JVM 规范对方法区的一个落地实现
  • 强调: 只有 HotSpot 才有 PermGen space。
  • HotSpot 1.7 永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。
  • 永久代 在JDK8被移除, JDK1.8方法区叫做元空间:

8. 你知道的几种主要的JVM参数

  1. 思路:
    可以说一下堆栈配置相关的,垃圾收集器相关的,还有一下辅助信息相关的。
  2. 参考答案:
1)堆栈配置相关
● -Xmx3550m: 最大堆大小为3550m。
● -Xms3550m: 设置初始堆大小为3550m。
● -Xmn2g: 设置年轻代大小为2g。
● -Xss128k: 每个线程的堆栈大小为128k。
● -XX:MaxPermSize: 设置持久代大小为16m
● -XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代),比例为1:4-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
(2)垃圾收集器相关
● -XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
● –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合
● -XX:ParallelGCThreads=20: 配置并行收集器的线程数
● -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合
● -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
● -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎
片 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值