知识回顾

145 阅读14分钟

Jvm知识回顾

1. jvm结构

  • 类加载机制 classloader :用来装载 .class 文件;
  • 执行引擎 :执行字节码,执行本地方法栈;
  • 运行区域 :方法区,虚拟机栈,堆,本地方法栈,程序计数器;
  • 运行原理 :Jvm将一个.class的文件加载到内存,并进行校验,加载,解析,初始化的过程,将其转换成虚拟机可以读取的java类型,这就是类的加载机制;

2. 类的加载机制(双亲委派机制)

  • Bootstrap classLoader (启动类加载器)

主要负责加载Java目录下的核心类,具体是负责加载<$JAVA_HOME>/ jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。

  • Extension classLoader(扩展类加载器)

负责加载java平台中扩展功能的一些jar包,包括<$JAVA_HOME>/jre/lib/ext/ .jar 或 -Djava.ext.dirs指定目录下的jar包。

  • App classLoader (应用程序加载器)

负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。它是java应用程序默认的类加载器。

  • Custom classLoader (用户自定义加载器)

通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

在虚拟机启动的时候会初始化BootstrapClassLoader,然后在Launcher类中去加载ExtClassLoader、AppClassLoader,并将AppClassLoader的parent设置为ExtClassLoader,并设置线程上下文类加载器。

p6-juejin.byteimg.com/tos-cn-i-k3…

  • 双亲委派机制

是指一个加载器接受到了一个类的加载请求,它首先会将请求给它的父级加载器,如果父级加载器加载不了,就交由自己来加载,所以所有请求都将执行到最上面bootstrap classLoader(启动类中),如上图所示。

3. 卸载(GC垃圾回收机制)

  • GC的基本原理 :将内存中不被使用的对象进行回收,GC中用于回收的方式称之为收集器,由于GC消耗的一些资源与时间,java在对象的生命周期进行分析后,将其分为 年轻态(新生态),老年态(旧生态)用于收集对象,以此来消除GC对应用所造成的损耗。
  • 哪些内存需要回收 :JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。
  • 年轻代(Young Generation)的回收算法 :在年轻代中jvm使用的是Mark-copy(标记-复制)算法

