1. jvm运行时区域划分及每个区域的作用
堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器
2. 堆内存分配策略:新生代,老年代,gc时机
• 对象优先分配在Eden区,如果Eden区没有足够的空间进行分配时,虚拟机执行一次MinorGC。而那些无需回收的存活对象,将会进到 Survivor 的 From 区(From 区内存不足时,直接进入 Old 区)。
• 大对象直接进入老年代(需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
• 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄(Age Count)计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值(默认15次),对象进入老年区。
• (动态对象年龄判定:程序从年龄最小的对象开始累加,如果累加的对象大小,大于幸存区的一半,则将当前的对象 age 作为新的阈值,年龄大于此阈值的对象则直接进入老年代)
• 每次进行Minor GC或者大对象直接进入老年区时,JVM会计算所需空间大小如小于老年区的剩余值大小,则进行一次Full GC。
3. 创建一个对象的步骤
类加载检查 —> 分配内存 —> 初始化零值 —> 设置对象头 —> 执行init方法.
①类加载检查:
虚拟机遇到 new 指令时,首先去检查是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类
是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
②分配内存:
在类加载检查通过后,接下来虚拟机将为新生对象分配内存,分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择
那种分配方式由Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
③初始化零值:
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这一步操作保证了对象的实例字段在 Java 代
码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
④设置对象头:
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据
信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态
的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
⑤执行 init 方法:
从虚拟机的视⻆来看,一个新的对象已经产生了,但从Java 程序的视⻆来看, 方法还没有执行,所有的字段都还为
零。所以一般来说(除循环依赖),执行 new 指令之后会接着执行 方法,这样一个真正可用的对象才算产生出来。
4. 对象引用:强引用、软引用、弱引用、虚引用(了解)
5. jvm类加载过程
①加载阶段
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。
②验证阶段
1.文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理)
2.元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求)
3.字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为)
4.符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生)
③准备阶段
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。将对象初始化为“零”值
④解析阶段
解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。
字符串常量池:堆上,默认class文件的静态常量池
v运行时常量池:在方法区,属于元空间
⑤初始化阶段
初始化阶段时加载过程的最后一步,而这一阶段也是真正意义上开始执行类中定义的Java程序代码。
6. 双亲委派机制
每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
为什么使用双亲委派机制?:此机制保证JDK核心类的优先加载;使得Java程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API不被篡改。如果不用没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的Object 类。
7. 破坏双亲委派机制:tomcat的类加载机制(了解)
WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类。
步骤:
- 先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。
- 如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。
- 如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,重点来了,这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制,直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然有效,ExtClassLoader 就会使用 Bootstrap ClassLoader来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,这个加载链,就保证了 Object 不会被重复加
载。- 如果BoostrapClassLoader,没有加载成功,就会调用自己的findClass方法由自己来对类进行加载,findClass
加载类的地址是自己本 web 应用下的 class。- 加载依然失败,才使用 AppClassLoader 继续加载。
- 都没有加载成功的话,抛出异常。
8. Jvm垃圾回收:存活算法
引用计数法
给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,判定效率也很高
缺点:他很难解决对象之间相互循环引用的问题,基本上被抛弃
可达性分析法
通过一系列的成为“GC Roots”(活动线程相关的各种引用,虚拟机栈帧引用,静态变量引用,JNI引用)的对象作为起始点,从这些节点ReferenceChains开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的;
两次标记过程
对象被回收之前,该对象的finalize()方法会被调用;两次标记,即第一次标记不在“关系网”中的对象。第二次的话
就要先判断该对象有没有实现finalize()方法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个
9. Jvm垃圾回收:回收算法
垃圾回收算法:复制算法、标记清除、标记整理、分代收集
复制算法:(young)
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收;
优点:实现简单,内存效率高,不易产生碎片
缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低
标记清除:(cms)
标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
缺点:效率低,标记清除后会产生大量不连续的碎片,需要预留空间给分配阶段的浮动垃圾
标记整理:(old)
标记过程仍然与“标记-清除”算法一样,再让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存;解
决了产生大量不连续碎片问题
分代收集
根据各个年代的特点选择合适的垃圾收集算法。
新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
10 Jvm垃圾回收:垃圾收集器
Serial
Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集
的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。适合用于客户端垃圾收集器。
Parnew
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余
的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
JDK5:parallel Scavenge+(Serial old/parallel old)关注吞吐量
parallel Scavenge:(关注吞吐量)
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验);高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
Serial old
Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。主要有两个用途:在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
作为年老代中使用 CMS 收集器的后备垃圾收集方案。
parallel old
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。
JDK8-CMS:(关注最短垃圾回收停顿时间)
CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,STW。
并发标记:进行 ReferenceChains跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,STW。
并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
优点:并发收集、低停顿
缺点:对CPU资源敏感;无法处理浮动垃圾;使用“标记清除”算法,会导致大量空间碎片产生。
JDK9-G1:(精准控制停顿时间,避免垃圾碎片
是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器.以极高概率满足GC停顿时间要求的
同时,还具备高吞吐量性能特征;相比与 CMS 收集器,G1 收集器两个最突出的改进是:
【1】基于标记-整理算法,不产生内存碎片。
【2】可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
初始标记:Stop The World,仅使用一条初始标记线程对GC Roots关联的对象进行标记
并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢
最终标记:Stop The World,使用多条标记线程并发执行
筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行
JDK11-ZGC:(在不关注容量的情况获取最小停顿时间5TB/10ms)着色笔技术:加快标记过程
读屏障:解决GC和应用之间并发导致的STW问题
• 支持 TB 级堆内存(最大 4T, JDK13 最大16TB)
• 最大 GC 停顿 10ms
• 对吞吐量影响最大,不超过 15%
9. jvm性能调优:一些常见问题、常用工具、命令
常用命令:jps、jinfo、jstat、jstack、jmap
jps:查看java进程及相关信息
jinfo:查看JVM参数
jstat:查看JVM运行时的状态信息,包括内存状态、垃圾回收
jstack:查看JVM线程快照,jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环
jmap:可以用来查看内存信息 (配合jhat使用)