JVM(垃圾回收)CMS G1

关于JVM

  • java内存模型
  • 垃圾收集器
  • 双亲委派模型。 类加载器 -->类加载器可以实现热部署 

深入理解Java类加载器(ClassLoader) https://blog.csdn.net/javazejian/article/details/73413292

优秀参考文章  《深入理解Java虚拟机》https://blog.csdn.net/zhang_jiayuan/article/details/82083163#%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8

###java内存模型

堆: 存储对象 新生代,老年代及永久代(存放类和元数据Meta)-->java8已经移除 因为会产生OOM异常 被"元空间"取代(它使用的是本地内存)

java栈:每个方法执行时创建栈帧(局部变量表<编译时期的基本数据类型,对象引用等>,操作数栈,动态链接,返回地址) 动态存储(数据目标在运行时刻内存分配是暂时的,存储的都是局部变量,临时变量,对象引用) 也就是OS在建立线程时为这个线程建立的存储区域,该区域有先进后出的特性 栈是暂时的 数据具有生命周期 属于临时存储 栈中共享数据与两个对象引用指向一个对象不同,他是由编译器完成 利于节省空间

程序计数器:

本地方法栈:

方法区:存储JVM加载的类信息,常量,静态变量(包括静态成员变量)static/final,运行时常量池,即时编译器编译后的代码等数据-->堆的逻辑部分

###堆栈分别存储的数据类型

1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制;(CPU存放计算数据的地方)

1. 栈:存放基本类型(会包含这个基本类型的具体值) 变量数据对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(字符串常量对象存放的常量池中),局部变量【注意:(方法中的局部变量使用final修饰后,放在堆中,而不是栈中)】

2.堆:存放使用new创建的对象(对象本身)及数组,全局变量,非静态成员变量(锁和同步,垃圾回收机制)

https://blog.csdn.net/lin542405822/article/details/80338256

堆区:   堆这块区域是JVM中最大的,应用的对象和数据都是存在这个区域,这块区域也是线程共享的,也是 gc 主要的回收区,一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:新生代、年老代、永久代
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令) 
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身 


栈区:   栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。基本类型的变量和对象的引用变量都是在函数的栈内存中分配。 栈中的数据都是以栈帧(Stack Frame)的格式存在栈帧是一个内存区块是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进......F3栈帧,再弹出F2栈帧,再弹出F1栈帧。
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中 
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。 
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

JVM(垃圾回收)CMS G1_第1张图片

第三行 创建了两个对象 一个常量池存储的 String对象 一个是存在堆中的String("xyz")对象  以及一个引用 在栈中c引用

我们先来记住两条黄金法则:

1.引用类型总是被分配到“堆”上。

2.值类型总是分配到它声明的地方:

   a.作为引用类型的成员变量分配到“堆”上

   b.作为方法的局部变量时分配到“栈”上

参考理解(含代码例子) https://www.cnblogs.com/Mind-Hacker/archive/2013/07/24/3210223.html

gc是针对堆进行垃圾回收的,栈是线程创建的时候就固定了的,它跟线程是绑定的。  对象 ,类信息  ,常量等都是保存在堆中。gc有很多中,但1.7以后基本上都是用的G1

### JVM垃圾回收机制

参考: 扒一扒JVM的垃圾回收机制,下次面试你准备好了吗

-----------------------------

JVM(垃圾回收)CMS G1_第2张图片应该还有B选项

  • 串行:单个线程执行垃圾回收,并且此时用户线程仍然处于等待状态。
  • 并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

参考 JVM性能调优

还有一个问题是,垃圾回收动作何时执行?

  • 年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
  • 年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代
  • 永久代满时也会引发Full GC,会导致Class、Method元信息的卸载

  另一个问题是,何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出

  • JVM98%的时间都花费在内存回收
  • 每次回收的内存小于2%

  满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。

除直接调用System.gc外,触发Full GC执行的情况有如下四种。
1. 老生代空间不足
老生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space 
为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
2. Permanet Generation(永久代)空间满
PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space .为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
3. CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
4. 统计得到的Minor GC晋升到老生代的平均大小大于老生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

###JVM CMS工作步骤 优点

大部分的GC采用了分代收集算法

三种新生代回收器

JVM(垃圾回收)CMS G1_第3张图片

三种老生代回收器

JVM(垃圾回收)CMS G1_第4张图片

