1JVM的内存模型
JVM包含两个子系统和两个组件,两个子系统为: Class loader(类装载子系统)、Execution engine(执行引擎);两个组件为:Runtime data area(运行时数据区)、Native Interface(本地接口)。
Class loader(类装载子系统):根据给定的全限定名类名(java.lang.Object)来装载class文件到Runtime data area中的method area。
Execution engine(执行引擎):执行classes中的指令。
Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
运行流程:首先通过 1 、编译器把 Java 代码转换成字节码(.java ——> .class),2 、类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的 3、 命令解析器 执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
2说一下 JVM 运行时数据区?
Java 虚拟机所管理的内存被划分为如下几个区域:
1 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
2 Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
3 本地方法栈(Native Method Stack):本地方法栈是为虚拟机调用 Native 方法服务的;
4 Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
5 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
3详细介绍下Java虚拟机栈?(重点理解)
Java虚拟机是线程私有的,它的生命周期和线程相同。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表:是用来存储我们临时基本数据类型、对象引用地址、returnAddress类型(returnAddress中保存的是return后要执行的字节码的指令地址。)
操作数栈:当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作(例如:在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的)
动态链接:Class文件的常量池中存有大量的符号引用,这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态方法,私有方法等),这种转化称为静态解析,另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
方法出口:当一个方法开始执行后,只有两种方式可以退出这个方法:
1 执行引擎遇到任意一个方法返回的字节码指令: 传递给上层的方法调用者,是否有返回值和返回值类型将根据遇到何种方法来返回指令决定,这种退出的方法称为正常完成出口。
2 方法执行过程中遇到异常: 无论是java虚拟机内部产生的异常还是代码中throw出的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出的方式称为异常完成出口,一个方法若使用该方式退出,是不会给上层调用者任何返回值的。
方法正常退出的时候,调用者的pc计数器的值可以作为返回地址,帧栈中很有可能会保存这个计数器的值作为返回地址。
java栈的大小是动态的或者是固定不变的,1 如果采用大小固定的虚拟机栈,若线程请求分配的栈容量大小超过了栈允许提供的最大容量,这时就会报stackoverFlowError; 2 如果采用动态扩展的虚拟机栈,并且在尝试动态扩展时无法申请到足够的内存,或者创建的新线程没有足够的内存去创建新的虚拟机栈,这时会报outofmemoryError;
注意:静态变量与局部变量的区别?
静态变量有两次初始化的机会,第一次是在准备阶段对类变量设置0值;另一次是在初始化阶段显示的赋值程序员定义的初始值;
局部变量不存在系统的初始化阶段,这意味着一旦定义了局部变量表则必须人为的初始化,否则无法使用;没有赋值的局部变量是不正确的;
注意:局部变量表中也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
4对方法区的理解
方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
1、线程共享;
2、方法区是在JVM启动时创建 ;
3、大小可以固定的或者是可扩展的;
4、方法区的大小决定了系统可以保存多少个类,如果定义了过多的类,导致方法区溢出:outofmemoryError;
5、关闭JVM就会释放这个区域的内存;
注意:元空间(方法区)不在虚拟机设置的内存中,而是使用本地内存。
注意:non-final 与 final的区别?
non-final(static):需要经历两个阶段:准备阶段(0值)——>初始化阶段(显示的赋值为程序员定义)
final:编译阶段就赋值了,如下图所示。
对运行时常量池和常量池的理解
常量池表是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容在字节码文件被加载到运行时数据区域的方法区中时,存放到了方法区的运行时常量池中。注意:这个时候常量池中存放的符号引用已经转换为了直接引用了。
5对堆的理解
一个JVM实例只存在一个堆内存,Java堆在JVM启动时即被创建,堆内存的大小是可以调节的;所有的线程共享Java堆;所有对象的实例以及数组都应该在运行时分配在堆中;数组和对象可能永远不会存储在栈,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置;在方法结束之后,堆中的对象不会马上删除,仅仅在垃圾收集的时候才会被移除;堆是GC执行垃圾回收重点区域。
现代的垃圾收集器大部分都是基于分代收集理论设计,堆空间细分为:Java7 年轻代+老年代+永久区;java8 年轻代+老年代+元空间;如下图所示。
堆可以进一步划分为:年轻代与老年代;年轻代又可以划分为Eden、s0、s1;
其中年轻代与老年代的内存占比:新生代:老年代 = 1/3 : 2/3 ; Eden : s0 : s1 = 8 : 2 : 2;
配置新生代,老年代在堆的占比 : -XX:NewRatio = 2 年轻代占1 老年代占2
几乎所有的Java对象都是在Eden区被new出来的,绝大多数的Java对象的销毁也是在新生代。
对象分配的过程:
1 new的对象先放在Eden区(此区的大小受限制);
2当Eden区的空间被填满时,程序又需要创建对象,JVM垃圾回收器将对Eden区进行垃圾回收(Minor GC),将Eden中的不在被其他对象的对象进行销毁;
3然后将Eden中的幸存的对象移动到s0区;
4 s0区没有被回收的就会移动到s1区;
5注意:触发垃圾回收,s区的幸存的对象会在s0 s1互相移动,并且在这个过程中,年龄要+1;6在年龄达到一定的数值时会移动到老年代,这个参数 --XX:MaxTenuringThreshold = 来设置;过程如下图所示。
关于垃圾回收:频繁在年轻代收集,很少在老年代收集,几乎不再永久区/元空间收集。
Java堆内存是线程共享的!不是的!!!
Java对象的内存分配过程是如何保证线程安全的?
因为堆是全局共享的,因此在同一时间,可能有多个线程在堆上申请空间,那么,在并发场景中,两个线程先后把对象引用指向了同一个内存区域:
为了解决这个并发问题,对象的内存分配过程就必须进行同步控制,HotSpot虚拟机的方案:
每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。这种方案被称之为TLAB分配,即ThreadLocal Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。
什么是TLAB?
TLAB是虚拟机在堆内存的eden
划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间。如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
因为有了TLAB技术,堆内存并不是完完全全的线程共享,其eden区域中还是有一部分空间是分配给线程独享的。TLAB是线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。
6详细的介绍下程序计数器?(重点理解)
PC寄存器用于存储指向下一条指令的地址,计数器会存储当前线程正在执行的方法的JVM指令地址。
Q:为什么要使用PC寄存器记录下一条指令的地址?
A:因为程序执行时CPU会不断切换线程执行,线程切换回来后必须要知道接下来要执行哪一条指令。
Q:PC寄存器为什么要线程私有
A:CPU不断在线程之间切换,如果共用PC寄存器,比如线程1没执行完切换到线程2,线程2就会覆盖掉线程1将要执行的指令地址
7JVM内存结构的细化(永久代vs方法区)
针对java7及以前版本的细化:
堆和方法区连在了一起,但这并不能说堆和方法区是一起的,它们在逻辑上依旧是分开的。但在物理上来说,它们又是连续的一块内存,也就是说,方法区和Eden和老年代是连续的。
永久代(PermGen)
方法区与永久代本质上来讲并不等价,仅因为Hotspot使用永久代来实现方法区。在其他虚拟机上是没有永久代的概念的。也就是说方法区是规范,永久代是Hotspot针对该规范进行的实现。
Java7及以前版本的Hotspot中方法区位于永久代中。同时,永久代和堆是相互隔离的,但它们使用的物理内存是连续的。
永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。
但在Java7中永久代中存储的部分数据已经开始转移到Java Heap或Native Memory中了。比如,符号引用(Symbols)转移到了Native Memory;字符串常量池(interned strings)转移到了Java Heap;类的静态变量(class statics)转移到了Java Heap。
然后,在Java8中,时代变了,Hotspot取消了永久代。
元空间(Metaspace)
对于Java8,HotSpots取消了永久代,那么是不是就没有方法区了呢?当然不是,方法区只是一个规范,只不过它的实现变了。在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。
本地内存(Native memory),也称为C-Heap,是供JVM自身进程使用的。当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC。
针对Java8的调整,我们再次对内存结构图进行调整。
元空间存在于本地内存,意味着只要本地内存足够,它不会出现像永久代中“java.lang.OutOfMemoryError: PermGen space”这种错误。
默认情况下元空间是可以无限使用本地内存的,但JVM同样提供了参数来限制它使用的使用。
-XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
-XX:MaxMetaspaceSize,可以为class metadata分配的最大空间。默认是没有限制的。
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。
永久代为什么被替换了
表面上看是为了避免OOM异常:因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,如果使用默认值很容易遇到OOM错误;当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。
更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有所谓的永久代,也不需要开发运维人员设置永久代的大小,但是运行良好。