1.为什么需要JVM?
1.Java语言与Jvm之间的关系
Java语言是跨平台的,编译过程是:.java -> .class -> 0101指令
- Java语言的不同,在C/C++中程序员用malloc和free来分配和释放内存,但在Java中,程序员不需要显式的释放一个对象的内存,而是通过虚拟机进行。
2.Java内存区域
https://www.cnblogs.com/dolphin0520/p/3613043.html
2.1内存区域图
jdk8以前
jdk8
2.2程序计数器
在汇编语言中,程序计数器是指CPU中的寄存器,保存的是当前执行的指令的地址(或下一条要执行的指令地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令的地址。
在JVM中,为了能够使得每个线程在切换后能够恢复切换之前的程序执行位置,每个线程都有自己独立的程序计数器,如果是Java方法,计数器记录的是Java字节码地址,如果是native本地方法,计数器就为空。
2.3虚拟机栈
https://blog.csdn.net/Yohohaha/article/details/89378915
https://www.cnblogs.com/code-duck/p/13559193.html
虚拟机栈存放的是一个个的栈帧,每个栈帧对应一个被调用的方法;
栈帧
-
局部变量表:存放编译期已知的数据类型,和对象引用;
-
操作数栈:程序中的计算过程都要借助操作数栈来完成;
-
指向运行时常量池的引用:方法执行过程中用到的类中的常量;
-
方法返回地址:方法执行完,需要知道返回之前调用他的地方;
但线程执行一个方法时,就会创建一个对应的栈帧,并压栈,执行结束后,弹栈,结束方法有return和抛出异常,所以递归的时候容易导致栈内存溢出的现象;
栈的大小决定了栈的可达深度,栈的大小可以是固定的,可以是动态扩展的;
- 如果是固定的,当请求深度大于最大可用深度就会抛
StackOverflowError
- 如果是动态扩展的,但没有内存支持扩展,会抛
OutofMemoryError
设置虚拟机的大小:-Xss128k
表示可以设置虚拟机栈的大小为128k
2.4本地方法栈
和虚拟机栈作用相似,管理的是本地的方法,虚拟机栈执行Java方法服务,本地方法栈则为native方法服务;
本地方法被执行的时候,也会创建一个栈帧,包括局部变量表,操作数栈,动态链接,出口信息;
也会抛StackOverflowError
或OutofMemoryError
2.5堆
堆是所有线程共享的一块区域,在虚拟机启动的时候创建,同时也是GC管理的主要区域,又称GC堆,存放所有对象实例和数组(数组引用是存放在Java栈中);
由于现在收集器都采用分代垃圾收集算法,所以堆又细分为:新生代,老生代,永生代(JDK8取消永生代,取而代之是元空间,元空间使用的直接内存);
-
新生代:
新生代又细分为:Eden空间、From Survivor、To Survivor;
首先在Eden区分配,进行一次垃圾回收后,还存在就会进入s0,s1区,对象的年龄也会+1,当年龄到达一定的阈值后,会进入老生代;
Eden区占大容量,2个Survivor区占小容量,默认比例8:1:1;
-
老生代:
占整个堆空间的4/5
堆没有内存支持扩展,会抛OutofMemoryError
;
2.6方法区
方法区也称为非堆,永久代;
存储每个类的信息:类名,类的方法,属性,静态变量,常量,编译之后的代码;
JDK8取消方法区(永久代),取而代之的是元空间,元空间使用的是直接内存;
为什么取消永生代?为融合HotSpot JVM与JRockit VM(新JVM技术)而作出的改变,JRockit没有永生代;
-XX:MetaspaceSize=N
设置元空间大小,-XX:MaxMetaspaceSize=N
设置元空间最大大小,默认是unlimited
只受系统内存限制;
元空间溢出异常:java.lang.OutOfMemoryError: MetaSpace
2.7运行时常量池
JDK8字符串常量池在堆中,运行时常量池在元空间中;
3.对象创建的过程
-
类加载检查
www.cnblogs.com/aspirant/p/7200523.html
https://blog.csdn.net/weixin_43897770/article/details/108529965
当虚拟机遇到一条new的指令,首先检查常量池中是否能够定位到这个类的符号引用,并检查这个符号引号代表的类是否被加载过,解析和初始化过,没有,执行类加载过程;
-
分配内存
类加载检查后,会在Java堆中分配一块内存,分配方式有指针碰撞和空闲列表,使用那种方式取决于Java堆是否规整决定;
-
指针碰撞适用于没有内存碎片的情况,即垃圾收集器的算法是标记-整理;
-
空闲列表适用于有内存碎片的情况,即垃圾收集器的算法是标记-清楚;
-
-
初始化零值
需要将分配到的内存都初始化为零值,保证了对象的实例字段在不赋初始值就可以直接使用;
-
设置对象头
虚拟机对这个对象进行信息标记,包括是否是新生代\老年代,对象的哈希码,GC 分代年龄等等
-
执行init方法
所有的字段都还为零值,执行init方法,按照程序员的意愿初始化这个对象;
对象的内存布局
对象头:一部分存储对象自身的运行时数据(哈希码,GC分代年龄,等等),一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定是那个类的实例;
实例数据:各种类型的字段内容;
对齐:8字节的倍数,不够填充来补齐;
4.JVM垃圾回收
https://cloud.tencent.com/developer/article/1623210
https://www.cnblogs.com/cielosun/p/6674431.html
https://zhuanlan.zhihu.com/p/159200599
4.1什么是垃圾回收
在C/C++中程序员用malloc和free来分配和释放内存,但在Java中,程序员不需要显式的释放一个对象的内存,而是通过虚拟机进行;
在JVM中,有一个垃圾回收线程,它是最低优先级的,在正常情况下不会执行,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行;
4.2分配原则
Java堆是垃圾收集器管理的主要区域;
可以细分为新生代、老年代:
- 新生代又分为Eden区,From Survivor ,To Survivor区;
- 细分的目的是为了更好的回收内存,或者更快的分配内存;
- 大部分情况下,会优先分配到Eden区
当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC,期间发现无法存入Survivor区,只好通过分配担保机制转移到老年代中区,发现老年代中有足够的空间,不会Full GC
分配担保机制:
在新生代无法分配内存的时候,把新生代的对象转移到老年代中。
-
大对象直接进入老年代,这样做的目的是避免在Survivor区来回复制;
-
长期存活的进入老年代,每进行一次Minar GC都是在使对象头中的GC 分代年龄+1,当年龄达到一个阈值(默认15岁)时,对象进入老年代;
进入老年代的阈值设置
-XX:MaxTenuringThreshold=threshold
-
动态判断对象的年龄:https://blog.csdn.net/u014493323/article/details/82921740
按照年龄从小到大对其所占的大小进行累积,当累积的某个年龄超过Survivor区的一半时,取这个年龄和MaxTenuringThreshold的最小值作为进入老年代的阈值;
-XX:TargetSurvivorRatio
:目标存活率,默认50%// 源码在hotspot中的src/share/vm/gc_implementation/ageTable.cpp //survivor_capacity是survivor空间的大小 uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); size_t total = 0; uint age = 1; while (age < table_size) { total += sizes[age];//sizes数组是每个年龄段对象大小 if (total > desired_survivor_size) break; age++; } uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshld; ... }
4.3什么是垃圾
在堆中存在的对象已经死亡,死亡是指不能再被任何途径使用的对象。
引用计数器
给对象添加一个计数器,当有被引用的时候计数器+1,引用被释放或失效的时候计数器-1,当计数器的值为0时就可以被回收;
优点是实现比较简单,缺点是需要额外的空间来存储计数器,还有检测不了循环引用的问题。
可达性分析
可达性分析也称为根搜索法,可达性是指一个对象至少被其他属性等直接或间接引用则为可达;
通过一系列的根“GC Roots”作为起点,向下搜索,当一个对象到GC Roots如果没有任何引用链的话,则该对象是不可用的。
什么才能是GC Roots?
- 虚拟机栈中栈帧中局部变量表引用的对象
- 本地方法栈栈帧中局部变量表引用的对象
- 方法区静态变量引用的对象
- 方法区常量引用的对象
- 活跃线程
4.4引用
https://blog.csdn.net/linzhiqiang0316/article/details/88591907
-
强引用
发生GC时不会被回收,宁愿发生
OutofMemoryError
,如果想中断和某个对象的引用,可以显式的引用赋值为null
,在ArrayList的remove中:elementData[--size] = null; // clear to let GC do its work jvm在合适的时候会回收
-
软引用
如果内存不足时,会被回收
// 对于a来说有2个引用路径,一个是变量强引用,一个是softReference的软引用 A a = new A(); SoftReference<A> softReference = new SoftReference<>(a); a.value = "hello"; // a = null 此时 a 称为了软引用对象 a = null; // softReference内存不足时才会回收 // 在垃圾回收之前get()方法返回a的强引用 // 在垃圾回收之后get()方法返回null // 如果存在大量的无用的SoftReference也会造成内存泄漏 // 使用ReferenceQueue来管理 ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); SoftReference<A> tSoftReference = new SoftReference<A>(a, referenceQueue);
-
弱引用
无关内存是否充足,垃圾回收时都会被回收
A a = new A(); WeakReference<A> weakReference = new WeakReference<>(a); a.value = "hello"; // 在垃圾回收之前get()方法返回Java对象的弱引用 System.out.println(weakReference.get()); a = null; // JVM尽快GC一次,但不会立即执行GC System.gc(); // 在垃圾回收之后get()方法返回null System.out.println(weakReference.get()); // output Java.basis.jvm.WeakReferenceDemo$A@1b6d3586 null
-
虚引用
任何时候都可以被回收,跟没有引用关联一样
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); A a = new A(); PhantomReference<A> phantomReference = new PhantomReference<>(a, referenceQueue); System.out.println(phantomReference.get()); // output null
4.5垃圾回收算法
标记-清除
首先标记不需要回收的对象,在标记结束后统一回收没有标记的对象,缺点,会产生很多不连续的空间,内存碎片化严重。
标记-复制
把内存分为大小相同的2块区域,每次把一块上的存活的对象移到另一块,然后再把已使用的一块内存一次清理,缺点,内存使用率低,只能用一半。
标记-整理
与标记-清除算法类似,不过标记-清除操作是删除可回收对象,而标记-整理是把所有不可回收的整理向一端移动,然后删除边界外的内存。
分代收集
分代收集是目前大部分JVM所采用的方法,根据对象的存活情况把内存分成几块,新生代,老年代
新生代:新生代每次回收都是大量的回收,所以采用的算法一般是标记-复制
老年代:老年代采用的算法一般是标记-整理和标记-清除
4.6垃圾收集器
https://blog.csdn.net/xiaoai1994/article/details/109644578
https://www.sohu.com/a/334034200_494939
其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,
回收老年代的收集器包括Serial Old、Parallel Old、CMS,
有用于回收整个Java堆的G1收集器。
Serial收集器
说明:Serical收集器是最基本,历史最悠久的垃圾收集器,是一个单线程收集器,不仅只会使用一条垃圾收集线程,而且收集工作的时候,必须暂停所有其他的工作线程。
组合:Serial + Serial Old
使用场景:单个CPU的环境,Serial收集器没有线程交互,可以获得最高的单线程收集效率。
相关参数:-XX:+UseSerialGc,显式的使用串行垃圾收集器。
ParNew收集器
说明:Serial收集器的多线程版本。
组合:ParNew + Serial Old,ParNew + ParNew Old,ParNew + CMS
使用场景:在Service模式下,除Serial收集器外,只有它能和CMS配合工作
相关参数:-XX:+UseConcMarkSweepGC,指定使用CMS后,会默认使用ParNew作为新生代收集器;
-XX:+UseParNewGC,强制指定使用ParNew;
-XX:ParallelGCThreads,指定垃圾收集的线程数量,PraNew默认开启的数量和CPU的数量相同。
Parallel Scavenge收集器
说明:Parallel Scavenge收集器关注的是吞吐量,吞吐量是指CPU中执行用户线程时间与CPU总消耗时间的比值。作用与新生代。
组合:Parallel Scavenge + Parallel Old
使用场景:注重吞吐量,高效利用CPU,不需要太多交互。
相关参数:-XX:MaxGCPauseMillis,设置收集器在多久之内完成收集,
-XX:GCTimeRatio:精准控制吞吐量,
-XX:UseParallelGC:使用Parallel Scavenge收集器作为新生代收集器,java7,java8默认是它
如果指定了
-XX:UseParallelGc
参数,则默认指定了-XX:UseParallelOldGc
,可以使用-XX:UseParallelOldGc
来禁用该功能
# java8默认使用的是Parallel Scavenge + Parallel Old
$ java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=534184832 -XX:MaxHeapSize=8546957312 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Serial Old收集器
说明:单线程收集器,Serial收集器的老年代版本。采用标记-整理算法。
组合:Serail + Serial Old
使用场景:Client模式(桌面应用),单核服务器,作为CMS的后备预案。
Parallel Old收集器
说明:Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
组合:Parallel Scavenge + Parallel Old
使用场景:和Paraller Scavenge搭配使用。
相关参数:-XX:+UseParallelOldGC,指定Parallel Old收集器
CMS(Concurrent Mark Sweep)收集器
说明:并发收集器,以最短回收停顿时间为目标的收集器,实现了垃圾收集线程与用户线程的同时工作,注重用户体验。作用与老年代,采用标记-清除算法。
组合:Serial + CMS ,ParNew + CMS
使用场景:重视服务器响应时间,要求系统停顿时间最少
相关参数:-XX:+UseConcMarkSweepGC,使用CMS作为老年代收集器
回收过程:
- 初始标记:暂停其他线程,记录
GC Roots
能关联的对象; - 并发标记:同时开始
GC
和用户线程,进行GC Roots Traving
; - 重新标记:为了修正并发标记过程中用户程序继续运行导致标记产生变动的那一部分对象的标记记录;
- 并发清除:开启用户线程,同时
GC
线程开始对未标记的区域做清扫。
优点:并发收集,低停顿;缺点:吞吐量低,标记-清除算法会产生空间碎片,无法处理浮动垃圾。
吞吐量低:因为并发执行,线程来回切换产生额外的CPU开销。
浮动垃圾:在并发标记的过程中,GC线程标记未存活的对象,用户线程不再会使用该对象,该对象没有任何引用,称为浮动垃圾,浮动垃圾过多会导致频繁GC;
使用场景:它关注的是垃圾回收的最短停顿时间,在老年代并不频繁的GC情况下,是比较适用的。
G1收集器
说明:弱化了CMS原有的分代模型,负责维护一个一个Region的列表,所以G1收集器是基于整个Java堆。标记-整理算法,不会产生内存碎片,G1收集器的范围是整个Java堆,同时具备低停顿,高吞吐量。JDK9默认收集器。
使用场景:可控GC停顿时间,内存占用较大的应用。
相关参数:-XX:+UseG1GC,使用G1收集器。
回收过程:
- 初始标记:同样的是停止所有线程,标记与
GC Roots
对象直接关联的对象; - 并发标记:进行
GC Roots Traving
,为了避免全堆扫描,在每个Region区域都有一个Remembered Set来实时记录该区域内的引用对象数据与其他区域的引用关系,在标记时直接参考这些引用关系可以知道这些对象是否被清除,而不用扫全堆的数据。 - 重新标记:停止所有线程,标记出并发标记中业务线程产生的浮动垃圾;
- 筛选回收:对每个Region的回收成本进行排序,按照用户自定义的回收时间来制定回收计划。
G1收集器虽然采用分代的处理方式,但是它的模型有了巨大的变化,G1负责维护一个Region的列表,每次进行GC的时候,首先评估每个Region的最大回收价值,然后回收掉价值最大的,从而获得最大的GC回收率。
ZGC收集器
下一代垃圾收集器,ZGC垃圾回收过程几乎全部是并发,实际STW停顿时间极短
5.JDK监控和故障处理工具
- jps:查看所有Java进程,jps-l
$ jps -l
# 本地虚拟机唯一ID:vmid Jar路径
3440 org.jetbrains.jps.cmdline.Launcher
9936 com.iboxchain.mini.share.ShareApplication
17288 sun.tools.jps.Jps
21416
23576 org.jetbrains.jps.cmdline.Launcher
-
jstat监视虚拟机各种运行状态信息
参数:https://blog.csdn.net/maosijunzi/article/details/46049117
jstat -class vmid #:显示 ClassLoader 的相关信息;
jstat -compiler vmid #:显示 JIT 编译的相关信息;
jstat -gc vmid :显示与 #GC 相关的堆信息;
jstat -gccapacity vmid #:显示各个代的容量及使用情况;
jstat -gcnew vmid #:显示新生代信息;
jstat -gcnewcapcacity vmid #:显示新生代大小与使用情况;
jstat -gcold vmid #:显示老年代和永久代的行为统计,从jdk1.8开始,该选项仅表示老年代,因为永久代被移除了;
jstat -gcoldcapacity vmid #:显示老年代的大小;
jstat -gcpermcapacity vmid #:显示永久代大小,从jdk1.8开始,该选项不存在了,因为永久代被移除了;
jstat -gcutil vmid #:显示垃圾收集信息;
- jinfo实时查看和调整虚拟机各项参数
$ jinfo 9936
# 输出当前 jvm 进程的全部参数和系统属性
Attaching to process ID 9936, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.281-b09
# 系统属性
Java System Properties:
spring.output.ansi.enabled = always
java.runtime.name = Java(TM) SE Runtime Environment
java.vm.version = 25.281-b09
sun.boot.library.path = E:\Java\JDK\jdk1.8.0_281\jre\bin
java.vendor.url = http://java.oracle.com/
java.vm.vendor = Oracle Corporation
path.separator = ;
java.rmi.server.randomIDs = true
file.encoding.pkg = sun.io
java.vm.name = Java HotSpot(TM) 64-Bit Server VM
sun.os.patch.level =
sun.java.launcher = SUN_STANDARD
user.script =
user.country = CN
......
# jvm属性
VM Flags:
Non-default VM flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:CICompilerCount=4 -XX:InitialHeapSize=534773760 -XX:+ManagementServer -XX:MaxHeapSize=8547991552 -XX:MaxNewSize=2848980992 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=178257920 -XX:OldSize=356515840 -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line: -XX:TieredStopAtLevel=1 -Xverify:none -Dspring.output.ansi.enabled=always -javaagent:D:\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar=62812:D:\IntelliJ IDEA 2020.3.2\bin -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dfile.encoding=UTF-8
# 查看参数具体值
$ jinfo -flag MaxHeapSize 9936
-XX:MaxHeapSize=8547991552
# 开启和关闭对应名称的参数
$ jinfo -flag [+|-]name
-
jmap生成堆转储快照
相当于-XX:+HeapDumpOnOutOfMemoryError,还可以查看finalizer执行队列,Java堆和永久代的详细信息,如空间使用率,当前使用的是哪种收集器等。
-
jhat,分析heapdump(堆转储快照)文件,可以在浏览器查看分析结果。
-
jsatck,生成当前虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
可以用来定位线程长时间停顿的原因,如线程死锁,死循环,请求外部资源导致的长时间等待。
6.JVM参数
https://www.cnblogs.com/qf123/p/8671325.html
https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
其三是非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;
-server:这个参数主要是用来指定运行模式的,指定了-server那么启动的速度就会慢一些
-Xms20M:堆的最小容量
-Xmx20M:堆的最大容量
单位都是M,设置成一样为了避免堆自动扩展:如果虚拟机启动时设置使用的内存比较小,这个时候又需要初始化很多对象,虚拟机就必须重复地增加内存。
-Xmn10M: 新生代容量
-XX:NewRatio=4:设置新生代,设置了-Xmn,则被忽略
https://www.jianshu.com/p/c4f00b61b423
NewRatio=4:值是老年代的占比,老年代占4,剩下的1给新生代
NewRatio=2:老年代占2,新生代占1
-XX:SurvivorRatio=8:表示设置2个Survivor的占比,SurvivorRatio=8说明Eden占8,剩下的from,to各占1
https://www.jianshu.com/p/f12b763606f5
Eden区占大容量,2个Survivor区占小容量,默认比例8:1:1;
-Xss128k:虚拟机栈的大小
-Xoss128k:本地方法栈的大小,HotSpot不区分虚拟机栈和本地方法栈,所以设置了也没用
-XX:PermSize=10M:永久代大小
-XX:MaxPermSize=10M:最大永久代大小
但是都是M,java8取消了PermSize
-XX:MetaspaceSize=64m:元空间大小
-XX:MaxMetaspaceSize=384m:最大元空间大小
-Xnoclassgc:关闭JVM对类的垃圾回收
-XX:+HeapDumpOnOutOfMemoryError:可以让虚拟机出现内存异常时Dump出当前的堆内存转储快照
-XX:HeapDumpPath=${LOG_PATH}/gc/gc_dump_`date +'%Y-%m-%d_%H-%M-%S'`.bin : dump的路径
-verbose:gc:输出虚拟机GC的详情情况
-XX:+PrintGCDetails:在控制台打印GC情况
2者功能一致,https://blog.csdn.net/xiaojin21cen/article/details/106673075
-XX:MaxTenuringThreshold=1:新生代年龄=1时进入老年代,进入老年代的阈值
-Xloggc:${LOG_PATH}/gc/gc_`date +'%Y-%m-%d_%H-%M-%S'`.log : gc日志
7.JVM调优
https://wangkang007.gitbooks.io/jvm/content/4jvmdiao_you.html