基础 : 可达性分析算法 GC ROOTShtml
如下部份内容 来自 这个博主的文章java
标记清除法/标记压缩法、复制收集算法、引用计数法程序员
这里的 引用计数法 由于书中讲解少,因此讲一下:
引用计数法,它的基本原理是,在每一个对象中保存该对象的引用计数,当引用发生增减时对计数进行更新。引用计数的增减,通常发生在变量赋值、对象内容更新、函数结束(局部变量再也不被引用)等时间点。当一个对象的引用计数变为0时,则说明它未来不会再被引用,所以能够释放相应的内存空间。
缺点:算法
分代回收的基本思路,是利用了通常性程序所具有的性质,即大部分对象都会在短期内成为垃圾,而通过必定时间依然存活的对象每每拥有较长的寿命。
HotSpot 虚拟机中,在新生代用复制算法,老年代使用标记清除/整理算法。express
问题:若是存在老生代对象对新生代对象的引用。若是只扫描新生代区域的话,那么从老生代对新生代的引用就不会被检测到。
这样一来,若是一个年轻的对象只有来自老生代对象的引用,就会被误认为已经“死亡”了。
所以,在分代回收中,会对对象的更新进行监视,将从老生代对新生代的引用,
记录在一个叫作记录集 Rset(remembered set)的表中。在执行小回收(Minor Gc)的过程当中,这个记录集也做为一个根来对待。segmentfault
解决方案:在老生代到新生代的引用产生的瞬间,就必须对该引用进行记录,而负责执行这个操做的子程序,须要被嵌入到全部涉及对象更新操做的地方。
这个负责记录引用的子程序是这样工做的。设有两个对象:A和B,当对A的内容进行改写,并加入对B的引用时,
若是①A属于老生代对象,②B属于新生代对象,则将该引用添加到记录集中。
这种检查程序须要对全部涉及修改对象内容的地方进行保护,所以被称为写屏障(Write barrier)。数组
不过,相对于在一个CPU上进行GC任务分割的增量回收来讲,并行回收能够利用多CPU的性能,尽量让这些GC任务并行(同时)进行。安全
为了支持高频率的新生代的回收,虚拟机使用一种叫作卡表(Card Table)的数据结构.
卡表做为一个比特位的集合,每个比特位能够用来表示年老代的某一区域中的全部对象是否持有新生代对象的引用。服务器
1、做用
卡表中每个位表示年老代4K的空间,
卡表记录为 0 的年老代区域没有任何对象指向新生代,
卡表记录为 1 的区域才有对象包含新生代引用,
所以在新生代GC时,只须要扫描卡表位为1所在的年老代空间。使用这种方式,能够大大加快新生代的回收速度。数据结构
2、结构
卡表是个单字节数组,每一个数组元素对应堆中的一张卡。
每次年老代对象中某个引用新生代的字段发生变化时,Hotspot VM就必须将该卡所对应的卡表元素设置为适当的值,从而将该引用字段所在的卡标记为脏。
以下图:
在Minor GC过程当中,垃圾收集器只会在脏卡中扫描查找年老代-新生代引用。
Hotspot VM的字节码解释器和JIT编译器使用写屏障 维护卡表。
写屏障 (Write barrier) 是一小段将卡状态设置为脏的代码。 解释器每次执行更新引用的字节码时,都会执行一段写屏障,JIT编译器在生成更新引用的代码后,也会生成一段写屏障。
虽然写屏障使得应用线程增长了 -- 性能开销,但Minor GC变快了许多,总体的垃圾收集效率也提升了许多,一般应用的吞吐量也会有所改善。
一、 吞吐量
应用系统的生命周期内,应用程序所花费的时间和系统总运行时间的比值
系统总运行时间=应用程序耗时+GC耗时
二、 垃圾回收器负载
垃圾回收器负载=GC耗时/系统总运行时间
三、 停顿时间
垃圾回收器运行时,应用程序的暂停时间
四、 垃圾回收频率
垃圾回收器多长时间运行一次。通常而言,频率越低越好,一般增大堆空间能够有效下降垃圾回收发生的频率,可是会增长回收时产生的停顿时间。
五、 反应时间
当一个对象成为垃圾后,多长时间内,它所占用的内存空间会被释放掉。
-Xms 堆大小
-Xmx 可扩展大小
-Xmn 老年代大小
-XX:SurvivorRatio Eden 区与 Survivor 区大小比例
注: surivor 区分为 from 区与 to 区
- 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。 - 紧接着进行GC,Eden区中全部存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。 - 年龄达到必定值(年龄阈值,能够经过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域 - 通过此次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From” - 新的“From”就是上次GC前的“To”。 - 无论怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满以后,会将全部对象移动到年老代中。
分配担保机制
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,可是没必要然执行
(2)老年代空间不足
(3)方法去空间不足
(4)经过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
4个特色:
4个步骤:
G1的GC模式
Young GC:选定全部年轻代里的Region。经过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
Mixed GC:选定全部年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽量选择收益高的老年代Region。
注意:Mixed GC不是full GC,它只能回收部分老年代的Region,若是mixed GC实在没法跟上程序分配内存的速度,致使老年代填满没法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。
global concurrent marking:相似CMS,为Mixed GC提供标记服务。
四个过程:
G1 中的几个重要概念 -- 原文连接--美团点评
1、Region
传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特色是各代的存储地址(逻辑地址,下同)是连续的。
以下图所示:
而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每一个Region占有一块连续的虚拟内存地址。以下图所示:
在上图中,咱们注意到还有一些Region标明了H,它表明Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有以下几个特征:
2、SATB
全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是经过Root Tracing获得的,做用是维持并发GC的正确性。
那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,咱们知道对象存在三种状态:
因为并发阶段的存在,Mutator(更改器和)Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的状况,这种状况发生的前提是:
对于第一个条件,在并发标记阶段,若是该白对象是new出来的,并无被灰对象持有,那么它会不会被漏标呢?Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。
对于在GC时已经存在的白对象,若是它是活着的,它必然会被另外一个对象引用,即条件二中的灰对象。若是灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而致使被回收掉,这是很是严重的错误,因此SATB破坏了第二个条件。
也就是说,一个对象的引用被替换时,能够经过 write barrier 将旧引用记录下来。(并无 看懂在说什么)
SATB也是有反作用的,若是被替换的白对象就是要被收集的垃圾,此次的标记会让它躲过GC,这就是float garbage。由于SATB的作法精度比较低,因此形成的float garbage也会比较多。
3、RSet
全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些相似。
还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了 GC要收集的Region集合 ,集合里的Region能够是任意年代的。
在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet便可。
Rset : 属于points-into结构(谁引用了个人对象)
Card Table : 则是一种points-out(我引用了谁的对象)的结构
G1的RSet是在Card Table的基础上实现的:每一个Region会记录下别的Region有指向本身的指针,并标记这些指针分别在哪些Card的范围内。
这个RSet实际上是一个Hash Table,Key -- 别的Region的起始地址,Value是一个集合 -- 里面的元素是Card Table的Index。
这里解释一下 :
上图有三个 Region 。红色表明 Rset , 灰色大方框表明 Card Table。
Region2 的 Rset2 中有两个 Region 的起始地址,分别指向 Region1 , Region3。 -- 表明 Region1 与 Region3 引用了个人对象。
Region1 的 Card Table 位置上,存在一个 对 Region2 的引用。 -- 表明 Region1 引用了 Region2 的对象。
Region3 同理。
做用:
在作YGC(Minor GC)的时候,只须要选定young generation region的RSet做为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。
而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描所有young generation region获得,这样也不用扫描所有old generation region。因此RSet的引入大大减小了GC的工做量。
4、Pause Prediction Model
G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.
G1 GC是一个响应时间优先的GC算法,它与CMS最大的不一样是,用户能够设定整个GC过程的指望停顿时间,参数'-XX:MaxGCPauseMillis'指定一个G1收集过程目标停顿时间,默认值200ms。
G1 经过这个模型统计计算出来的历史数据来预测本次收集须要选择的Region数量,从而尽可能知足用户设定的目标停顿时间。
停顿预测模型是以衰减标准误差为理论基础实现的。
这里就不详细介绍了,有兴趣的,能够看 美团大神的文章
若是虚拟机在扩展栈时没法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
经过 动态代理 或 经过 CGLIB 动态生成大量的类,以及大量 JSP与 动态JSP 文件的应用 。
一. 可经过命令按期抓取heap dump或者启动参数OOM时自动抓取heap dump文件。
二. 经过对比多个heap dump,以及heap dump的内容,分析代码找出内存占用最多的地方。
三. 分析占用的内存对象,是不是由于错误致使的内存未及时释放,或者数据过多致使的内存溢出。
启动(Bootstrap)类加载器:采用 C++ 实现,它负责将 <Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。因为启动类加载器到本地代码的实现,开发者没法直接获取到启动类加载器的引用,因此不容许直接经过引用进行操做。编写自定义类加载器时,若是须要把加载请求委派给启动类加载器,直接使用 null 代替.
扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者能够直接使用标准扩展类加载器。
系统(System)类加载器:系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者能够直接使用系统类加载器。
工做过程:
若是一个类加载器收到了类的加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器去完成。直到顶层的启动类加载器中,当父加载器反馈本身没法完成这个加载请求时,子加载器会尝试本身去加载。
方便 JNDI 服务:SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类通常是由系统类加载器来加载的。引导类加载器是没法找到 SPI 的实现类的,由于它只加载 Java 的核心库。它也不能代理给系统类加载器,由于它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式没法解决这个问题。
解决方法:Java 应用的线程的上下文类加载器 默认 就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就能够成功的加载到 SPI 实现的类。线程上下文类加载器在不少 SPI 的实现中都会用到。
Java默认的线程上下文类加载器是系统类加载器(AppClassLoader)。如下代码摘自sun.misc.Launch的无参构造函数Launch()。
能够经过 java.lang.Thread类 的 setContextClassLoader() 设置。
方便执部署的实现。能够在不重启服务器的状况下,对其中的逻辑代码进行更新。
由 父类加载器 与 Bundle 组成 , 每一个 Bundle 的功能都是 发布 export 与依赖 import。从而造成复杂的网状结构
原理:
OSGi 中的每一个模块都有对应的一个类加载器。它负责加载模块本身包含的 Java 包和类。
当它须要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(一般是启动类加载器)来完成。
当它须要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。
在双亲委派模型的基础上加入了 Common类加载器,Catalina类加载器,Shared类加载器,WebApp类加载器,Jsp类加载器
Common类加载器, /common 目录 被 Tomcat 与 因此 Web 应用程序共同使用
Catalina类加载器, /server 目录中, 被 Tomcat 使用
Shared类加载器, /shared 目录中 ,被全部 Web 应用程序共同使用
WebApp类加载器,Jsp类加载器 , /WebApp/WEB-INF 目录中,只能被此 Web 应用程序使用。
-version 命令,能够输出显示这三种模式
JDK1.7 中的 Server 模式虚拟机中被做为默认编译策略。
由于存在屡次执行的循环体,因此触发 OSR 编译,以整个方法 做为编译对象。
发生在方法执行过程当中,因此叫( On Stack Replacement ) 方法栈帧还在栈上,方法就被替换了。
热点代码的分类:
热点探测(Hot Spot Detection)
基于计数器 -- HotSpot 虚拟机中采用。
计数器分类:
方法调用计数器(Invocation Counter) :
**统计一段时间内**,方法被调用的次数,若是超过期间限度,则将这个方法的调用计数器减小一半,称为**衰减**
回边计数器(Back Edge Counter) : 统计一个方法中循环体被执行的次数 -- OSR 编译
在字节码中遇到控制流向后跳转的指令,称为回边。
hotspot中内嵌有2个JIT编译器,分别为Client Compiler,Server Compiler,但大多数状况下咱们称之为C1编译器和C2编译器。
client compiler,又称C1编译器,较为轻量,只作少许性能开销比较高的优化,它占用内存较少,适合于桌面交互式应用。
在寄存器分配策略上,JDK6之后采用的为线性扫描寄存器分配算法,其余方面的优化,主要有方法内联、去虚拟化、冗余消除等。
A、方法内联
多个方法调用,执行时要经历屡次参数传递,返回值传递及跳转等,C1采用方法内联,把调用到的方法的指令直接植入当前方法中。-XX:+PringInlining来查看方法内联信息,-XX:MaxInlineSize=35控制编译后文件大小。
B、去虚拟化
是指在装载class文件后,进行类层次的分析,若是发现类中的方法只提供一个实现类,那么对于调用了此方法的代码,也能够进行方法内联,从而提高执行的性能。
C、冗余消除
在编译时根据运行时情况进行代码折叠或消除。
Server compiler,称为C2编译器,较为重量,采用了大量传统编译优化的技巧来进行优化,占用内存相对多一些,适合服务器端的应用。和C1的不一样主要在于寄存器分配策略及优化范围.
寄存器分配策略上C2采用的为传统的图着色寄存器分配算法,因为C2会收集程序运行信息,所以其优化范围更多在于全局优化,不只仅是一个方块的优化。
收集的信息主要有:分支的跳转/不跳转的频率、某条指令上出现过的类型、是否出现过空值、是否出现过异常等。
逃逸分析(Escape Analysis) 是C2进行不少优化的基础,它根据运行状态来判断方法中的变量是否会被外部读取,如不会则认为此变量是不会逃逸的,那么在编译时会作标量替换、栈上分配和同步消除等优化。
若是证实一个对象不会逃逸到方法或线程以外,则:
- 栈上分配(Stack Allocation) :肯定不会逃逸到**方法外**,让这个对象在栈上分配内存,对象占用的内存空间能够随栈帧的出栈而销毁。 - 同步消除(Synchronization Elimination) :肯定不会逃逸到**线程外**,则没法被其余线程访问,因此能够取消同步措施。 - 标量替换(Scalar Repalcement) : 标量(Scalar)指一个数据没法再分解成更小的数据来表示 -- Java 中的原始数据类型 聚合量(Aggregate)指一个数据能够继续分解 -- Java 中的对象 **原理:**直接建立若干个能够被方法使用的成员变量来替代。
若是一个表达式E 已经计算过,而且从先前的计算 到如今 值不曾改变,那么若是 E 再次出现,则能够直接使用以前的表达式结果,代替 E 。
静态分派 : 依靠静态类型 定位方法。
编译阶段:Human man = new Man(); // 静态类型为 Human
运行阶段:man.sayHello() // 动态类型为 Man
重载的优先级
sayHello(char arg);
char -> int -> long -> float -> double // 不可转化为 byte short , 由于char 转化是不安全的。
-> Character -> Serializable/Comparable -> Object -> char...(变长参数)
宗量:方法的接收者与方法的参数统称为宗量
单分派 根据一个宗量对目标方法进行选择
多分派 根据多个宗量对目标方法进行选择
public class QQ{}; public class _360{}; public static class Father { public void hardChoice(QQ arg); public void hardChoice(_360 arg); } public static class Son extends Father{ public void hardChoice(QQ arg); public void hardChoice(_360 arg); } Father father = new Father(); Father son = new Son(); // 静态多分派 - 编译 : 方法的接收者 Father - Son, 参数 QQ - _360 father.hardChoice(_360); // 动态多分派 - 运行 : 已经肯定 参数为 QQ ,再判断 实际类型 , son的实际类型为 Son 。 son.hardChoice(QQ);
都看到这里了,点个关注,点波赞再走,QAQ。
你的小手轻点,是我最大的动力哦。
一只想当程序员的1米88处女座大可爱如此说道。