JVM(垃圾回收)CMS G1_第5张图片

CMS收集器

目标为获取最短停顿时间 (良好响应速度) 标记清除算法  优点为并发标记和低停顿

步骤: 初始标记(Stop the World) 并发标记 重新标记(Stop the World) 并发清除

总体来说,CMS收集器的内存回收过程就是与用户线程一起并发执行的

缺点:浮动垃圾无法处理 可能会concurrent mode failure 导致另一次 full GC产生;  产生空间碎片 ; CPU资源敏感 默认启动线程数为

(CPU数+3)/4 因此 CPU不足4时 影响很大

 

G1收集器

G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  1. 并行与并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间
  2. 分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆。
  3. 空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。(整体看是"标记整理",局部看是"标记复制" 意味着不会产生内存空间碎片)
  4. 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,G1除了追求低停顿外,还能建立可预测的停顿时间模型, 能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,[横跨整个堆内存]它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合(通过rememberset来避免全表扫描)。

虽然G1看起来有很多优点,实际上CMS还是主流。

详细补充 参考 JVM中的G1垃圾回收器

JVM(垃圾回收)CMS G1_第6张图片

步骤:

初始标记(GCroots直接关联到的对象(Stop the World) ) 并发标记(可达性分析) 最终标记(修正程序运行时变动部分) 筛选回收(排序 根据停顿时间制定回收计划)

 

 

与GC相关的常用参数

除了上面提及的一些参数,下面补充一些和GC相关的常用参数:

  • -Xmx: 设置堆内存的最大值。
  • -Xms: 设置堆内存的初始值。
  • -Xmn: 设置新生代的大小。
  • -Xss: 设置的大小(单线程)。
  • -PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。
  • -MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。
  • -UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。
  • -SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。
  • -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。
  • -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。
  • -XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。

参考 http://www.importnew.com/26383.html

###双亲委派模型 

*首先了解类加载机制  https://blog.csdn.net/qq_38182963/article/details/78660779

Java虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,(准备)转换解析初始化最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类加载器的任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM中,
然后转换为一个与目标类对应的java.lang.Class对象实例。

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这动作的代码模块成为“类加载器”。

类和类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。这句话可以表达的更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这个两个类就必定不相等。
 

  1. Bootstrap ClassLoader:根类加载器,负责加载java的核心类(将存放在 JAVA_HOME/lib 目录中的,或者被-Xbootclasspath 参数所指定的路径中的), 这个类加载器使用C++语言实现,是虚拟机自身的一部分,它不是java.lang.ClassLoader的子类,而是由JVM自身实现;并且启动类加载器无法被Java程序直接使用。其它所有的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。
  2. Extension ClassLoader:扩展类加载器,扩展类加载器的加载路径是JDK目录下jre/lib/ext,或者被java.ext.dirs 系统变量所指定的路径种的所有类库。扩展类的getParent()方法返回null,实际上扩展类加载器的父类加载器是根加载器,只是根加载器并不是Java实现的;开发者可以直接使用扩展类加载器。
  3. System ClassLoader:系统(应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性或CLASSPATH环境变量所指定的jar包和类路径。程序可以通过getSystemClassLoader()来获取系统类加载器;它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

JVM(垃圾回收)CMS G1_第7张图片

先看父类类加载器是否有相应类加载器 由上到下查询   双亲委派方式的作用:保证JDK核心类的优先加载;(保证类的唯一)

所有的代码都在java.lang.ClassLoader中的loadClass方法之中

逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass方法, 如父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass方法进行加载。

如何打破双亲委派模型? 

  1. 自定义类加载器,重写loadClass方法;
  2. 使用线程上下文类加载器;

参考2 : https://www.cnblogs.com/wxd0108/p/6681618.html

1.第一次破坏

由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法唯一逻辑就是去调用自己的loadClass()。

2.第二次破坏

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美。

如果基础类又要调用回用户的代码,那该么办?

一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,
它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

3.第三次破坏

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。

*Tomcat 加载器为何违背双亲委派模型?

如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。我们再看第二个问题,我们想我们要怎么实现jsp文件的热修改(楼主起的名字),jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

JVM(垃圾回收)CMS G1_第8张图片

CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*、/server/*、/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

*如果tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类

可以使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。

 

你可能感兴趣的:(JVM(垃圾回收)CMS G1)