每日学习,Offer滴滴
JVM内存模型
1. 什么是栈?
Java的指令都是根据栈来设计的,栈是运行时的单位,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(stack Frame) ,对应着一次次的Java方法调用。
2. 栈是线程私有的吗?
是的,栈的生命周期跟线程的生命周期一致,线程结束后栈也会释放
3. 栈有什么特点?
- 栈是一种快速有效的分配存储方式,访问速度仪次于程序计数器
- JVM直接对Java栈的操作只有两个:每个方法执行伴随着进栈(入栈、压栈) 和 执行结束后的出栈工作
- 对于栈来说不存在垃圾回收问题GC,但存在内存溢出问题OOM
4. 栈的结构?
每个线程都有一个虚拟机栈,栈的内部是一个个栈帧,每个栈帧由局部变量表、操作数栈、动态链接、方法返回地址构成
- 局部变量表:定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference) ,以及returnAddress类型。
- 操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 动态链接:每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。多态链接实现了我们的多态写法。
- 方法返回地址:一个方法的结束,有两种方式:正常执行完成和出现未处理的异常,非正常退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
5. 栈会发生内存溢出吗?
会,栈有两种异常状况,StackOverflowError和OutOfMemoryError异常。StackOverflowError为栈的深度不足,通俗的解释就是栈帧大小的综合大于了-Xss配置的值,OutOfMemoryError则是线程创建时需要分配给它一个私有栈而内存空间不足所发生的异常。
- StackOverflowError:每当java程序代码启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。当线程调用java方法时,虚拟机压入一个新的栈帧到该线程的java栈中。只要这个方法还没有返回,它就一直存在。如果线程的方法嵌套调用层次太多(如递归调用),随着java栈中帧的逐渐增多,最终会由于该线程java栈中所有栈帧大小总和大于-Xss设置的值,而产生StackOverflowError内存溢出异常。
- OutOfMemoryError:java程序代码启动一个新线程时,没有足够的内存空间为该线程分配java栈(一个线程java栈的大小由-Xss参数确定),jvm则抛出OutOfMemoryError异常。
6. 垃圾回收会回收栈内存吗?
不会,栈是线程私有的,生命周期随着线程的结束而结束,所以不需要GC来进行回收。
7. 栈的大小是固定的还是动态的?
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量, Java虚拟机将会抛出一个stackoverflowError异常。(递归操作不当容易发生stackoverflowError异常)
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个outofMemoryError异常。
8. 局部变量表的容量?
局部变量表的容量以变量槽(Variable Slot,下称 Slot)为最小单位,如果访问的是 32 位数据类型的变量,索引 n 就代表了使用第 n 个 Slot,如果是 64 位数据类型的变量,则说明会同时使用 n 和 n+1 两个 Slot。
9. 程序计数器是干什么的?
内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个 java 虚拟机规范没有规定任何 OOM 情况的区域。
10. 那些地方用到了程序计数器?
使用java指令的地方,用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
11. 本地方法栈是干嘛的?
本地方法栈和虚拟机栈类似,只不过本地方法栈为 Native 方法服务。
- 本地方法是使用C语言实现的。
- 它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不收虚拟机限制的世界。它和虚拟机拥有同样的权限。
12. 方法区是做什么的?
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。即方法区里存放着类的版本、字段、方法、接口和常量池(存储字面量和符号引用)。
13. 永久代跟方法区的关系?
《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它,在JDK1.7版本之前永久代是对方法区的实现。永久代是基于
HotSpot虚拟机
14. 永久代的变迁?
- jdk1.6及之前:有永久代(permanent generation) ,静态变量存放在永久代上
- jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
- jdk1.8及之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆
15. 符号引用包括什么?
1、类的权限定名;2、字段名和属性;3、方法名和属性。
1、类型信息:
类的完整名称
类的直接父类的完整名称
类的直接实现接口的有序列表
类型标志(类类型还是接口类型)
类的修饰符(public private defautl abstract final static)
2、类型的常量池
存放该类型所用到的常量的有序集合,包括直接常量(字符串、整数、浮点数)和对其他类型、字段、方法的符号引用。
3、字段信息(该类声明的所有字段)
字段修饰符(public、peotect、private、default)
字段的类型
字段名称
4、方法信息
方法信息中包含类的所有方法。
方法修饰符
方法返回类型
方法名
方法参数个数、类型、顺序等
方法字节码
操作数栈和该方法在栈帧中的局部变量区大小
异常表
5、类变量(静态变量)
6、指向类加载器的引用
7、指向Class实例的引用
8、方法表
9、运行时常量池(Runtime Constant Pool)
16. 为什么移除永久代?
- 永久代设置空间大小难以确定,如果设置比较小容易发生FullGC影响程序性能,而且容易出现OOM,如果过大又占用内存
- 对永久代的调优是很困难的
- 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。
17. 方法区是存在堆里吗?
java虚拟机规范中明确说明:"尽管所有的方法区在逻辑上是属于堆的一部分,但是一些简单的实现可能不会选择去进行垃圾回收或进行压缩",但是对于HotSpot JVM 而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆区分开来
所以,方法区看作是一块独立于堆的内存空间
18. 怎么设置方法区大小?
JDK1.7及以前
- -XX:PermSize来设置永久代初始分配空间,默认为20.75m
- -XX:MaxPermSize来设定永久代最大可分配空间,32位机器默认64m,64位机器默认82m
- 当jvm加载类信息超过最大容量,会报OutOfMemoryError:PermGen Space
JDK1.8以及后
- 元数据区大小使用参数-XX:MetaSpaceSize和-XX:MaxMetaspaceSize指定,来替代jdk7原有的两个参数
- 默认值依赖于平台,windows下默认初始化大小为21M,最大值为-1,及没有限制
- 于永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存,如果元数据区发生移除,虚拟机一样也会抛出异常OutOfMemoryError:Metaspace
- -XX:MetaspaceSize:设置初始的元空间大小,对于一个64位的服务器端JVM来说,其默认的内存大小为21MB,这就是初始的高水位线,一旦触及这个水位线,Full GC将会触发并卸载没有用的类(即这些类对应的类加载器不在存活),然后这个高水位线会重置,新的高水位线取决于GC后释放了多少元空间,如果释放的空间不足,那么在不超过MaxMetaspaceSize的情况下适当提高该值,如果释放空间过多,则适当降低该值
- 如果初始化高水位线设置过低,上述高水位线调整情况会发生很多次,通过垃圾回收日志可以观察到Full GC多次调用,为了避免频繁GC,建议将-XX:MetaspaceSize设置为一个相对较高的值
19. 如何判断是否属于不再使用的类?
- 该类所有实例都已经被回收,也就是java堆中不存在该类以及任何派生子类的实例
- 加载该类的类加载器已经被回收,这个条件除非精心设计的可替换类加载器的场景,如OSGi,JSP的重加载等,否则通常很难达成
- 对应该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
java虚拟机被允许对满足上述3个条件的无用类进行回收,这里说仅仅是"被允许",而不是和对象一样,没有引用了就必然进行回收,关于是否要对类型进行回收,HotSpot虚拟机提供了-Xonclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading,-XX:+TraceClassUnLoading查看类加载和卸载信息
在大量使用反射动态代理CGLIb等字节码框架,动态生成JSP以及OSGI这类频繁自定义类加载器场景中,通常都需要java虚拟机具备类型卸载的能了,以保证不会对方法区造成过大的内存压力
20. StringTable为什么调整?
jdk7中将StringTbale放到了堆空间,因为永久代中回收频率很低,在Full GC 时候才会回收,而Full GC是老年代的空间不足,永久代空间不足时才会触发,这就导致StringTable回收频率不高,而我们开发中会有大量字符串创建,回收效率低导致永久代内存不足,放到堆里能及时回收内存
21. 对象内存布局?
对象头
- 运行时元数据(Mark Word)
- 哈希值(HashCode)
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针
- 指向类元数据InstanceKlass,确定该对象所属类型
- 如果是数组,还需要记录数组长度
实例数据(InstanceData)
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
- 相同宽度的字段被分配在一起
- 父类中定义的变量会出现在子类之前
- 如果CompactFields参数为true(默认为true),子类的窄变量可以插入到父类变量的空隙
对齐填充(Padding)
不是必须,也没有特别含义,仅仅起到占位符的作用
22. 对象的访问方式?
由于reference类型在java虚拟机规范中并没有定义这个引用应该通过什么方式去定位,所以对象访问方式也是由虚拟机自己决定的,主流的访问方式主要有两种:直接指针和句柄访问
句柄访问
直接指针
这两种方式各有优势,使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时移动对象是非常普遍的行为)时只需要修改句柄的实例指针,而reference本身不需要修改,而使用直接指针访问的最大好处就是快,因为少一次指针定位的时间开销,在HotSpot虚拟机中采用的是直接指针
23. 什么是直接内存?
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中的内存区域,在JDK1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提升性能,因为避免了在java堆和native堆中来回复制数据,直接内存分配不会受到java堆大小的印象,但是既然是内存,则肯定会受到本机内存大小的限制,如果内存区域大于物理内存限制,则会抛出OOM异常
直接内存大小可以通过MaxDirectMemorySize设置,如果不指定,默认大小与堆的最大值-Xmx参数值一致
24. 常量池有几种?
Class文件常量池(class constant pool)
Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 这里的字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;声明为final的常量值指的是成员变量,不包含本地变量,本地变量是属于方法的。这些都在常量池的
UTF-8 表中(逻辑上的划分); - 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用;
运行时常量池
运行时常量池是方法区的一部分,是一块内存区域
。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。
字符串常量池(string pool也有叫做string literal pool)
字符串常量池是全局的
,JVM 中独此一份,因此也称为全局字符串常量池
。运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。其实,“使用常量池”对应的字节码是一个 ldc 指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。String 类的 intern() 方法还可在运行期间把字符串放到字符串常量池中。JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲池,超出此范围仍然会去创建新的对象。
其中:
- 在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例;
- 在 jdk1.7(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;
- jdk1.8 已移除永久代,字符串常量池是在本地内存当中,存储的也只是引用。
25. 堆的布局?
上图中,刻画了Java程序运行时的堆空间,可以简述成如下2条
1.JVM中共享数据空间可以分成三个大区,新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation),其中JVM堆分为新生代和老年代,新生代跟老年代的比例默认为 1:2
2.新生代可以划分为三个区,Eden区(存放新生对象),两个幸存区(From Survivor和To Survivor)(存放每次垃圾回收后存活的对象)他们的内存大小默认为 8:1:1
3.永久代管理class文件、静态对象、属性等(JVM uses a separate region of memory, called the Permanent Generation (orPermGen for short), to hold internal representations of java classes. PermGen is also used to store more information )(在jdk1.8后移除了永久代)
4.JVM垃圾回收机制采用“分代收集”:新生代采用复制算法,老年代采用标记清理算法。
26. 堆是线程共享的吗?
是的,几乎所以的对象实例都是在堆上分配内存,在栈里存放的是对象的引用地址
27. -Xms和-Xmx作用于永久代吗?
-Xms和-Xmx不作用于永久代
“-Xms”用于表示堆区的起始内存,等价于-XX:InitialHeapSize
“-Xmx”则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
.默认情况下,初始内存大小:物理电脑内存大小 / 64;最大内存大小:物理电脑内存大小 / 4