JVM,全称Java Virtual Machine,即java虚拟机。它让java语言实现了“write once,run anywhere”的效果,即一次编译,到处运行 ,也就是java语言的跨平台性。简单理解就是:.java文件通过java编译器成.class文件,然后将.class文件交给不同操作系统的JVM,JVM在进行二次编译,解释执行完成相关操作。JVM还有许多特性,需要我们理解和学习,下面,我就介绍一下JVM内存方面的相关知识。
定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。
定义Java 内存模型(Java Memory Model)来屏蔽掉各层硬件和操作系统的内存访问差异。
i. 所有的变量都存储在主内存(Main Memory)中,位于物理硬件的内存中;
ii. 每条线程自己的工作内存(Working Memory),工作内存中保存了被该线程使用的变量(主内存拷贝的副本),线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,存优先存储于寄存器和高速缓存中。
iii. 线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
内存间交互操作:
主内存相关操作:lock(锁定)、unlock(解锁)、read(读取)、write(写入)
工作内存相关操作:load(载入)、store(存储)、use(使用)、assign(赋值)
举例:
变量从主内存复制到工作内存,要顺序地执行read和load操作,反之顺序地执行store和write操作。Java要求上述操作按顺序执行,但没有保证是连续执行。此外Java内存模型还规定一些规则(省略)
内存屏障(内存栅栏):是一个CPU指令,可以保证一些特定操作执行的顺序和影响一些数据的可见性(原因:强制刷新不同CPU的缓存,例如,在使用写屏障前,会刷新数据到缓存,让试图读取该数据的线程都得到最新值)。
完全内存屏障:保障了早于屏障的内存读写操作的结果提交到内存之后,再执行晚于屏障的读写操作。
内存读屏障:仅确保了内存读操作。
内存写屏障:仅保证了内存写操作。
一块较小的内存空间,用来指示执行是哪条指令,它是‘线程私有’的内存(每一条线程都一个独立的程序计数器)。
Tips:
线程执行Java方法,程序计数器记录的是正在执行的字节码指令地址;线程执行的是Native方法,程序计数器值则为空(Undefined);程序计数器中存储空间大小不会随程序的执行而发生改变,所有他不会发生内存溢出(OutOfMemory)
栈是线程私有的(他栈不能访问),每个线程包含一个栈,它的生命周期与线程相同;
栈中只保存基础数据类型对象、对象引用和returnAddress类型(指向字节码的指针,即指向字节码指令的地址);
JVM栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
Java栈结构:
Tips:
如果线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError异常(栈溢出);虚拟机栈扩展时无法申请足够内存,抛出OutOfMemoryError异常(内存溢出)。
本地栈与虚拟机栈的作用非常相似;虚拟机栈为JVM执行Java方法(也就是字节码)服务,本地栈为JVM执行使用到的Native方法服务;它也会抛出StackOverflowError和OutOfMemoryError异常。
JVM只有一个堆,内存中最大的一块,它被所有线程共享,堆中只存放对象实例和数组(即对象本身);
Java堆是GC管理的主要区域,GC会自动进行空间释放;
Java堆分为:新生代和老年代。
Tips:
Java堆的大小无法扩展时,将抛出OutOfMemoryError异常;Java虚拟机规范规定:Java堆可以处于物理内存空间不连续,但逻辑上必须是连续的。
方法区是线程共享区域,它存储已被JVM加载的类信息(名称、方法信息、字段信息)、常量、静态变量和编译器编译后的代码等数据;
Tips:
方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
常量池存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的常量池中存放;
常量池具备动态性,它不要求常量一定只有在编译期才能产生,运行期间可以可能将新的常量放入常量池中。
常量池位置:
Tips:
常量池无法申请到内存时,将抛出OutOfMemoryError异常;
线程共享区域随JVM的启动而创建,关闭而销毁。
分配在JVM的运行时数据区以外的内存,这些内存直接受操作系统管理(而不是虚拟机),它可以减少垃圾回收对应用程序造成的影响。它使用NIO包下ByteBuffer来创建堆外内存。
Tips:
直接内存受本级总内存影响,如果超出将抛出OutOfMemoryError异常。
JDK8中,永久代已经被移除,被元数据区(元空间)的区域所取代。元空间(-MaxMetaspaceSize参数)的本质和永久代(-MaxPermSize参数)类似,都是对JVM中方法区的实现。
位置不同:元空间不在JVM中,而是使用本地内存。默认情况下,元空间的大小仅受本地内存限制,很大程度避免了内存溢出(OOM);
垃圾回收不同:
概念:用一组符号来描述所引用的目标,它可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
它包含三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
使用:第一次运行时,通过符号(字符串),逐层替换符号,找到符号引用代表的常量,然后将符号引用替换为直接引用,下次就不用搜索了。
概念:JVM能“直接使用”的数据。
直接引用形式:
i. 直接指向目标的指针(例,指向类对象、类变量、类方法的直接引用,可能是指向方法区的指针);
ii. 相对偏移量(例,指向实例变量、实例方法的直接引用都是偏移量);
iii. 一个能间接定位到目标的句柄。
规则一:对象优先在Eden分配(Eden空间不足,JVM将发起一次Minor GC,然后再把对象分配到Eden)
规则二:大对象直接进入老年代(-XX:PretenureSizeThreshold,大于该参数值的对象直接在老年代分配,但只对Serial和ParNew管用)
规则三:长期存活的对象将进入老年代(对象在Survivor区中每熬过一次Minor GC,年龄增加一岁。设置年龄阈值-XX:MaxTenuringThreshold,默认值15)
规则四:动态对象年龄判断(Survivor中相同年龄所有对象大小的总和大于Survivor的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄)
规则五:空间分配担保(JDK6之后,在发生Minor GC之前,JVM会先检查老年代的连续空间大小是否大于新生代对象总大小或者历次晋升的平均大小, 如果是则进行Minor GC, 否则将进行Full GC)
寄存器:最快的存储区, 由编译器根据需求进行分配,程序中无法控制,它用来暂存指令、数据和地址;
栈:存放基本类型的变量数据和对象的引用;
堆:存放所有new出来的对象。
静态域:存放静态成员(static定义的)
常量池:存放字符串常量和基本类型常量(public static final)
非RAM存储:硬盘等永久存储空间
内存溢出:内存不够用了【出现时候,见《JVM内存区域(运行时数据区)》】
内存泄漏:指对象可达,但是没用了。即本该被GC回收的对象并没有被回收
内存溢出原因:内存泄露积累起来将导致内存溢出(只是其中一种)。
内存泄漏原因:长生命周期的对象引用短生命周期的对象;没有将无用对象置为null。
新产生的对象最初分配在新生代,新生代空间不足时会进行一次Minor GC,Minor GC会把新生代满足条件的对象放入老年代,老年代空间不足时会进行Full GC,Full GC之后,如果空间还不足以存放新对象则抛出内存溢出异常。20200317
理论上使用GC不会存在内存泄露(Java被广泛使用的重要原因之一);然而在实际开发中,会存在不可用可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。
对象头,实例数据和对齐填充。
Tips:对象的大小总是8字节的整数倍。
对象在运行期间,对象头的Mard Word中存储的数据会随着锁标志位的变化而变化。对象头(非数组类型对象,32位的JVM)的数据结构如下: