前言
- 你真的研究过JVM吗?JVM的这些知识你深入了解过吗?
问题:请你谈谈你认识几种 JVM?(3种)
- SUN 公司 HotSpot (默认,掌握这个jvm即可)
- BEA 公司 JRockit
- IBM 公司 J9VM
问题:jvm除了Service模式是否有Client模式?他们有什么区别?如何修改?(了解即可)
Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升(64位系统默认)
Client模式启动时,启动较快,占用内存少,针对客户端进行优化(32位系统默认,一般用不到)
在路径JAVA_HOME/jre/lib/amd64/jvm.cfg 修改-server和-client的配置即可完成更改。
问题:jvm有几种mode?如何修改?(了解即可)
- mixed mode(默认):混合模式执行,编译+执行。
-Xmixed
- interpreted mode :解释模式执行。
-Xint
- compiled mode :编译模式执行。
-Xcomp
JVM的位置
- 是java程序跨平台,不是jvm跨平台,jvm将java代码编译成不同操作系统能执行的.class字节码。
JVM体系架构
ClassLoader
ClassLoader的层级
- 官方提供了3种ClassLoader
BootStrap ClassLoader
称为启动类加载器,处于加载器的最顶层,负责加载rt.jar、resources.jar、charsets.jar等,由于是C写的所以java程序触及不到。
public class JvmDemo { |
Ext ClassLoader
称为扩展类加载器,父类是BootStrap ClassLoader,负责加载Java的扩展类库,Java 虚拟机会提供一个扩展库目录,该类加载器在此目录里面查找并加载 Java 类。默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。
public class JvmDemo { |
App ClassLoader
称为应用程序加载器,父类是Ext ClassLoader,负责加载我们自定义的类,是我们接触最多的加载器,加载classpath下面的类。
public class JvmDemo { |
双亲委派机制
双亲委派机制 可以保护java的核心类不会被自己定义的类所替代
一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推,直到某个类加载器覆盖到了为止。
类加载的过程
- 当一个类被第一次加载的时候,jvm会通过类加载器将.class文件载入内存当中(方法区),并将这个类创建一个全局唯一的Class对象作为这个类对象的模板。
加载.class文件
连接
- 验证:验证.class文件是否正确
- 准备:给静态变量分配内存空间并赋予初始值(基本数据类型:0、false,引用类型:null)
- 解析:将类中的符号引用替换为为真实引用
在把java编译为class文件的时候,虚拟机并不知道所引用的地址;助记符:符号引用!
转为真正的直接引用,找到对应的直接地址!静态变量赋予正确的值
静态块
当类被加载时,会执行的代码块:static{},从基类开始从上至下加载。
package vc.coding.jvm; |
静态常量
当一个常量的值如果编译期间可以确定的,那这个值就会被加载到调用者类的常量池中!
package vc.coding.jvm; |
Native方法
- JNI : Java Native Interface (Java 本地方法接口)
- 凡是带native关键字的方法都是本地方法
- java触及不到的功能,就会调用C或C++的本地方法库,比如说java不能创建线程:
- 1995年,java 还未立足所以必须可以去调用 c、c++的库,所以说Java就在内存中专门开辟了一块区域标记为 native 方法。
public class Test { |
程序计数器
每个线程都有一个程序计数器,是线程私有的。
程序计数器就是一块十分小的内存空间;几乎可以不计
作用: 当前字节码执行的行号指示器
分支、循环、跳转、异常处理!都需要依赖于程序计数器来完成!
bipush
将 int、float、String、常量值推送值栈顶
istore
将一个数值从操作数栈存储到局部变量表
iadd
加
imul
乘
栈 Stack
什么是栈?
对比队列来说
队列是吃多了拉 = 先进先出 = FIFO(First Input First OutPut)
栈是喝多了吐 = 后进先出 = LIFO (Last Input First OutPut)
- 栈是线程私有的(每个线程都有自己的栈)
栈是掌管程序运行的,毎执行一个命令就是往栈中压一层,直到最后上一层执行完毕之后就弹出,栈空了线程就Over了。
存储一些基本类型的值、对象的引用(引用指向堆内存)、方法等…
问:为什么java的方法传递是值传递而不是引用传递?
答:因为实参传递到方法形参的过程,就是把栈中对象的引用值(也可以理解为堆的地址)传递过去,形参同样会开辟栈空间存储这个引用。
- 栈存取速度比堆快!仅次于cpu寄存器。
StackOverFlow
public class Demo01 { |
- 栈是一定不会存在垃圾回收的问题的,只要线程一旦结束,栈就Over了,与线程的生命周期一致。
Stack 原理
- java栈的组成元素 :
栈帧
- 每个栈帧存储着父、子栈帧的引用、方法引用、参数等…
方法区
因为物理上不在堆中而逻辑上可以归为堆中(下面👇堆会提到),所以又称:非堆区(Non-Heap space)
Method Area 方法区 是 Java虚拟机规范中定义的运行数据区域之一,和堆(heap)一样可以在线程之间共享!
JDK1.7之前
永久代:用于存储一些虚拟机加载类信息,常量,字符串、静态变量等等。。这些东西都会放到永久代中;
永久代大小空间是有限的:如果满了 OutOfMemoryError:PermGen
JDK1.8之后
HotSpot jvm 中彻底将永久代移除,新增了 Meta Spaces (元空间)
元空间就是方法区在 HotSpot jvm 的实现;
方法区主要就是来存:类信息,常量,字符串、静态变量、符号引用、方法代码。。。
元空间和永久代最大的区别:元空间并不在Java虚拟机中,使用的是本地内存!
调整元空间大小:-XX:MetasapceSize10m
堆 Heap
什么是堆?
一个JVM实例只有一个堆,可供所有线程共享数据,堆的大小是可以调节的。
可以存储的内容:对象实例、类、方法、常量。
分为三个部分:
- 新生区 Young (Eden-from-to)
- 养老区 Old Tenure
- 方法区(1.7之前:永久区 Perm,之后:元空间 Meta Space)(逻辑上)
GC垃圾回收主要发生在新生区与养老区中,又分为普通GC和Full GC,如果堆满了,则会报出OOM异常(OutOfMemoryError)。
调整堆的大小
-Xmx:最大堆大小 (默认为系统内存的1/4)
-Xms:初始堆大小 (默认为系统内存的1/64)
-Xmn: 年轻代大小 (默认为堆的1/3)
新生区
新生区是一个对象诞生、成长、消亡的地方。
新生区的细分:Eden、s0与s1区(from to),所有的对象Eden被new出来,慢慢当Eden满了,程序还需要创建对象的时候,会触发一次轻量级的GC,清理完后将活下来的对象放入幸存者区(也就是s0与s1),毎执行一次轻GC就会将活着的对象在s0与s1中进行复制、交换、销毁(下面👇GC会详细解释)。
- 99%的对象在新生区都是临时对象,在这里创建也在这里消亡。
养老区
新生区中清理了很多次之后,如果有一些特别顽强的对象经历了15次垃圾回收(默认也是最大值15,可调参)!则送入养老区!
如果运行了很久之后,养老区也满了,就会触发Full GC。
- 如果养老区都清理不动了的话,则会出现OOM异常。
方法区(永久区、元空间)
存放类信息、方法、常量。
几乎不会被垃圾回收。
常量回收条件:当没有对象引用一个常量的时候,该常量即可以被回收。
对象回收条件:1.该类的实例都被回收。 2.加载该类的classLoader已经被回收 3.该类不能通过反射访问到其方法,而且该类的java.lang.class没有被引用 当满足这3个条件时,可以被回收。
方法区和堆一样,是共享的区域,是JVM 规范中的一个逻辑的部分,别名:非堆。
JDK1.6之前:有永久代、常量池在方法区;
JDK1.7:有永久代、但是尝试去永久代,常量池在堆中;
JDK1.8之后:永久代没有了,取而代之的是元空间:常量池在元空间中;
标配参数、-X、-XX参数
- JVM只有三种参数类型:标配参数、X参数、XX参数。
标配参数
以
-
开头的为标配参数在各种版本之间都很稳定,很少有变化
X参数
- 以
-X
开头的为标配参数
XX参数
- 以
-XX
开头的为标配参数 - 公式:
- Boolean类型
-XX:+
开头代表开启某个功能,例如:-XX:+PrintGCDetails。-XX:-
开头代表关闭某个功能,例如:-XX:-PrintGCDetails。
- Key=Value类型
-XX:
key=value,例如:-XX:InitialHeapSize=268435456。
- Boolean类型
常用JVM调优参数整理
标配参数
参数 | 描述 |
---|---|
-verbose:class | 输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断。 |
-verbose:gc | 输出每次GC的相关情况。 |
-verbose:jni | 输出native方法调用的相关情况,一般用于诊断jni调用错误信息。 |
-X参数
参数 | 描述 |
---|---|
-Xint | 解释执行 |
-XComp | 第一次使用就编译成本地的代码 |
-Xmixed | 混合模式(Java默认) |
-Xprof | 跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。 |
-XX参数
参数 | 描述 |
---|---|
-Xmx8m | 最大堆内存8m (-XX:InitialHeapSize=8m缩写) |
-Xms8m | 初始堆内存8m (-XX:MaxHeapSize=8m缩写) |
-Xmn200m | 设置年轻代大小为200M。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。(缩写语法糖) |
-Xss128k | 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。(缩写语法糖) |
-XX:+PrintGCDetails | 打印GC详情 |
-XX:+HeapDumpOnOutOfMemoryError | dump 内存溢出异常 |
-XX:MetaspaceSize=10m | 元空间大小=10m |
-XX:MaxMetaspaceSize=10m | 最大元空间大小=10m |
-XX:+TraceClassLoading | 跟踪类加载 |
-XX:MaxTenuringThreshold = 15 | 进入老年区的存活阈值 |
-XX:NewRatio=2 | 设置年轻代与老年代的占比:NewRatio = 2 新生代1,老年代是2,默认新生代整个堆的 1/3;NewRatio = 4 新生代1,老年代+是4,默认新生代整个堆的 1/5; |
Java程序监控、Dump内存快照
-XX:+HeapDumpOnOutOfMemoryError
可以在内存溢出时,dump内存快照-XX:HeapDumpPath=目录+产生的时间.hprof
设置内存快照生成地址- 在java程序运行的时候,想测试运行的情况!可以使用一些工具来查看:
Jconsole
jps
命令:查看当前运行java进程jconsole
进程号:查看java进程当前运行状况:
IDEA(Jprofiler插件)
Jprofiler是一个强大的java程序监控软件,功能十分丰富!
Jprofiler官网下载安装:https://www.ej-technologies.com/products/jprofiler/overview.html
在IDEA 中绑定 JProfiler,则可在idea以Jprofiler监控的方式启动:
Arthas
- 我的另一篇博客有详细解释:Arthas - java服务监控神器
GC
GC就是java中的垃圾回机制,一直在追求更快且高效的道路中。
随着应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化
GC的作用域
- 新生代:GC频繁区域!
- 老年代:GC次数较少!
- 方法区:GC次数极少!
一个对象的诞生、成长、消亡
- GC分为两个类型:
- 普通垃圾回收:只针对新生代!「
GC
」 - 完全垃圾回收:主要针对老年代,偶尔伴随新生代或方法区!「
Full GC
」
- 普通垃圾回收:只针对新生代!「
GC四大算法
复制算法
- 新生代中就是使用复制算法!
- 一般普通GC 之后,差不多Eden几乎都是空的了!
- 每次存活的对象,都会被从 from 区和 Eden区复制到 to区,from 和 to 会发生一次交换;记住一个点就好,谁空谁是to,每当幸存一次,就会导致这个对象的年龄+1;如果这个年龄值大于15(默认也是最大值15,可调参),就会进入养老区!
- 优点:没有标记的过程、只用扫描一次,效率高!没有内存碎片!
- 缺点:需要浪费一倍的空间
标记清除算法
- 老年代一般用的算法,并且和后面的压缩算法一起使用!
优点:不需要额外的空间!
缺点:两次扫描、耗时比较严重,并且会产生内存碎片,内存不连续!(如果再来存东西会多判断这个地址是否存的下)
标记清除压缩算法
- 就是在标记清除算法后面加了一次压缩,去内存碎片!
优点:不需要额外的空间!无内存碎片!
缺点:耗时同样比较严重!
在我们这个要使用算法的空间中,假设这个空间中对象较少,不经常发生GC,那么可以考虑使用这个算法!
小总结
内存效率:复制算法 > 标记清除算法 > 标记压清除缩算法(时间复杂度)
内存整齐度:复制算法=标记清除压缩算法>标记清除算法
内存利用率:标记清除压缩算法 = 标记清除算法 > 复制算法
效率上来说复制算法最好,但是浪费空间较多!为了兼顾所有指标,标记清除压缩算法比较平滑,但是也不太完美!
难道没有一种最优的算法吗?
答案:没有!没有最好的只有最合适的!—–>分代收集算法:不同的区域使用不同的算法!
分代收集算法
新生代:相对于老年代,存活率较低、GC次数较多、99%的对象在这里被清理、区域小!推荐使用:复制算法!
老年代:相对于新生代,存活率高、GC次数较少、区域比较大! 推荐使用:标记清除压缩算法!
JVM 如何确定垃圾?
什么是垃圾:简单来说,就是不再被引用的对象!
Person person = new Person(); |
引用计数法(了解即可)
- JVM一般不采用这种方式,现在一般使用可达性算法,GC Root。
- 引用计数法就是每个对象有一个引用计数器,被几个对象所引用count就是几,如果为0,则这个对象可以被清理。
如此可知每次发生GC时,上图A对象count=0,所以会被清理,而其他对象则不会被清理,这里有一个严重的问题,比如C与D对象是互相引用的关系,这两个对象可能永远无法被清理。
- 缺点:
- 引用计数器维护麻烦!
- 循环引用无法解决!
可达性算法
- 一切都是从 GC Root 这个对象开始遍历的,只要在GC Root的引用链中就不是垃圾!
什么是GC Root(4种)
虚拟机栈中引用的对象。
在类中静态属性引用的对象。
在方法区中的常量。
本地方法栈中Native方法引用的对象(JNI了解即可)。
public class GCRoots{
// private static GCRoots2 t2; // GC root;
// private static final GCRoots3 t3 = new GCRoots3(); // GC root;
public static void m1(){
GCRoots g1 = new GCRoots(); //GC root;
System.gc();
}
public static void main(String[] args){
m1();
}
}
不同的垃圾收集器
- 垃圾收集器工作方式分为四大类:串行、并发、并行、G1。
STW:Stop the World:代表JVM准备GC时,所有的线程都停止!
串行收集器
- 单线程收集,会STW
并发收集器
- 多线程工作,也会STW
- 并发:一个cpu同时处理多个任务
并行收集器
- 在回收垃圾的同时,其它线程可以执行,并行处理,但是如果是单核CPU,只能交替执行!
- 并行:多个cpu同时处理多个任务
G1收集器 (待深入研究)
- G1(Garbage-First)收集器,面向服务器端的应用的收集器;
- 将堆内存分割成不同的区域,然后并发的对其进行垃圾回收。
-XX:MaxGCPauseMillis=100
调整最大的GC停顿时间单位:毫秒,JVM尽可能的保证停顿小于这个时间!
G1的优点:
- 没有内存碎片。
- 可以精准控制垃圾回收时间。
七种垃圾收集器
JDK7/8中,HotSpot虚拟机所有收集器及组合(连线),如下图:
图中一共有7中收集器
Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。
它们每个收集器都有自己负责的区域:
- 新生代收集器:Serial、ParNew(串行的多线程版本)、Parallel Scavengez
- 老年代收集器:Serial Old、Parallel Old、CMS
- 整堆收集器:G1
哪个是默认的垃圾收集器?
- 垃圾收集器一直在更新换代,到了jdk8默认的是Parallel Scavenge + Serial Old,而到了jdk9则默认为G1收集器了。
查看默认的垃圾收集器
java -XX:+PrintCommandLineFlags -version |
如何选择/设置垃圾回收器
- 设置了任何一个新生代收集器默认会匹配一个对应的老年代收集器
单CPU,单机程序,内存小。
-XX:+UseSerialGC
多CPU,大吞吐量,后台计算!
-XX:+UseParNewGC
多CPU,但是不希望有时间停顿,快速响应!
XX:+UseParallelGC
、-XX:+UseG1GC
摘自 《深入理解 Java 虚拟机》第二版 P90
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用 Serial + Serial Old 的收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收 |
UseConcMarkSweepGC | 使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在 Server 模式下的默认值, 使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收 |
UseParallelOleGC | 使用 Parallel Scavenge + Parallel Old 的收集器组合进行回收内存 |
ServivorRatio | 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为8,代表 Eden : Survivor=8 : 1 |
PretenureSizeThreshold | 直接晋升老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次 Minor GC 之后,年龄旧增加1,当超过这个参数值就进入老年代 |
UseAdaptiveSizePolicy | 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailiure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行 GC 时进行内存回收的线程数 |
GCTimeRatio | GC 时间占总时间的比率,默认值为99,即允许1%的 GC 时间。仅在使用 Parallel Scavenge 收集时生效 |
MaxGCPauseMillis | 设置 GC 的最大停顿时间。仅在使用 Parallel Scavenge 收集器时生效 |
CMSInitiatingOccupancyFraction | 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集。默认值68%,尽在使用CMS 收集器时生效 |
UseCMSCompactAtFullCollection | 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用 CMS 收集器时生效 |
CMSFullGCsBeforeCompaction | 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用 CMS 收集器时生效 |
UseG1GC | 使用 G1 (Garbage First) 垃圾收集器 |