JVM运行时数据区
- 线程独占:每个线程都会有它独立的空间,随线程生命周期而创建和销毁(虚拟机栈、本地方法栈、程序计数器)
- 线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁(堆和元空间)
方法区
方法区和堆一样,是各个线程共享的内存区域,JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据;
虚拟机规范中方法区是堆的一块逻辑区域,但是却有一个别名叫Non-Heap(非堆),目的应该是和堆区分开。具体实现由不同的虚拟机来实现。
如:oracle的HotSpot在java7中方法区放在永久代,java8以后放在元数据空间,并且通过GC机制对这个区域进行管理。
Metaspace文章
元空间
区别永久代,元空间是在本地内存中分配。在JDK8中,Perm区中的所有内容中字符串常量移至堆内存,其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间中。
堆内存
所有对象实例以及数组都要在对上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也逐渐变得不是那么“绝对”了。
堆内存从内存回收的角度可以细分为:老年代、新生代(Eden、From Survivor、To Survivor)
堆内存从内存分配的角度可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
JVM启动时创建,存放对象的实例。垃圾回收器主要就是管理堆内存。
根据JVM规范,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以是可扩展的,主流虚拟机都是可扩展的(通过-Xmx和-Xms控制)。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,就会出现OutOfMemoryError内存溢出。
新生代=1个Eden区+2个Survivor区。绝大部分对象在Eden区生成,当Eden区装满的时候,会触发Young Garbage Collection,即YGC。
虚拟机栈
虚拟机栈,每个线程都在这个空间中有一个私有的栈,生命周期和线程相同,虚拟机栈描述的是Java方法执行的内存模型。
线程栈由多个栈帧(Stack Frame)组成,一个线程会执行一个或多个方法,一个方法对应一个栈帧。栈帧的内容包括:局部变量表、操作数栈、动态连接、方法返回地址、附加信息等。栈内存默认最大是1M,超出则抛出StackOverflowError栈溢出。
在JVM规范中,对于这个区域规定了两个异常情况:如果线程请求的栈深度大于JVM所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,扩展时无法申请到足够的内存,就会抛出OutOfMemoryError。
- 局部变量表
局部变量表是存放方法参数和局部变量的区域。对于非静态方法,在index[0]位置上存储的是方法所属对象的实例引用,随后存储的是参数和局部变量。字节码指令中的STORE指令就是将操作栈中计算完成的局部变量写回局部变量表的存储空间内。
- 操作数栈
操作栈是一个初始状态为空的桶式结构栈。JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈。
- 动态连接
每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程中动态连接。
- 方法返回地址
方法执行有两种退出情况:第一,正常退出,即正常执行到任何方法的返回字节码指令,如RETURN、IRETURN、ARETURN等;第二,异常退出。无论何种退出情况,都将返回至方法当前被调用的位置。
方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:
- 返回值压入上层调用栈帧
- 异常信息抛给能够处理的栈帧
- PC计数器指向方法调用后的下一条指令
本地方法栈
和虚拟机栈功能类似, 虚拟机栈是为虚拟枧执行JAVA 方法而准备的, 本地方法栈是为虚拟枧使用Native 本地方法而准备的。
虚拟机规范没有规定具体的实现, 由不同的虚拟机厂商去实现。
HotSpot 虚拟机中虚拟机栈和本地方法栈直接合二为一。同样, 也会抛出StackOverflowError和OutOfMemoryError。
程序计数器
程序计数器(Program Counter Register) 记录当前线程执行字节码的位置, 存储的是字节码指令地址, 如果执行Native 方法, 则计数器值为空。
每个线程都在这个空间有一个私有的空间, 占用内存空间很少,不会发生内存溢出异常。此区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。
CPU 同一时间, 只会执行一条线程中的指令。JVM 多线程会轮流切换并分配CPU 执行时间的方式。为了线程切换后, 需要通过程序计数器,来恢复正确的执行位置。
类生命周期
字节码
在代码的执行过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖;JVM也可以将字节码编译执行,如果是热点代码,会通过JIT动态编译为机器码,提高执行效率。
字节码的主要指令:
- 加载或存储指令
- 将局部变量加载到操作数栈中:ILOAD(将int类型的局部变量压入栈)、ALOAD(将对象引用的局部变量压入栈)
- 从操作栈顶存储到局部变量表:ISTORE、ASTORE
- 将常量加载到操作栈顶,这是极为高频使用的指令:ICONST、BIPUSH、SIPUSH、LDC等
- ICONST,加载的是-1~5的数(ICONST和BIPUSH的加载界限)
- BIPUSH,即Byte Immediate PUSH,加载-128~127之间的数
- SIPUSH,即Short Immediate PUSH,加载-32768~32767之间的数
- LDC,即Load Constant,在-2147483648~2147483647或者是字符串时
- 运算指令
将两个操作栈帧上的值进行运算,并把结果写入到操作栈顶,如IADD、IMUL
- 类型转换指令
显示转换两种不同的数值类型,如I2L、D2F等
- 对象创建和访问指令
- 创建对象指令:NEW、NEWARRAY等
- 访问属性指令:GETFIELD、PUTFIELD、GETSTATIC等
- 检查实例类型指令:INSTANCEOF、CHECKCAST等
- 操作栈管理指令
- 出栈操作:POP即一个元素,POP2即两个元素
- 复制栈顶元素并压入栈:DUP
- 方法调用与返回指令
- INVOKEVIRTUAL指令:调用对象的实例方法
- INVOKESPECIAL指令:调用实例初始化方法、私有方法、父类方法等
- INVOKESTATIC指令:调用类静态方法
- RETURN指令:放回VOID类型
- 同步指令
JVM使用方法结构中的ACC_SYNCHRONIZED标志同步方法,指令集中有MONITORENTER和MONITOREXIT支持synchronized语义。
- 字节码必须通过类加载器加载到JVM环境后才可以执行,执行有三种模式:
/**
- @ClassName TestWhoLoad
- @Description 测试哪个类加载器加载
- @Author dukz
- @Date 12/29/20
@Version 1.0 */ public class TestWhoLoad { public static void main(String[] args) {
// 正在使用的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader c = TestWhoLoad.class.getClassLoader();
System.out.println(c);
// AppClassLoader的父加载器是:sun.misc.Launcher$ExtClassLoader@4d7e1886
ClassLoader c1 = c.getParent();
System.out.println(c1);
// ExtClassLoader的父加载器是BooStrap。它是使用C++实现的,不在JVM体系中,返回null
ClassLoader c2 = c1.getParent();
System.out.println(c2);
} }
ExtClassLoader在JDK9以后改为了Platform ClassLoader,即平台类加载器
<a name="xus4G"></a>
##### 自定义类加载器
什么情况使用自定义类加载器:
- 隔离加载类:在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境
- 修改类加载方式:类的加载模型并非强制,出BootStrap外,其他的加载并不一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
- 扩展加载源:比如从数据库、网络,甚至是电视机机顶盒进行加载
- 防止源码泄露:Java代码容易被编译和篡改,可以进行编译加密,那就需要在自定义加载器中进行加密解密
自定义类加载器的步骤:<br />继承ClassLoader,重写findClass()方法,调用defineClass()方法。实例代码如下:
```java
package com.dukz.jvm;
import java.io.*;
/**
* @ClassName CustomClassLoader
* @Description 自定义类加载器
* @Author dukz
* @Date 12/29/20
* @Version 1.0
*/
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFormFile(name);
if(result == null){
throw new FileNotFoundException();
}else{
return defineClass(name, result, 0, result.length);
}
}catch (Exception e){
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
/**
* 注意:此处Class.forName和loadClass由于双亲委派模型,如果能被AppClassLoader加载则会被加载,最终结果是AppClassLoader;
* 而findClass由于方法重写所以始终会由customClassLoader加载;
*/
Class<?> clazz = Class.forName("com.dukz.jvm.TestWhoLoad", true, customClassLoader);
//Class<?> clazz = customClassLoader.loadClass("com.dukz.jvm.TestWhoLoad");
//Class<?> clazz = customClassLoader.findClass("com.dukz.jvm.TestWhoLoad");
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
}catch (Exception e){
e.printStackTrace();
}
}
/**
* @Author dukz
* @Description 根据字节码文件获取字节码数组
* @Date 12/29/20
* @Param [file]
* @return
**/
public byte[] getClassFormFile(String name){
String path = "/Users/dukz/"+name.replace('.', '/')+".class";
File file = new File(path);
if (file.exists()){
FileInputStream in = null;
ByteArrayOutputStream out= null;
try{
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer))!=-1){
out.write(buffer, 0, size);
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
}else {
return null;
}
}
}
执行结果如下:
com.dukz.jvm.CustomClassLoader@4d7e1886
双亲委派模型
垃圾回收机制
自动垃圾收集
自动垃圾收集是查看堆内存,识别正在使用哪些对象以及哪些对象未被删除以及删除未使用对象的过程。
使用中的对象或引用的对象意味着程序的某些部分仍然维护着指向该对象的指针。
程序的任何部分都不再引用未使用的对象或未引用的对象,因此可以回收未引用对象使用的内存。
如何确定内存需要被回收:
该过程的第一步称为标记。这是垃圾收集器识别哪些内存正在使用而哪些内存不在使用的地方。
可达性分析算法
简单来说,将对象及其引用关系看成一张图,选定活动的对象作为GC Roots。
然后追踪引用链条,如果一个对象和GC Roots之间不可达,也就是不存在引用,那么即可认为是可回收对象。
引用类型和可达性级别
引用类型:
- 强引用(StrongReference):最常见的普通对象引用,只要还有强引用指向一个对象,就不会回收。
- 软引用(SoftReference):JVM认为内存不足时,才会去试图回收软引用指向的对象。(缓存场景)
- 弱引用(WeakRenference):虽然是引用,但随时可能被回收掉。
- 虚引用(PhantomReference):不能通过它访问对象。提供了对象被finalize以后,执行指定逻辑的机制(Cleaner)。
可达性级别:
- 强可达(Strongly Reachable):一个对象可以有一个或多个线程可以不通过各种引用访问到的情况。
- 软可达(Softly Reachable):就是当我们只能通过软引用才能访问到对象的状态。
- 弱可达(Weakly Reachable):只能通过弱引用访问时的状态。当弱引用被清除的时候,就符合销毁条件。
- 幻象可达(Phantom Reachable):不存在其他引用,并且finalize过了,只有幻象引用指向这个对象。
- 不可达(unreachable):意味着对象可以被清除了。
垃圾回收算法
标记-清除(Mark-Sweep)算法:
首先识别出所有要回收的对象,然后进行清除。
标记-清除过程效率有限,有内存碎片化问题,不适合特别大的堆;
复制(Copying)算法:
划分两块同等大小的区域,收集时将活着的对象移动到另一块区域。
拷贝过程中将对象顺序放置,就可以避免内存碎片化。复制+预留内存,有一定的空间浪费。
标记-整理(Mark-Compact)算法:
类似于标记-清除,但为了避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。
分代收集
根据对象的存活周期,将内存划分为几个区域,不同区域采用合适的垃圾收集算法。
新对象会分配到Eden,如果超过-XX:+PertenureSizeThreshold:设置大对象直接进入老年代的阈值。
新生代中,S0:S1:Eden = 1:1:8
新生代:老年代 = 1:2
新生代采用复制算法,新生的对象进入Eden区,然后垃圾回收时,然后进入S0或者S1区来回复制进行清除,当GC一定次数仍存活则进入老年代,大对象会直接进入老年代,因为大对象来回复制消耗性能,新生代内存不足时,新生的对象也会进入老年代。
老年代采用标记-整理算法。
垃圾回收器
- 串行收集器-Serial GC -XX:+UseSerialGC
单个线程来执行所有垃圾收集工作,适合单处理器机器。Client模式下JVM的默认选项。
- 串行收集器-Serial Old -XX:+UseSerialOldGC
可以在老年代使用,它采用了标记-整理算法,区别于新生代的复制算法。
- 并行收集器-Parallel GC -XX:+UseParallelGC
- 并行收集器-Parallel Old GC -XX:+UseParallelOldGC
server模式JVM的默认GC选择,整体算法与Serial类似,区别在于新生代和老年代都是并行进行;
可以设置GC时间或吞吐量等值,可以自动适应性调整Eden、Survivor大小和MaxTenruingThreshold(进入老年代年龄,默认15)的值。
也称吞吐量优先的GC:吞吐量=用户代码运行时间/(用户代码运行时间+GC时间)
-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常设置与cpu数量相等。
-XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于0的整数。
-XX:GCTimeRatio:设置吞吐量大小,它的值是0-100之间的整数。
-XX:+UseAdaptiveSizePolicy:打开自适应GC策略。以达到在堆大小、吞吐量和停顿时间之间的平衡点。
- 并发收集器-CMS(Concurrent Mark Sweep)GC -XX:+UseConcMarkSweepGC
专用老年代,基于标记-清除算法,设计的目标是尽量减少停顿时间。
采用的标记-清除算法,存在内存碎片化的问题,长时间运行等情况下发生Full GC,导致恶劣的停顿。
CMS会占用更多的CPU资源,并和用户线程争抢。
减少了停顿,这一点对于互联网web等对时间敏感的系统非常重要,一直到今天,仍然有许多系统使用CMS GC。
- 并行收集器-ParNew GC -XX:+UseParNewGC
新生代GC实现,实际是Serial GC的多线程版本。
可以控制线程数量,参数:-XX:ParallelGCThreads
最常见的应用场景是配合老年代的CMS GC工作。参数-XX:+UseConcMarkSweepGC
- 并发收集器-G1 -XX:+UseG1GC
针对大堆内存设计的收集器,兼顾吞吐量和停顿时间,JDK9后为默认选型,目标是替代CMS。
G1将堆分为固定大小的区域,Region之间是复制算法,但整体上实际可以看作是标记-整理算法。
可以有效地避免内存碎片。红色新生代(Eden和Surivor),淡蓝色老年代。找不到大内存时执行FullGC。
垃圾收集器组合
JDK内置命令工具
javap
java反编译工具,主要用于根据Java字节码文件反汇编为Java源代码文件。
javap
jps
jps(Java Virtual Machine Process Status Tool) 显示当前所有Java进程pid的命令。
jps [options] [hostid]
jstat
jstat监视Java虚拟机统计信息
用法:jstat [generalOption | outputOptions vmid [interval [s I ms] [count]]]
-t参数可以在输出信息前面加上一个Timestamp列,显示程序运行的时间。
-h参数可以在周期性的数据输出时,输出多少行数据后,跟着输出一个表头信息。
interval指定输出统计周期,count指定输出多少次。
jcmd
jcmd工具,可以替代jps工具查看本地JVM信息。
jcmd -l
jcmd
jinfo
jinfo可以查看运行中jvm的全部参数,还可以设置部分参数。
jinfo [option] pid
jinfo [option] executable core
jinfo [option] [server-id@]remote-hostname-or-IP
jhat
Java heap analyse tool 分析java堆的命令,可以将堆中的对象以html的形式显示出来,支持对象查询语言oql。
jhat [-stack
jmap
打印出java进程内存中Object的情况。或者将VM中的堆,以二进制输出文本。常用于内存泄漏、内存溢出时查看。
jmap [option] pid (to connect to remote debug server)
jmap [option] executable core (to connect to remote debug server)
jmap [option] [server-id@]remote-host-or-IP (to connect to remote debug server)
jstack
堆栈跟踪工具,常用于死锁或cpu占用100%,当前运行的线程的堆栈信息。jstack用于打印出给定java进程ID或core file或远程调试服务的java堆栈信息,如果是在64位机器上,需要指定选项“-j-d64”。
jstack [option] pid
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-IP
JDK内置工具-Jconsole
Java内置工具-JvisualVM
JVM参数及调优
调优基本概念
在调整性能时,JVM有3个组件:
- 堆大小调整
- 垃圾收集器调整
- JIT编译器
大多数调优选项都与调整堆大小和为你的情况选择最合适的垃圾收集器有关。
JIT编译器对性能也有很大影响,但很少需要使用较新版本的JVM进行调优。
通常,在调优Java程序时,重点是以下两个目标之一:
- 响应性:应用程序或系统对请求的数据进行响应的速度,对于专注于响应性的应用程序,长的暂停时间是不可接受的,重点是在短时间内做出回应。
- 吞吐量:侧重于在特定时间内最大化应用程序的工作量,对于专注于吞吐量的应用程序,高暂停时间是可以接受的。由于高吞吐量应用程序在较长时间内专注于基准测试,因此不需要考虑响应时间。
系统瓶颈核心还是在应用代码,一般情况下无需过多调优,JVM本身在不断优化。
常用的JVM参数
JVM版本不断更新,JVM参数和具体说明,建议需要时参考oracle官网的手册。
GC调优思路
- 分析场景
例如:启动速度慢;偶尔出现响应慢于平均水平或者出现卡顿
- 确定目标
内存占用、低延时、吞吐量
- 收集日志
通过参数配置收集GC日志;通过JDK工具查看GC状态
- 分析日志
使用工具辅助分析日志,查看GC次数,GC时间
- 调整参数
通过GC参数
GC日志查看工具:https://github.com/chewiebug/GCViewer
垃圾收集器Parallel参数调优
- JDK默认的垃圾收集器
- 吞吐量优先
垃圾收集器CMS参数调优
- 响应时间优先
- Parallel GC无法满足应用程序延迟要求时再考虑使用CMS垃圾收集器
- 新版建议使用G1垃圾收集器
垃圾收集器G1参数调优
- 兼顾吞吐量和响应时间
- 超过50%的Java堆被实时数据占用
- 建议大堆(大小约为6GB或者更大)
- GC延迟要求有限的应用(稳定切可预测的暂停时间低于0.5秒)
运行时JIT编译器参数调优
JIT编译指的是字节码编译为本地代码(汇编)执行,只有热点代码才会编译为本地代码。
解释器执行节约内存,反之可以使用编译执行来提升效率。
很少需要对较新版本的JVM进行JIT调优。