Java虚拟机:内存区域与内存模型、垃圾收集、类文件结构及类加载机制、线程与锁优化、jdk命令行与可视化工具
一、Java虚拟机内存区域
Java虚拟机运行时数据区包括几部分内存:方法区、Java堆、虚拟机栈、本地方法栈、程序计数器
1、程序计数器:线程私有的内存区域
字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令。
多线程就是通过线程轮流切换并分配处理器执行时间的方式来实现的,任何时刻一个处理器都只会执行一个线程的指令,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器
它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域
2、虚拟机栈:线程私有,生命周期与线程相同。Java方法执行时就在虚拟机栈内创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法从调用到执行完成的过程就是栈帧在虚拟机栈中入栈到出栈的过程。
虚拟机栈是为虚拟机执行Java方法服务的(对比本地方法栈)
内存异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出栈溢出StackOverFlowError异常
如果虚拟机可以动态扩展,而扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
3、本地方法栈:线程私有,作用类似虚拟机栈,区别是本地方法栈为虚拟机使用到的native方法服务。本地方法栈会抛出跟虚拟机栈一样的异常。
4、Java堆:所有线程共享的内存区域(新生代、老年代),在虚拟机启动时创建。此区域唯一目的就是存放对象实例。
Java堆是垃圾收集器管理的主要区域
内存异常:如果在堆中没有内存完成实例分配且堆也无法扩展时,将抛出OutOfMemoryError异常。
5、方法区:线程共享的内存区域(永久代)。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
内存异常;当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常
6、运行时常量池:方法区的一部分
class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
7、虚拟机在堆中对象分配、布局、访问的全过程
(1)检查:虚拟机遇到一条new指令时,先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有则先执行相应的类加载过程;
(2)内存分配:类加载完成后对象所需的内存大小既已确定。若堆中内存绝对规整,则通过指针碰撞的方式在空闲的内存中移动指针来为对象分配内存;若堆中内存并不规整,则通过空闲列表的方式将可用内存块维护一个列表,从列表中找一块足够大的空间划分给对象实例。
注:(堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定,因此,在使用Serial\ParNew等采用复制算法的收集器时,系统采用指针碰撞,在使用CMS这种采用标记-清除算法的收集器时,系统采用空闲列表)
(3)初始化:内存分配完成后,虚拟机要将分配到的内存空间都初始化为零值
(4)设置:对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等。这些信息存放在对象的对象头中
执行new指令后接着执行initial()方法,按程序猿意愿把对象进行初始化
二、垃圾收集器、内存分配与回收策略
了解GC和内存分配的原因:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们需要对这些自动化的技术实施必要的监控和调节。
1、如何判断对象已死:(1)引用计数算法;(2)可达性分析算法
(1)是在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;当计数器值为0时对象便不可再被使用
但是这种算法不能解决对象之间相互循环引用的问题。
(2)是主流用来判断对象是否存活的做法。
算法思路是:由GC Roots节点开始向下搜索,当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的。
可作为GC Roots的对象包括:1-虚拟机栈中引用的对象;2-方法区中类静态属性引用的对象;3-方法区中常量引用的对象;4-本地方法栈引用的对象
注意:在可达性分析算法中不可达的对象,要经历两次标记过程,才会真正对象死亡:第一次标记是发现没有与GC Roots相连接的引用链,第二次标记是当对象覆盖了finalized()方法且虚拟机没调用过finalized方法时,若对象被判定有必要执行finalize()方法,将对象放入F-queue队列中由Finalizer线程去执行,GC将对F-Queue中的对象进行第二次标记。若没有逃脱则对象真的被回收了。。
!任何一个对象的finalize()方法只会被系统自动调用一次!
2、判断类无用要满足的条件:
(1)该类的所有实例都已经被回收(即堆中不存在该类的实例)
(2)加载该类的classLoader已经被回收
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;
3、垃圾收集算法
1-标记-清除算法:老年代使用
先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象
不足有两个:a-效率问题,标记和清除这两个过程效率都不高;b-空间问题,标记清除之后会产生大量不连续的内存碎片。
2-复制算法:新生代使用
将可用内存按容量划分为大小相等的两块,每次使用其中一块。当一块用完了就将还存活的对象复制到另一块上,再把已使用的内存空间一次清理掉
主要用来回收新生代(新生代中的对象都是朝生夕死的),将内存划分为Eden和两个Survivor空间,每次使用Eden和一个Survivor,回收的时候将活着的对象一次性复制到另一块Survivor上,最后清理掉Eden和刚才用过的Survivor。其中Eden和Survivor的大小比例是8:1
3-标记-整理算法:老年代使用
标记过程与标记清除算法一样,后续让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存(解决了标记清除算法产生不连续内存空间的问题)
4-分代收集算法:即新生代用复制算法,老年代用标记清除或标记整理算法
4、垃圾收集器:一般都搭配使用,没有一种收集器是完美的,都是出于某种目的(缩短停顿时间或控制吞吐量)而产生的
1-Serial收集器
单线程收集器,使用复制算法,进行垃圾收集时,必须暂停其他所有工作线程,直到收集结束。
特点:简单高效,没有线程交互的开销
适用:虚拟机运行在client模式下的默认新生代收集器
2-ParNew收集器
ParNew是Serial的多线程版本,除使用多线程进行垃圾收集外,与Serial可用的控制参数、收集算法、对象分配规则、回收策略等完全一样
默认开启的收集线程数与CPU的数量相同(CPU非常多时可以通过参数-XX:ParallelGCThreads来限制垃圾收集器的线程数)
特点:目前只有它能与CMS收集器配合使用
适用:运行在Server模式下的虚拟机首选的新生代收集器
注意:从ParNew开始接触到并发和并行的收集器,并行Parallel是指多条垃圾收集线程并行工作,此时用户线程处于等待状态,并发Concurrent是指用户线程与垃圾收集线程同时执行,不一定并行,可能会交替执行,用户程序在继续运行而垃圾收集程序运行在另一个CPU上
3-Parallel Scavenge收集器
使用复制算法,并行的多线程收集器,也是新生代收集器
特点:关注点是目标是去达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
适用:高吞吐量可以高效利用CPU,适合在后台运算而不需要太多用户交互的任务
扩展:
用于精确控制吞吐量的两个参数:控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis,直接设置吞吐量大小-XX:GCTimeRatio
自适应调节策略参数-XX:UseAdaptiveSizePolicy
MaxGCPauseMillis允许大于0 的毫秒数,收集器尽可能保证内存回收话费的时间不超过它,并不是设置的越小越好,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的
GCTimeRatio参数的值是(0,100)内的整数,是吞吐量的倒数
UseAdaptiveSizePolicy是一个开关参数,打开后不需要手动指定新生代的大小、Eden与Survior区的比例、晋升老年代对象大小等参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量
4-Serial Old收集器
Serial的老年代版本,使用标记整理算法,单线程收集器,也是老年代收集器
适用:给Client模式下的虚拟机使用。Server模式下一方面可以与Parallel Scavenge搭配使用,另一方面是作为CMS的后备预案
5-Parallel Old收集器
Parallel Scavenge的老年代版本,使用多线程和标记整理算法,老年代收集器
特点:与Parallel Scavenge配合使用
6-CMS收集器
以获取最短停顿时间为目标的老年代收集器,使用标记-清除算法
适用:与用户交互希望系统停顿时间最短的应用
标记过程:初始标记:标记GC Roots能关联到的对象,需要stop the world
并发标记:进行GC Roots Tracing的过程
重新标记:修正并发标记期间用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,停顿时间比初始标记长,需要stop the world
并发清除:
优点:并发收集、低停顿
缺点:1-对CPU资源非常敏感,默认启动的回收线程数是(CPU数量+3)/4;
2-无法处理浮动垃圾(浮动垃圾是指CMS并发清理的同时用户线程运行产生的垃圾,没有被标记,当次收集处理不掉),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生;
3-标记清除算法产生大量的空间碎片,CMS提供了-XX:UseCMSCompactAtFullCollection开关参数,默认开启,当收集器顶不住要进行fullGC时开启内存碎片的合并整理过程
7-G1收集器
面向服务端应用的垃圾收集器,将整个Java堆划分成多个大小相等的独立区域Region
特点:1、并行与并发:使用多CPU来缩短stop- the- world停顿的时间同时通过并发的方式让Java程序继续执行
2、分代收集:G1可以独立管理GC堆,采用不同方式处理新创建的对象、已经存活一段时间且熬过多次GC的旧对象
3、空间整合:整体来看基于标记整理算法,局部来看基于复制算法实现,不会产生内存空间碎片
4、可预测的停顿:能建立可预测的停顿时间模型,指定在M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒,可以避免在整个堆中进行全区域的垃圾收集
标记过程:初始标记:仅标记GC Roots能直接关联到的对象,并修改TAMS(next top at mark start)的值,让下一阶段用户程序并发运行时能在正确可用的region中创建对象
并发标记:可达性分析找出存活的对象
最终标记:修正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记记录,
筛选回收:对region的回收价值和成本进行排序,根据用户期望的停顿时间来制定回收计划
5、内存分配与回收策略
内存分配规则:
1-对象优先在Eden分配:大部分情况下对象在新生代Eden区中分配,没有足够空间时虚拟机将发起一次Minor GC
备注:新生代GC即Minor GC是发生在新生代的垃圾收集动作,老年代GC即Major GC/Full GC是发生在老年代的垃圾收集,速度比Minor GC慢10倍
2-大对象直接进入老年代:大对象是指需要大量连续内存空间的Java对象,典型的就是长字符串或数组。-XX:PretenureSizeThreshold参数可以设置大于这个参数值的对象直接分配在老年代,可避免新生代的内存区域发生大量内存复制
3-长期存活的对象将进入老年代:虚拟机给每个对象定义一个年龄计数器,对象在Eden出生并经过一次Minor GC后仍存活且能被Survivor容纳,将被移入Survivor并对象年龄设为1,对象在SurvI or中每熬过一次Minor GC年龄就加1,加到一定程度(默认是15)就会晋升到老年代中。年龄阈值可以通过参数-XX:MaxTenuringThreshold设置
4-动态对象年龄判定:如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold设置的值
5-空间分配担保:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若成立则Minor gc可确保是安全的,若不成立则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,若允许则会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试进行一次Minor gc,如果小于或者HandlePromotionFailure设置不允许冒险,则改为进行一次Full gc.不过HandlePromotionFailure在代码里已经不在使用,所以只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor gc,否则将进行Full gc
三、类文件结构与类加载机制
四、Java虚拟机内存模型与线程
五、线程安全与锁优化
六、jdk命令行与可视化工具