JVM学习笔记
复习之前学的内容,同时补充以下知识点:JVM的双亲委派机制、伊甸区与老年代相关知识;
双亲委派机制
双亲的含义应该就是AppClassLoader有:ExtClassLoader和BootstrapClassLoader“两个”父加载器。
首先介绍Java中的类加载器
Java中的类加载器
Bootstrap ClassLoader(启动类加载器),默认加载jdk\lib目录下jar中的诸多类。可以使用-Xbootclasspath指定。
Extension ClassLoader(扩展类加载器),默认加载jdk\lib\ext目录下的jar中的诸多类。可以使用java.ext.dirs系统变量更改。
Application ClassLoader(应用程序加载器),应用程序加载器,负责加载开发人员所编写的诸多类。
User ClassLoader(自定义加载器),自定义类加载器,当存在上述加载器解决不了的特殊情况时,或者存在特殊要求时,可以自行实现类加载逻辑。
关系图:
双亲委派
通俗故事:
- 假设用户刚刚摸鱼时写了个Test类想进行加载,此时会发送给应用程序类加载器AppClassLoader;
- 然后AppClassLoader并不会直接去加载Test类,而是会委派于父类ExtClassLoader,来完成此操作;
- ExtClassLoader同样不会直接加载Test类,而是会继续委派父类BootstrapClassLoader;
- BootstrapClassLoader已经是顶层了,没有更高的父类加载器了,因此BootstrapClassLoader就从jdk\lib中搜索是否存在,因为这里是用户自己写的Test类,因此不会存在于jdk下,所以此时会给子类一个反馈;
- ExtClassLoader收到父类传回的反馈,知道父类加载器没有找到对应的类,爸爸靠不住,就只能自己来加载了,结果显而易见,自己也不行,只能给下面的子类加载器,AppClassLoader;
- AppClassLoader收到父类加载器的反馈,顿时明白,原来爸爸虽然是爸爸,但是终究不能管儿子的私事,所以此时,AppClassLoader就自己尝试去加载。
- 结果,就这样成功了,走了一大圈,兜兜转转还是自己干。
什么是双亲委派
为什么使用双亲委派
专业性解释:①避免类的重复加载;②防止核心API被篡改;
为了避免原始类被覆盖的问题。
老子走过的路,小子不用走
比如,用户编写了一个Object类,放入程序中加载。
当没有双亲委派机制时,就会出现重复的Object类,给开发人员造成很大的困扰,本来就只需要基于JDK开发就好了,现在还得把JDK中的类全记住,避免编写重复的类。
当存在双亲委派机制时,整个事情就不一样了,每次加载类时,都会遵循双亲委派机制,去问父类是否可以加载,如果可以呢,那就不需要再次加载了,这样事情就简单了。
Tomcat为什么要自定义类加载器
如何打破双亲委派模型(太高级,尝试看一下)
为什么要考虑这个问题?
- 自定义类加载器时,重写loadClass方法。
双亲委派还有什么
运行时数据区
蓝色部分是多个线程共享部分;
绿色部分为单个线程独享部分;
方法区
;
解释器
JIT编译器
GC
为什么需要GC
垃圾是指JVM中没有任何引用指向的对象,如果不清理这些垃圾对象,那么他们就一直占用内存,而不能给其他对象使用,最终垃圾对象越来越多,就会出现OOM。
垃圾标记阶段
先找到垃圾对象。
引用计数法:
每个对象保存一个引用计数器属性,用户记录对象被引用的次数。
a)优点:实现简单,计数器为0就是垃圾对象;
b)缺点:
①无法解决循环引用问题;
②需要额外的空间记录;
③需要额外的时间维护应用计数。
可达性分析方法:
以GCRoots作为起始点,然后一层一层找到对应的对象,被找到的对象就是存活对象,那么其他对象就是不可达对象,即垃圾对象。
GCRoots包括:
- 线程中虚拟机栈中正在执行的方法中方法参数、局部变量所对应的对象引用;
- 线程中本地方法栈中正在执行的方法中方法参数、局部变量所对应的对象引用;
- 方法区保存的类对象的静态属性所对应的对象引用;
- 方法区保存的类对象的常量属性所对应的对象引用;
- 等等;
标记清除算法
STW,Stop The World;
- 标记阶段:
- 清除阶段:
a) 缺点:效率不高;产生内存碎片;
b) 优点:逻辑简单;
复制算法
Copying
将内存分为两块,每次只使用其中一块,进行GC时,将可达对象赋值到另外没有被使用的内存块中,然后再清楚当前内存块中的所有对象,内存块交替使用。
a) 缺点:耗费空间较大;可达对象多时,效率很低,因此适用于新生代,垃圾对象多的空间;对象内存之地变化之后,需要额外的时间修改对象的引用地址。
b) 优点:没有内存碎片;没有标记和清除阶段,直接复制操作,不需要修改对象头
;
标记-整理算法
Mark-Compact算法
第一阶段,从GCRoots找到并标记可达对象;
第二阶段,将所有存活对象移动到内存的一端;
最后清理边界外所有的空间;
a) 缺点:需要修改对象引用地址;适用于垃圾对象少、可达对象多;效率低 ,三种当中最低的;
b) 优点:没有内存碎片;不需要额外的内存空间;
分代收集算法
分代收集的理念
不同对象的存活时间不一样,因此可以针对不同的对象采取不同的垃圾回收算法。
- 新生代的对象存活时间比较短,那么就可以利用复制算法,它适合对象比较多的情况。
- 老年代的对象存活时间比较长,所以不适合用复制算法,可以用标记-清除或者标记-整理算法,比如:
a) CMS垃圾收集器采用的就是标记-清除算法;
b) Serial Old采用的就是标记-整理算法;
CMS垃圾收集器
整个垃圾收集过程变长了,但是STW时间变短了;
- 初始标记;STW
- 并发标记;
- 重新标记;STW,时间很短;
- 并发清理;
- 并发重置;
当出现新对象要进入老年代,但是空间不够时,会导致“concurrent mode failure”,此时可以利用Serial Old进行一次垃圾收集,就是做一次全局STW。
G1垃圾收集器
Garbage First
将整个内存分为一个个的方块,均分为2048块。
- 初始标记;STW;
- 并发标记;
- 最终标记;STW;
- 筛选回收;STW;可以通过-XX:MaxGCPauseMillis来制定GC的STW停顿的时间,所以可能并不会回收掉所有垃圾对象,默认200ms;采用复制算法,不会产生碎片(会把某个region对象复制到另外空闲的region中)
YoungGC:Eden区满了,就会触发G1的YoungGC,对Eden区进行GC
MixedGC:老年代占用率达到了-XX:InitiatingHeapOccupancyPercent指定的百分比,回收所有的新生代以及部分老年代,以及大对象区
FullGC:在进行MixedGC过程中,采用复制算法,如果复制过程内存不够,会触发FullGC,会STW,并采用单线程进行标记-整理算法进行GC,相当于一次Serial GC;
堆区
所有的对象和数组都应该存放在堆区,在执行字节码指令时,会把创建的对象存入堆中,对象对应的引用地址存入虚拟机栈中的栈帧中,不过当方法执行完之后,刚刚被创建的对象并不会被回收,而是要等JVM后台执行GC之后,对象才会被回收。
- Xms:设置堆的初始化内存大小,等价于-XX:InitialHeapSize;
- Xmx:设置堆的最大内存,等价于-XX:MaxHeapSize;
一般会把两个设置为一样,这样JVM就不需要再GC后去修改堆的内存大小了,提高了效率,默认,初始化内存大小=物理内存大小/64,最大内存大小=物理内存大小/4;
新生代
可以通过-XX:NewRatio参数来配置新生代和老年代的比例,默认是2,新生代占1,老年代占2,也就是新生代占堆区的1/3;
一般不需要调整,只有明确知道存活时间比较长的对象偏多,那么就需要调大NewRatio,从而调整老年代的占比;
- Eden:伊甸园区,新对象都存放在伊甸区(除非对象的大小超过了Eden区,那么就只能直接进入老年代)
- S0、S1区:Survivor0、Survivor1区,也可以叫做from区,to区,用来存放MinorGC(YGC)后的对象。
- 默认情况下(Eden区:S0区:S1区)的比例关系为(8:1:1),即Eden区占8/10;可以用-XX:SurvivorRatio调整。
YGC,Young Garbage Collection,新生代垃圾回收,将Eden区对象放入S0区;S0区和S1区交替使用;
老年代
当达到某个条件之后,剩余对象就会被保存到老年代中。
遇到第二个非常大的对象,Eden区原有一个大对象,内存不够用时,Eden区的大对象会被直接放到老年代(S0、S1区放不下)
或者来了一个超大对象,可以直接放进老年代;
GC分类
- Young GC/Minor GC:负责对新生代进行垃圾回收
- Old GC/Major GC:负责对老年代进行垃圾回收,目前只有CMS垃圾收集器会单独对老年代进行垃圾收集,其他垃圾收集器基本都是在整堆回收的时候对老年代进行垃圾收集
- Full GC:整堆回收,也会堆方法区进行垃圾收集。
程序计数器
记录下一条待执行指令的地址;
- 是物理寄存器的抽象实现;
- 用来记录下一条待执行指令的地址;
- 它是程序控制流的指示器,循环、if else、异常处理、线程恢复等都依赖它来完成;
- 解释器工作时就是通过它来获取下一条需要执行的字节码指令的;
- 是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。
Java方法栈
虚拟机栈是线程私有的,每个线程创建时都会创建一个虚拟机栈,栈内保存一个栈帧,一个栈帧就对应一个方法。
- 虚拟机栈是线程私有的
- 一个方法开始执行,栈帧入栈、方法执行完对应的栈帧就出栈,所以虚拟机栈不需要进行垃圾回收
- 虚拟机栈存在OutOfMemoryError,和StackOverFlowError;
- 线程太多,就会出现OutOfMemoryError,线程创建时没有足够的内存去创建虚拟机栈了;
- 方法调用层数过多,就会出现StackOverFlowError;
- 可以通过-Xss来设置虚拟机栈的大小。
栈帧:
操作数栈:也可以叫做操作栈,是栈帧的一部分,操作数栈是用来执行字节码指令过程中用来进行计算的。
操作数栈
比如加法过程,运算操作数在计算过程中的进栈出栈;
局部变量表
保存局部变量,比如加法过程中,其实每个变量的值都是保存在局部变量表中的。
方法返回地址
动态链接
本地方法栈
C/C++语言写的一些代码
存的是本地方法的栈帧,也是线程私有的,也会OOM和SOF。