运行时数据区
Java虚拟机运行时将其管理的内存区域划分为几个数据区。包括以下几个:
-
虚拟机栈(VM Stack)
- 虚拟机栈也是线程私有的。生命周期与线程相同。
- 虚拟机栈描述的是
Java方法执行的线程内存模型
:每个方法执行时,Java虚拟机会同步创建一个栈帧(Stack Frame)
用于存储局部变量表
,操作数栈
,动态链接
,方法出口
等信息。一个方法从执行到结束,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 - 对于C/C++中所谓的堆内存和栈内存,在Java中划分的要更为细腻一些。栈通常指的就是这里的虚拟机栈,或者更多情况下指的就是虚拟机栈的
局部变量表
部分。 - 局部变量表存放了编译器克制的各种Java虚拟机基本数据结构(boolean, byte, char, short, int, long, float, double)、对象引用(reference类型,指向对象起始地址的指针,或者指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向一条字节码指令的地址)。
- 上述数据结构在局部变量表中的存储空间以
局部变量槽
(slot)来表示的。其中64位的long和double型占两个槽,其余占1个。具体的每个槽占32bit还是64bit,取决于具体的虚拟机如何实现。 - 《Java虚拟机规范》中,对此内存区域规定了两类异常情况:
- 线程请求栈深超过虚拟机所允许栈深,抛
StackOverflowError
异常。 - 若虚拟机栈容量可动态伸展,栈扩展是无法申请足够内存时,抛
OutOfMemoryError
异常。
- 线程请求栈深超过虚拟机所允许栈深,抛
-
本地方法栈(Native Method Stack)
该内存区域发挥的作用类似虚拟机栈,只是为本地方法提供服务。有的虚拟机(如HotSpot)直接把本地方法栈和虚拟机栈合为一体。抛异常也同虚拟机栈。
-
程序计数器(Program Counter Register)
-
是一块较小的内存空间 。是程序控制流的指示器。可以看做当前线程所执行java字节码的行号指示器。
-
是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖这个计数器来完成。
-
线程私有。
-
若线程执行的是一个Java方法,PCR记录的是正在执行的虚拟机字节码指令的地址;如果是本地方法(Native),这个计数器的值应为空。
-
-
堆区(Heap)
- Java堆(Java Heap)是虚拟机所管理的内存中最大的一块儿。堆是所有线程共享的内存区域,在虚拟机启动时创建。此内存区域的唯一作用就是存放
对象实例
,java世界里几乎所有的对象实例都在这里分配内存。 - Java堆是垃圾收集器管理的内存区域。GC操作的目标区域。
- 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。堆即可被实现为固定的,也可被实现为可扩展的,不过当前主流虚拟机都将其实现为可扩展的。(通过参数 -Xms 和 -Xmx设定)。
- 如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,抛
OutOfMemoryError
异常。
- Java堆(Java Heap)是虚拟机所管理的内存中最大的一块儿。堆是所有线程共享的内存区域,在虚拟机启动时创建。此内存区域的唯一作用就是存放
-
方法区(Method Area)
- 方法区同Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的
类型信息
、常量
、静态变量
、即时编译器编译后的代码缓存
等数据。《Java虚拟机规范》中把方法区描·述为堆的一个逻辑部分,但它却有一个别名“非堆”(Non-Heap),目的是与Java堆区分开来。 - **static final Class 常量池 **放在方法区
- 方法区同Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的
方法区指的是理论概念。
持久代/元空间指的是实现机制。
JDK 1.6 及之前:有永久代,字符串常量池和运行时常量池都存在持久代。
JDK 1.7 : 有永久代,字符串常量池移到堆区 运行时常量池还在方法区。
JDK 1.8 及之后 :无永久代,字符串常量池在堆区,运行时常量池在元空间。
双亲委派机制
什么是双亲委派?
- 类加载器收到类加载请求
- 将这个请求向上委托给父类加载器完成,一直向上,直到bootstrap类加载器。
- bootstrap检查是否能加载这个类,能加载就结束,不能加载就抛出异常,通知子加载器加载该类
- 重复步骤3。
双亲委派机制的作用?
让基础类得以正确地统一地加载。出于一种安全的考虑,不让破坏基础类?越基础的类越由上层的类加载器加载。
破坏双亲委派机制?
为什么要破坏
基础类型要调用用户的代码怎么办?由于加载范围限制,某些情况父要委托子加载Class,就不得不破坏双亲委派机制。
破坏方法
线程上下文类加载器(ThreadContextClassLoader)
Java.lang.Thread.setContextClassLoader()方法
JDK1.6提供了java.util.ServiceProvider
类
类加载
启动程序如何查看加载了哪些类
-XX:+TraceClassLoading [类名]
-verbose [类名]
类必须初始化的时机
1.遇到 new、getstatic、putstatic、invokestatic 4 个字节码指令时。
2.使用 java.lang.relect 包的方法对类进行反射调用时。
3.当初始化一个类的时候,如果父类没被初始化,则先初始化父类。
4.当虚拟机启动时,指定了执行主类(main()的方法的那个类),则需要先初始化这个主类。
5.JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后解析结果 REFgetStatic,REFputStatic,REF_invokeStatic 的方法句柄,且该句柄对应的类没有初始化,则需要先初始化这个类。
类加载的过程/类的生命周期
- 加载
-
将类以全类名方式获取二进制流,
-
字节流的静态存储结构==>方法区的运行时数据结构。
-
生成描述该类的Class对象。作为在方法区内这个类的数据的访问入口。
- 链接
2.1 验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
2.2 准备
该阶段是正式为类变量分配内存并设置类变量初始值。强调一点,这里说的类变量是指被 static 修饰的,而不是实例变量。
2.3 解析
该阶段虚拟机将常量池内的符号引用替换为直接引用的过程。
- 初始化
类或者类执行初始化方法,类变量真正被赋值的阶段。
-
使用
-
卸载
判断对象已死
引用计数法
比如对象 a,只要任何一个对象引用了 a,则 a 的引用计数器就加 1,当引用失效时,引用计数器就减 1,当计数器为 0 时,就可以对其回收。
无法解决循环引用的问题。
可达性分析算法
以跟为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达(使用根搜索算法后,内存中 的存活对象都会被根对象集合直接或间接连接着),如果目标对象不可达,就意味着该对象己经死亡,便可以在 instanceOopDesc 的 Mark World 中将其标记为垃圾对象。
在根搜索算法中, 只有能够被根对象集合直接或者间接连接的对象才是存活对象。
可以作为GC Roots的对象
- JAVA栈中引用的对象
- 方法区中类静态属性引用的变量
- 方法区中常量引用的对象
- Native方法引用的对象
- JVM内部的引用,如Class对象,常驻的异常对象,系统类加载器等
- 所有被同步锁(Synchronized关键字)持有的对象。
- 反应Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等。
GC算法
术语规定:
- 部分收集(Partial GC): 不是收集整个Java堆的垃圾收集
- 新生代收集(Minor GC/Young GC):目标只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC): 目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为
- 混合收集(Mixed GC)
- 整堆收集(Full GC)
标记-复制
他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:解决了内存碎片问题。
缺点:将原来的内存缩小为原来的一半,存活对象越多效率越低。
标记-清除
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有的被标记对象。
缺点:
- 标记和清除的效率都不高。
- 空间问题,清除后产生大量不连续的内存随便。如果有大对象会出现空间不够的现象从而不得不提前触发另一次垃圾收集动作。
标记-整理
先标记出要被回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。解决了复制算法和标记清理算法的问题。
常用垃圾收集器
Serial收集器
单线程收集器,单线程的含义在于它会 stop the world。垃圾回收时需要 stop the world ,直到它收集结束。所以这种收集器体验比较差。
ParNew收集器
Serial 收集器的多线程版本,除了使用采用并行收回的方式回收内存外,其他行为几乎和 Serial 没区别。
可以通过选项“-XX:+UseParNewGC”手动指定使用 ParNew 收集器执行内存回收任务。
Parallel Scavenge收集器
是一个新生代收集器,也是复制算法的收集器,同时也是多线程并行收集器,与 PartNew 不同是,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码 的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)), 高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
他可以通过 2 个参数精确的控制吞吐量,更高效的利用 cpu。
分别是: -XX:MaxCcPauseMillis 和 -XX:GCTimeRatio
Serial Old收集器
Serial 收集器的老年代版本,使用单线程和标记-整理算法。
Parallel Old收集器
Parallel Scavenge 收集器的老年代版本,使用多线程和标记-整理算法。JDK 1.6 中才开始提供。
CMS收集器
Concurrent Mar Sweep 收集器是一种以获取最短回收停顿时间为目标的收集器。重视服务的响应速度,希望系统停顿时间最短。采用标记-清除的算法来进行垃圾回收。
过程:
-
初始标记 (stop the world)
-
并发标记
-
重新标记 (stop the world)
-
并发清除
初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
并发标记就是进行 Gc Roots Tracing 的过程。
重新标记则是为了修正并发标记期间,因用户程序继续运行而导致的标记产生变动的那一部分对象的标记记录,这个阶段停顿时间一般比初始标记时间长,但是远比并发标记时间短。
整个过程中并发标记时间最长,但此时可以和用户线程一起工作。
优缺点:
优点:并发收集、低停顿
缺点:
- 对 cpu 资源非常敏感。
- 无法处理浮动垃圾。
- 内存碎片问题。
Garbage First(G1)收集器
Garbage First 收集器是当前收集器技术发展的最前沿成果。jdk 1.6_update14 中提供了 g1 收集器。
G1 收集器是基于标记-整理算法的收集器,它避免了内存碎片的问题。
可以非常精确控制停顿时间,既能让使用者明确指定一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不多超过 N 毫秒,这几乎已经是实时 java(rtsj)的垃圾收集器特征了。
G1收集器是如何改进收集方式的?
极力避免全区域垃圾收集,之前的收集器进行收集的范围都是整个新生代或者老年代。而 g1 将整个 Java 堆(包括新生代、老年代)划分为多个大小固定的独立区域,并且跟踪这些区域垃圾堆积程度,维护一个优先级列表,每次根据允许的收集时间,优先回收垃圾最多的区域。从而获得更高的效率。
各收集器特点对比
垃圾收集器 | 串行并行并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境下Server模式,与CMS配合 |
Serial Old | 串行 | 老年代 | 标记整理 | 响应速度优先 | 单CPU环境Client模式,CMS后备预案 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不用太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记整理 | 吞吐量优先 | 在后台运算而不用太多交互的任务 |
CMS | 并发 | 老年代 | 标记清除 | 响应速度优先 | 集中在互联网或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 标记整理+复制算法 | 响应速度优先 | 面向服务端,将来替换CMS |
JVM内存调试工具
虚拟机进程状况
jps (Jvm process status tool ),他的功能与 ps 类似
列出正在运行的虚拟机进程,并显示执行主类(Main Class,main()函数所在的类)的名称,以及进程的本地虚拟机的唯一 ID。
虚拟机统计信息工具
jstat(JVM Statistics Montoring Tool)
Jstat是用于监视虚拟机各种运行状态信息命令行工机具。他可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、jit 编译等运行数据。
jstat [option vmid [interval[s|ms] [count]] ]
interval 查询间隔
count 查询次数
如果不用这两个参数,就默认查询一次。
option 代表用户希望查询的虚拟机信息,主要分 3 类:
- 类装载
- 垃圾收集
- 运行期编译状况
jstat 工具主要选项?
-class 监视类装载、卸载数量、总空间及类装载锁消耗的时间
-gc 监视 Java 堆状况,包括 Eden 区,2 个 survivor 区、老年代
-gccapacity 监视内容与-gc 基本相同,但输出主要关注 Java 堆各个区域使用的最大和最小空间
-gcutil 监视内容与-gc 基本相同,主要关注已经使用空间站空间百分比
-gccause 与-gcutil 功能一样,但是会额外输出导致上一次 GC 产生的原因
-gcnew 监视新生代的 GC 的状况
-gcnewcapacity 监视内容与 -gcnew 基本相同,输出主要关注使用到的最大最小空间
-gcold 监视老年代的 GC 情况
-gcoldcapacity 监控内容与 -gcold 基本相同,主要关注使用到的最大最小空间
-compiler 输出 jit 编译器编译过的方法、耗时等信息
内存映像工具
jmap(Memory Map for Java) 命令用于生成堆转储快照(一般称为 heapdump 或 dump 文件)。
语法 :jmap [option] vmid
它还可以查询 finalize 执行队列,Java 堆和永久代的详细信息,如果空间使用率、当前用的是哪种收集器等。
- -dump 生成 Java 堆转储快照,其中 live 自参数说明是否只 dump 出存活对象
- -finalizerinfo 显示在 F -Queue 中等待 Finalizer 线程执行 finalize 方法的对象。只在 Linux/Solaris 平台下有效
- -heap 显示 Java 堆详细信息,如使用哪种回收器、参数配置、分代状况。
- -histo 显示堆中对象统计信息、包括类、实例数量和合计容量。
- -F 当虚拟机进程对-dump 选项没有响应时,可使用这个选项强制生成 dump 快照。
虚拟机堆转存储快照分析工具
jhat ( JVM Heap Analysis Tool) 用来分析 jmap 生成的堆转储快照。
堆栈跟踪工具
jstack(Stack Trace for Java) 命令用于生成虚拟机当前时刻的线程快照(一般称为 thread dump 或 javacore 文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因。
语法: jstack [option] vmid
-F 当正常输出的请求不被响应时,强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用本地方法的话,可以显示 C/C++ 的堆栈