了解JVM可以通过以下个模块入手:Java运行时区域与线程、垃圾回收(GC)、Java的内存模型与线程
(1)Java的运行时区域
Java的运行时区域主要分成两类线程私有、线程共享
【线程私有】包括:
程序计数器
若正在执行的是java方法,则计数器记录的是正在执行的字节码指令的地址
若正在执行的是native方法,则计数器为空
该区域是唯一一个不会导致outofmemoryError的区域
虚拟机栈
描述的是Java方法执行的内存模型:每个方法都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息
局部变量表存放了编译期可知的基本数据类型,对象引用,和returnAddress类型(指向一条字节码指令地址),局部变量表的内存空间在编译器确定,在运行期不变
可导致两种异常:线程请求的栈深度大于虚拟机允许的深度-StackOverflowError;虚拟机无法申请到足够的内存-OutOfMemoryError
本地方法栈
和虚拟机栈类似,但它是为Native方法服务的
【线程共享】包括:
堆
java堆是被所有线程共享的内存区域,在虚拟机启动时创建,用来分配对象实例和数组
堆是垃圾回收器主要管理的区域,堆可分为新生代和老年代
从内存分配角度看,堆可划分出多个线程私有的分配缓冲区(TLAB)
大小可通过 -Xmx 和 -Xms 控制
方法区
用来存放虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等信息
GC会回收该区域的常量池和进行类型的卸载 *运行时常量池
Class文件的常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放在运行时常量池中
还把翻译出来的直接引用也放在运行时常量池中,运行时产生的常量也放在里面
(2)垃圾回收(GC)
引用计数法和可达性分析法
gc roots 类型
引用类型
两次标记过程
垃圾回收算法
内存分配策略
触发垃圾回收
垃圾回收器
也会回收方法区
JVM的垃圾回收主要是针对堆的,但也会回收方法区要进行垃圾回收,首先要判断对象是否存活,引出了两个方法:
引用计数法
思想:给对象设置引用计数器,没引用该对象一次,计数器就+1,引用失效时,计数器就-1,当任意时候引用计数器的值都为0时,则该对象可被回收
Java不适用原因:无法解决对象互相循环引用的问题
可达性分析法
虚拟机栈(栈帧中的局部变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(Native方法)引用的对象
以GC Roots为起点,从这些起点开始向下搜索,经过的路径称为引用链。若一个对象到GC Roots之间没有任何引用链,则该对象是不可达的。
那么可作为GC Roots的对象有
在可达性分析过程中,对象引用类型会对对象的生命周期产生影响,JAVA中有这几种类型的引用:
强引用:只要该引用还有效,GC就不会回收
软引用:内存空间足够时不进行回收,在内存溢出发生前进行回收、用SoftReference类实现
弱引用:弱引用关联的对象只能存活到下一次Gc收集、用WeakReference类实现
虚引用:无法通过虚引用获得对象实例,也不会对对象的生存时间产生影响、唯一目的:当该对象被Gc收集时,收到一个系统通知。用PhantomReference类实现
一个对象真正不可用,要经历两次标记过程:
首先进行可达性分析,筛选出与GC Roots没用引用链的对象,进行第一次标记
第一次标记后,再进行一次筛选,筛选条件是是否有必要执行finalize()方法。若对象有没有重写finalize()方法,或者finalize()是否已被jvm调用过,则没必要执行,GC会回收该对象
若有必要执行,则该对象会被放入F-Queue中,由jvm开启一个低优先级的线程去执行它(但不一定等待finalize执行完毕)。
Finalize()是对象最后一次自救的机会,若对象在finalize()中重新加入到引用链中,则它会被移出要回收的对象的集合。其他对象则会被第二次标记,进行回收
【JAVA中的垃圾回收算法有:】
标记-清除(Mark-Sweep)
两个阶段:标记, 清除
缺点:两个阶段的效率都不高;容易产生大量的内存碎片
复制(Copying)
把内存分成大小相同的两块,当一块的内存用完了,就把可用对象复制到另一块上,将使用过的一块一次性清理掉
缺点:浪费了一半内存
标记-整理(Mark-Compact)
标记后,让所有存活的对象移到一端,然后直接清理掉端边界以外的内存
分代收集
把堆分为新生代和老年代
新生代使用复制算法
将新生代内存分为一块大的Eden区和两块小的Survivor;每次使用Eden和一个Survivor,回收时将Eden和Survivor存活的对象复制到另一个Survivor(HotSpot的比例Eden:Survivor = 8:1)
老年代使用标记-清理或者标记-整理
【JVM内存分配规则】(对象主要分配在Eden,若启动了本地线程分配缓冲,将优先在TLAB上分配)
对象优先在Eden分配
当Eden区没有足够的空间时就会发起一次Minor GC
大对象直接进入老年代
典型的大对象是很长的字符串和数组
长期存活的对象进入老年代
每个对象有年龄计数器,每经过一次GC,计数器值加一,当到达一定程度时(默认15),就会进入老年代
年龄的阈值可通过参数 -XX:MaxTenuringThreshold设置
对象年龄的判定
Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可直接进入老年代,无须等到MaxTenuringThreshold要求的年龄
空间分配担保
发生Minor GC前,jvm会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若大于,则Minor GC是安全的
若不大于,jvm会查看HandlePromotionFailure是否允许担保失败,若不允许,则改为一次Full GC
若允许担保失败,则检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,则尝试进行Minor GC;若小于,则要改为Full GC
【垃圾收集器】
Serial(串行收集器)
特性:单线程,stop the world,采用复制算法
应用场景:jvm在Client模式下默认的新生代收集器
优点:简单高效
ParNew
特点:是Serial的多线程版本,采用复制算法
应用场景:在Server模式下常用的新生代收集器,可与CMS配合工作
Parallel Scavenge
特点:并行的多线程收集器,采用复制算法,吞吐量优先,有自适应调节策略
应用场景:需要吞吐量大的时候
SerialOld
特点:Serial的老年代版本,单线程,使用标记-整理算法
Parallel Old
Parallel Scavenge的老年代版本,多线程,标记-整理算法
CMS
对CPU资源敏感
无法处理浮动垃圾(并发清除 时,用户线程仍在运行,此时产生的垃圾为浮动垃圾)
产生大量的空间碎片
初始标记:stop the world 标记GC Roots能直接关联到的对象
并发标记:进行GC Roots Tracing
重新标记:stop the world;修正并发标记期间因用户程序继续运作而导致标记产生变动的 那一部分对象的标记记录
并发清除:清除对象
特点:以最短回收停顿时间为目标,使用标记-清除算法
过程:
优点:并发收集,低停顿
缺点:
G1
初始标记:stop the world 标记GC Roots能直接关联到的对象
并发标记:可达性分析
最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
并行与并发
分代收集
空间整合:从整体看是基于“标记-整理”的,从局部(两个region之间)看是基于“复制”的。
可预测的停顿:使用者可明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
特点:面向服务端应用,将整个堆划分为大小相同的region。
执行过程:
GC自适应调节策略 Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
【回收方法区】
永久代中主要回收两部分内容:废弃常量和无用的类
废弃常量回收和对象的回收类似
无用的类需满足3个条件
该类的所有实例对象已被回收
加载该类的ClassLoader已被回收
该类的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法