JVM的简化架构
运行时数据区
包括:程序计数器(PC寄存器)、Java虚拟机栈、Java堆、方法区、运行时常量池、本地方法栈等等。
PC寄存器,也叫程序计数器
1、JVM支持多个线程同时运行,每个线程拥有一个程序计数器,是线程私有的,用来存储指向下一条指令的地址。
2、在创建线程的时候,创建相应的程序计数器。
3、执行本地native方法时,程序计数器的值为undefined。
4、是一块比较小的内存空间,是唯一一个在JVM规范中没有规定OutOfMemoryError的内存区域。
虚拟机栈
栈是由一系列帧(Frame)组成(因此Java栈也叫作帧栈),是线程私有的。
帧是用来保存一个方法的局部变量、操作数栈(java没有寄存器,所有的参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等。
每一次方法调用创建一个帧并压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁。
局部变量表存放了编译期可知的各种基本数据类型和引用数据类型、每个slot存放32位的数据,long、double占两个槽位。
栈的优点:存取速度比堆快,仅次于程序计数器。
栈的缺点:存在栈中的数据太小,生存期是在编译期决定的,缺乏灵活性。
StackOverflowError异常:当线程请求的栈深度大于虚拟机所允许的深度;
OutOfMemoryError异常:如果栈的扩展时无法申请到足够的内存。
Java堆
用来存放应用系统创建的对象和数组,所有线程共享Java堆。
GC主要管理堆空间,对分代GC来说,堆也是分代的。
堆的优点:运行期动态分配内存大小,自动进行垃圾回收。
堆的缺点:效率相对较慢。
方法区
方法区是线程共享的,通常用来保存装载的类的结构信息。
通常和元空间关联在一起,但具体的跟JVM实现和版本有关。
JVM规范把方法区描述为堆的一个逻辑部分,但它有一个别名称为Non-heap(非堆),应该是为了和Java堆区分开。
运行时常量池
是Class文件中每个类或接口的常量池表,在运行期间的表示形式,通常包括:类的版本、字段、方法、接口等信息。
运行时常量池在方法区中分配
通常在加载类和接口到JVM后,就创建相应的运行时常量池。
本地方法栈
在JVM中用来支持native方法执行的栈就是本地方法栈。
在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
栈、堆、方法区交互关系
Java堆内存模型和分配
Java堆用来存放应用系统创建的对象和数组,所有线程共享Java堆。
Java堆是在运行期动态分配内存大小,自动进行垃圾回收。
Java垃圾回收(GC)主要是回收堆内存,对分代GC来说,堆也是分代的。
Java堆的结构
Java堆的结构
堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在堆上进行分配。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。堆有自己进一步的内存分块划分,按照GC分代收集角度的划分。
新生代用来放新分配的对象,新生代中经过垃圾回收,没有回收掉的对象,被复制到老年代中。
老年代存储的对象比新生代存储的对象的年龄大的多。
老年代会存储一些大对象。
整个堆大小=新生代+老年代
新生代=Eden+存活区
从前的持久代用来存放Class、Method等元信息的区域,从JDK8开始去掉了,取而代之的是元空间(MetaSpace),元空间并不在虚拟机里面,而是直接使用本地内存。
对象的内存布局
对象在内存中存储的布局(这里以HotSpot虚拟机为例说明),分为:对象头、实例数据和对齐填充。
对象头包含两个部分:
1、MarkWord:用于存储对象自身的运行时数据,如:HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
2、类型指针:对象指向它的类元数据的指针。
实例数据:
真正存放对象实例数据的地方
对齐填充:
这部分不一定存在,也没什么特别意义,仅仅是占位符。这是因为HotSpot要求对象起始地址都是8字节的整数倍,如果不是就对齐。
对象的访问定位
在JVM规范中只规定了reference类型是一个指向对象的的引用,但没有规定这个引用具体如何去定位,访问堆中对象的具体位置。
因此对象的访问方式取决于JVM的具体实现,目前主流的有:使用句柄、使用指针两种方式。
使用句柄:Java堆中会划分出一块内存来做句柄池,reference中存储句柄地址,句柄中存储对象的实例数据和类元数据的地址。
通过句柄访问对象
使用指针
:
Java堆中会存放访问类元数据的地址,reference存储的就直接是对象的地址。
使用指针访问对象
各自的优势:
句柄访问:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针访问:速度快,它节省了一次指针定位的时间开销,由于对象的访问在JAVA中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。
trace跟踪参数
-verbosec:打印gc简要信息
-XXPrintGC:打印gc简要信息
-XXPrintGCDetails:打印GC详细信息
-XXPrintGCTimeStamps:打印Gc发生的时间戳
-Xlogc:log/gc.log:指定GClog的位置,以文件输出
-XXPrintHeapAtGC:每一次gc后,都打印堆信息
-Xlogc+heap=debug:每一次gc后,都打印堆信息
-XXTraceClassLoading:监控类的加载
GC日志格式
GC发生的时间,也就是JVM从启动以来经过的秒数
日志级别信息和日志类型标记
GC识别号
GC类型和说明GC的原因
容量:GC前容量—GC后容量(该区域总容量)
GC持续时间、单位秒。有的收集器会有更详细的描述,比如:user表示应用程序消耗的时间,sys表示系统内核消耗的时间,real表示操作从开始到结束的时间。
Java堆的参数
-Xms:初始堆大小,默认为物理内存的1/64。
-Xmx:最大堆大小,默认物理内存1/4。
-Xmn:新生代大小,默认整个堆的3/8。
-XX:MinHeapFreeRatio:设置堆空间最小空闲比例。当对空间的空闲内存小于这个数值时,JVM便会扩展堆空间。
-XX:MaxHeapFreeRatio:设置堆空间的最大空闲比例。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆。
-XX:NewSize:设置新生代的大小。
-XX:NewRatio:设置老年代与新生代的比例,它等于老年代大小除以新生代大小。
-XX:SurviorRatio:新生代中eden区与survivior区的比例,设置为8,则两个Survior区与一个Eden区的比值为2,一个Survior区占整个新生代的1/10。
-XXargetSurvivorRatio:设置survivior区的可使用率。当survivior区的空间使用率达到这个数值时,会将对象送入老年代。
-XX:+HeapDumpOnOutOfMemoryError:OOM时导出堆到文件。
-XX:HeapDumpPath:导出OOM的路径。
-XX:OnOutOfMemoryError:在OOM时执行一个脚本。
Java栈的参数
-Xss:设置线程栈的大小,通常只有几百K,决定了函数调用的深度。
元空间的参数
-XX:MetaspaceSize:元空间GC阈值(JDK1.8)
-XX:MaxMetaspaceSize:最大元空间大小(JDK1.8)
-XX:MaxDirectMemorySize:直接内存大小,默认为最大堆空间
-XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空间容量的百分比。
-XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空间容量的百分比。
内存分配与回收策略
内存分配,主要就是堆内存分配,(也有可能经过JIT编译后被拆散为标量类型并间接的栈上分配),主要分配在新生代的eden上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配,少数情况下可能直接分配在老年代中。分配的规则不是百分百固定的。取决于垃圾收集器的组合和虚拟机的参数设置
对象优先在Eden分配
大多数情况下,对象在新生代eden区中分配,当Eden区没有足够空间进行分配时,发起一次minorGC。虚拟机提供-XX:+PrintGCDetails这个收集器日志参数,来打印日志收集日志。
大对象直接进老年代
大对象:需要大量连续内存空间的java对象如很长的字符串和数组。-XX:PretenureSizeThreshold设置大于这个值的对象直接分配在老年代
长期存活的对象进入老年代
-XX:MaxTenuringThreshold设置年龄大于多少的对象进入老年代默认15对象在survivor中每熬过一次minorGC年龄加一岁
动态对象年龄判断
当survivor空间中相同年龄所有对象大小综合大于survivor空间的一半,年龄大于或等于该年龄的对象可以提前进入老年代,无需等到年龄达到。
空间分配担保
发生minorGC前虚拟机计算老年代的连续空间是否大于新生代对象总大小或历次晋升的平均大小,是就会进行MinorGC否则进行FullGC