Android经典面试题笔记之JVM内存管理剖析

2024-10-08 20:37:15 浏览数 (4)

心里种花,人生才不会荒芜,如果你也想一起成长,请点个关注吧。

JVM

class文件通过类加载器加载到运行时数据区,运行时数据区又分为线程私有和线程共享的内存;

运行时数据区的数据和方法,通过执行引擎,利用解释执行或者是JIT解释成0101的数据给操作系统

1、运行时数据区
  • 线程共享区
    • 方法区
  • 线程私有
    • 虚拟机栈
    • 本地方法栈:
    • 程序计数器:
程序计数器
  • 当前线程正在执行的字节码指令的地址
  • 内存区域中不会有OOM
  • 为什么会有?时间片轮转、多线程,需要记录
虚拟机栈
  • 组成:栈帧--> 局部变量表,操作数栈、动态连接、完成出口(返回地址)
  • 局部变量表:存储8大基本数据类型和引用
  • 操作数栈:存放方法的执行和操作
  • 动态连接:涉及多态 ,用于判断当前的方法应该执行的是哪个态的方法
  • 完成出口(返回地址) :记录的是方法返回以后应该继续执行哪里。这是正常情况,里面是根据程序计数器的地址来的,但如果是异常情况的话,就跟完成出口无关,而是走异常处理表
本地方法栈
  • 保存的是native方法的信息
  • 执行native方法时,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单的动态链接并直接调用native方法。程序计数器不会记录,是个null
2、方法区
  • 存放类信息、常量、静态变量和即时编译期编译后额代码
  • JDK>=1.8称为元空间,使用机器内存,大小基本不受限制,方便拓展;但可能会挤压堆空间
  • JDK<=1.7 是永久代,内存受制于堆,会进行垃圾回收
3、Java堆
  • 对象实例(几乎所有)
  • 数组
为什么要分方法区和Java堆?

堆里面放的是一些经常变动需要回收的对象,方法区是一些静态的,不容易回收的信息,这是一种动静分离的思想

直接内存

不是虚拟机内存运行时数据区的一部分,也不是JVM规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在Java堆内可以用idrectByteBuffer对象直接引用并操作;

不受Java堆大小限制,但受本机总内存限制,也会OOM,也会被垃圾回收

NDK用的是直接内存

Intellij IDEA 在跑的时候可以设置JVM虚拟机参数,比如-Xms等

4、从底层深入理解运行时数据区
  1. JVM向操作系统申请内存,设置堆、方法区和栈的内存大小
  2. 进行类加载,class进入方法区
  3. 常量、静态变量 进入方法区
  4. 虚拟机栈---入栈帧
  5. 栈帧的方法执行,局部变量的引用进入栈帧的局部变量表

HSDB 工具 查看JVM内存

深入辨析堆和栈
功能
  • 以栈帧的方式存储方法调用的过程,并存储方法调用过程中的基本数据类型的变量(int、short、long、byte、float、double、boolean、char)和对象的引用变量,其内存分配咋栈上,变量出了作用域就会释放
  • 堆内存用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
线程独享还是共享
  • 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属的线程中可见,即栈内存可以理解成线程的私有内存
  • 堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问
空间大小
  • 栈内存要远远小于堆内存,栈的深度有限,会抛出StarkOverFlowError问题
5、内存溢出
  • 栈溢出、堆溢出、方法区溢出、本机直接内存(堆外)溢出(ByteBuffer allocateDirect方法)
虚拟机优化技术
  • 编译优化 --> 方法内联,就是运行的时候判断方法是不是直接的表达式,可以提取出来,这样就减少入栈出栈操作
  • 栈的优化技术 -->栈帧之间数据共享,方法相互调用时有传参数,也就是操作数栈和另一个栈帧的局部变量表之间的共享

虚拟机中对象的创建过程

类加载 --> 检查加载 -->分配内存 --> 内存空间初始化 --> 设置 --> 对象初始化

6、对象的分配策略

几乎所有的对象都在堆上分配,此外满足一定条件还可以在栈上分配,也就是虚拟机栈上

对象分配策略
  1. new关键字创建对象的时候,会首先判断是否能栈上分配,判断条件是逃逸分析,就是这个对象不会逃出方法作用域,以及逃出当前线程,也就是被其他线程引用,没有的话就是没有逃逸,可以在栈上分配,当然大小不能太大
    • 栈上分配的优点:栈上没有内存回收,效率高;
    • 对应JVM设置:-XX: DoEscapeAnanlysis,默认是开启的
  2. 不能栈上分配的话,接着判断是否本地线程分配缓冲(TLAB)(占Eden 1%),是的话直接在Eden区分配;
  3. 没有在本地线程缓冲区分配的话,看是否是大对象,不是的话在Eden区分配,是的话直接去老年代
  4. 是否是大对象的话,可以设置JVM的参数,比如设置为10M,大于10M就是大对象
为什么新生代分为Eden from和to,并且是8:1:1

Eden区里只存放新生成的对象,经过一次GC后,存活的就会复制到from,然后from和to采用的是复制算法GC,复制算法GC效率高;

止于比例,大数据分析过90%的对象都会被回收,复制算法空间利用率只有50%,所以最后比例是8:1:1

为什么新生代的年龄最大是15?

因为对象的GC年龄存放在对象头中,用的是一个4位的数据,4位最多就是15;这个值可以通过JVM参数修改:-XX:MaxTenuringThreshold,改成15以下的,默认是15。CMS垃圾收集器默认是6

进入老年代后,这个GC分代年龄信息就没用了

空间分配担保

就是在对象晋级到老年代时,有可能老年代内存会不够,但如果每次老年代都进行GC又影响性能,所以JVM会进行担保,让老年代不用每次都GC,直到内存真不够时再进行GC,这样提高性能

动态年龄判断

如果from和to区里面大对象比较多,然后年龄又还没到15,这时候也会直接到老年代

7、常量池
静态常量池
  • 字面量,比如String i = "abc",abc这个字面量
  • 符号引用,比如String 这个类:java.lang.String
  • 类的、方法的信息
运行时常量池

类加载 -- 运行时数据区 --- 方法区(逻辑区域)

实体类,符号引用 --> 直接引用(hash值)

运行时字符串常量JDK1.8后也在堆

字符串相关

String str = "ab" "cd" "ef"首先会生成ab对象,再生成abcd对象,最后生成abcdef对象

代码语言:javascript复制
 //会先在常量池中创建king,并在堆里面创建一个a的String对象,并引用常量池中的King
String a = new String("king").intern();
//调用intern方法之后,会去常量池里查找是否有等于该字符串对象的引用,有就直接返回引用
String b = new String("King").intern();
所以 a == b 为true;如果没有加intern方法,则不为true,因为是2个String对象
8、面试题

什么情况下内存栈溢出?

  • 无限递归,StackOverflowError
  • 不断建立线程,JVM申请栈内存,机器没有足够的内存,OOM
9、问题
  1. 常量池是在方法区还是在堆 JDK1.8 运行时常量池(字符串部分放入堆),静态的都在方法区
  2. 普通成员变量在哪里? 是在堆里面,比如一个class的int成员变量,在new出来的时候,会跟随对象创建在堆里面
  3. 方法区:A类,类会在哪个时候卸载?回收?要同时满足下面的条件 满足以上条件只是能被回收,但不一定会被回收,比如有可能虚拟机的类的垃圾回收器被禁用了,-Xnoclassgc
    • 类---所有的实例都要回收掉
    • 加载该类的classload已经被回收
    • 该类,Java.lang.class对象,没有任何地方被引用,并且不能通过反射访问该类的方法
代码语言:javascript复制
public class Location {
 //这个地方不会在常量池中创建
 private String city;
 private String region;

    public static void main(String[] args) {
        //JVM首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。
        //这种方式可以减少同一个值的字符串对象的重复创建,节约内存。
        String str ="abc";

        //首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;
        //其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,
        // 在堆内存中创建一个 String 对象;最后,str1 将引用 String 对象。
        String str1 =new String("abc");

        //这里就跟第一步类似。
        Location location = new Location();
        location.setCity("深圳");
        location.setRegion("南山");

        //首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象
        String str2= "ab"   "cd"   "ef";

        //new Sting() 会在堆内存中创建一个a的String对象,
        // “king"将会在常量池中创建
        // 在调用intern方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
        String a =new String("king").intern();
        //调用 new Sting() 会在堆内存中创建一个b的String 对象,。
        //在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
        String b = new String("king").intern();
        //所以 a 和 b 引用的是同一个对象。
        if(a==b) {
            System.out.print("a==b");
        }else{
            System.out.print("a!=b");
        }
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getRegion() {
        return region;
    }

    public void setRegion(String region) {
        this.region = region;
    }

}

END

点赞转发,让精彩不停歇!关注我们,评论区见,一起期待下期的深度好文!

0 人点赞