(a)所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。 (b)年轻代分三个区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当另外一个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。 (c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。 (d)新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

  • 老年代(Old Generation)的回收算法:老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact(标记-整理)算法。

(a)在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。 (b)内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC或Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

  • 永久代(Permanent Generation)的回收算法:永久代(permanent generation)也称为“方法区(method area)”,他存储class对象和字符串常量。所以这块内存区域绝对不是永久的存放从老年代存活下来的对象的。在这块内存中有可能发生垃圾回收。发生在这里垃圾回收也被称为major GC。
  • Minor GC、Full GC触发条件
Minor GC触发条件 :
  1. Eden区域满了,或者新创建的对象大小 > Eden所剩空间
  2. CMS设置了CMSScavengeBeforeRemark参数,这样在CMS的Remark之前会先做一次Minor GC来清理新生代,加速之后的Remark的速度。这样整体的stop-the-world时间反而短
  3. Full GC的时候会先触发Minor GC
Full GC触发条件 :
  1. Minor GC后存活的对象晋升到老年代时由于悲观策略的原因,有两种情况会触发Full GC, 一种是之前每次晋升的对象的平均大小 > 老年代剩余空间;一种是Minor GC后存活的对象超过了老年代剩余空间。这两种情况都是因为老年代会为新生代对象的晋升提供担保,而每次晋升的对象的大小是无法预测的,所以只能基于统计,一个是基于历史平均水平,一个是基于下一次可能要晋升的最大水平。这两种情况都是属于promotion failure
  2. CMS失败,发生concurrent mode failure会引起Full GC,这种情况下会使用Serial Old收集器,是单线程的,对GC的影响很大。concurrent mode failure产生的原因是老年代剩余的空间不够,导致了和gc线程并发执行的用户线程创建的大对象(由PretenureSizeThreshold控制
  3. 新生代直接晋升老年代的对象size阀值)不能进入到老年代,只要stop the world来暂停用户线程,执行GC清理。可以通过设置CMSInitiatingOccupancyFraction预留合适的CMS执行时剩余的空间新生代直接晋升到老年代的大对象超过了老年代的剩余空间,引发Full GC。注意于promotion failure的区别,promotion failure指的是Minor GC后发生的担保失败
  4. Perm永久代空间不足会触发Full GC,可以让CMS清理永久代的空间。设置CMSClassUnloadingEnabled即可
  5. System.gc()引起的Full GC,可以设置DisableExplicitGC来禁止调用System.gc引发Full GC
结论:
1. Full GC == Major GC指的是对老年代/永久代的stop the world的GC
2. Full GC的次数 = 老年代GC时 stop the world的次数
3. Full GC的时间 = 老年代GC时 stop the world的总时间
4. CMS 不等于Full GC,我们可以看到CMS分为多个阶段,只有stop the world的阶段被计算到了Full GC的次数和时间,而和业务线程并发的GC的次数和时间则不被认为是Full GC
5. Full GC本身不会先进行Minor GC,我们可以配置,让Full GC之前先进行一次Minor GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度。比如老年代使用CMS时,设置CMSScavengeBeforeRemark优化,让CMS remark之前先进行一次Minor GC。
  • Heap什么时候会发生OOM
1. 当花在GC的时间超过了GCTimeLimit,这个值默认是98%,
2. 当GC后的容量小于GCHeapFreeLimit,这个值默认是2%。
  • stop-the-world

不管选择哪种GC算法,stop-the-world都是不可避免的。Stop-the-world意味着从应用中停下来并进入到GC执行过程中去。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。GC调优通常就是为了改善stop-the-world的时间。

  • 降低GC的调优的策略

代大小优化最关键参数:-Xms、 -Xmx 、-Xmn 、-XX:SurvivorRatio、-XX:MaxTenuringThreshold、-XX:PermSize、-XX:MaxPermSize

  1. -Xms、 -Xmx 通常设置为相同的值,避免运行时要不断扩展JVM内存,这个值决定了JVM heap所能使用的最大内存。
  2. -Xmn 决定了新生代空间的大小,新生代Eden、S0、S1三个区域的比率可以通过-XX:SurvivorRatio来控制(假如值为 4  表示:Eden:S0:S1 = 4:3:3 )
  3. -XX:MaxTenuringThreshold 控制对象在经过多少次minor GC之后进入老年代,此参数只有在Serial 串行GC时有效。
  4. -XX:PermSize、-XX:MaxPermSize 用来控制方法区的大小,通常设置为相同的值。
  1. 避免新生代大小设置过小当新生代设置过小时,会产生两种比较明显的现象,一是minor GC次数频繁,二是可能导致 minor GC对象直接进入老年代。当老年代内存不足时,会触发Full GC。
  2. 避免新生代大小设置过大新生代设置过大,会带来两个问题:一是老年代变小,可能导致Full  GC频繁执行;二是 minor GC 执行回收的时间大幅度增加。
  3. 避免Survivor区过大或过小-XX:SurvivorRatio参数的值越大,就意味着Eden区域变大,minor GC次数会降低,但两块Survivor区域变小,如果超过Survivor区域内存大小的对象在minor GC后仍没被回收,则会直接进入老年代,-XX:SurvivorRatio参数值设置过小,就意味着Eden区域变小,minor GC触发次数会增加,Survivor区域变大,意味着可以存储更多在minor GC后任存活的对象,避免其进入老年代。
  4. 合理设置对象在新生代存活的周期新生代存活周期的值决定了新生代对象在经过多少次Minor GC后进入老年代。因此这个值要根据自己的应用来调优,Jvm参数上这个值对应的为-XX:MaxTenuringThreshold,默认值为15次。
减少GC开销的措施 

(1) 不要显式调用System.gc()。此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。大大的影响系统性能。    (2)尽量减少临时对象的使用。临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。     (3)对象不用时最好显式置为Null。一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。     (4)尽量使用StringBuffer,而不用String来累加字符串。由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。     (5)能用基本类型如Int,Long,就不用Integer,Long对象。基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。     (6)尽量少用静态对象变量。静态变量属于全局变量,不会被GC回收,它们会一直占用内存。     (7)分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。 内存溢出 out of memory :指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。 

二者的关系
  1. 内存泄漏的堆积最终会导致内存溢出
  2. 内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
  3. 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。
  4. 内存溢出:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错。
内存溢出的原因及解决方法:

1. 内存溢出原因: 

1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;  2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;  3.代码中存在死循环或循环产生过多重复的对象实体;  4.使用的第三方软件中的BUG;  5.启动参数内存值设定的过小

2. 内存溢出的解决方案: 

第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。) 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。