
【jvm】《尚硅谷宋红康JVM全套教程(详解java虚拟机)》上篇 笔记
是一个很小的内存空间,几乎可用忽略不记。也是运行速度最快的存储区域在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致任何时间一个线程都只有一个方法执行,也就是所谓的当前方法。程序计数器会存储当前方法正在执行的java方法的jvm指令地址;或者,如果是在执行native方法,则是未指定值(undefined)他是程序控制流的指示器,分支、循环、跳转、异常处
文章目录

资料
学习于B站视频:https://www.bilibili.com/video/BV1PJ411n7xZ
语言热度排行榜:https://www.tiobe.com/tiobe-index/
JVM 8官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html,若使用其他版本,步骤:
Java8 命令行可用参数:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html,其他版本:
虚拟机
一台虚拟的计算机,两类:
- 系统虚拟机(完全对物理计算机的仿真)
- 程序虚拟机(专门为执行某个程序)
作用:二进制字节码的运行环境
jvm:装在操作系统上,与硬件没有直接交互
jvm架构模型
基于栈式架构的特点:
- 设计和实现更简单,适用于资源受限的系统
- 避开了寄存器的分配难题,使用零地址指令方式分配
- 指令流的大部分是零地址指令,其执行过程依赖于操作栈,指令集小容易实现
- 不需要硬件支持,可移植性更好,更好跨平台
基于寄存器架构的特点:
- 典型的应用是x86的二进制指令集
- 指令集架构完全依赖硬件,可抑制性差
- 性能优秀和执行更高效
- 花费更少的指令去完成一项操作
- 在大多数情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主
java的指令是根据栈来设计的,跨平台、指令集小、指令多;执行性能比寄存器差
jvm生命周期
启动:引导类加载器创建初始类完成,这个类是由虚拟机具体实现决定的
执行:执行一个叫java虚拟机的进程
退出:
- 正常退出
- 执行过程出现异常错误
- 操作系统出现错误
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java返券管理器也允许这次exit或halt操作
- JNI(java native interface)规范描述了用jni invocation api来加载或者写在 java 虚拟机时,java虚拟机的退出情况
类的加载过程
加载
- 通过一个类的全限定类名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区运行的数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 加载.class文件方式:
- 从本地系统中直接加载
- 从网络获取
- 压缩包(jar、war)
- 运行计算生成(动态代理技术)
- 其他文件生成,例如jsp
- 专有数据库中提取
- 加密文件中获取,放class文件被反编译的保护措施
链接
:
-
验证:
- 目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证,文件格式验证、元数据验证、字节码验证、符号引用验证
-
准备:
- 为类变量分配内存并且设置该类变量的默认初始值,即零值
- 不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
- 不会为实例变量初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中
-
解析
- 将常量池的符号引用转换为直接引用
初始化
- 初始化阶段就是执行类构造器方法<clinit>()的过程
- 不需要定义,javac编译器自动收集类中的所有类变量和静态代码块中的语句合并起来
- <clinit>()不同于类的构造器(构造器是虚拟机视角的<init>())
- 若该类具有父类,jvm会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
- 虚拟机保证一个类的<clinit>()方法再多线程下被同步加锁
类变量有两次初始化机会,一次是在链接的准备阶段,赋0值,另一次是在初始化阶段,赋予我们定义的值
类加载器
类加载器分类(按照java虚拟机规范)
- 引导类加载器(Bootstrap ClassLoader)
- 自定义类加载器(User-Defined ClassLoader,除了引导类加载器以外的)
虚拟机自带的加载器:
1、引导类加载器:Bootstrap ClassLoader
- c/c++语言实现,嵌套在jvm内部
- 用来加载java核心库:JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容,用于提供jvm自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 架载扩展类和应用程序类加载器,并指定为他们的父类加载器
- 处于安全考虑,只加载包名为java、javax、sun等开头的类
//通过Launcher.getBootstrapClassPath().getURLs()获取: file:/C:/Java/jdk1.8.0_271/jre/lib/resources.jar file:/C:/Java/jdk1.8.0_271/jre/lib/rt.jar file:/C:/Java/jdk1.8.0_271/jre/lib/sunrsasign.jar file:/C:/Java/jdk1.8.0_271/jre/lib/jsse.jar file:/C:/Java/jdk1.8.0_271/jre/lib/jce.jar file:/C:/Java/jdk1.8.0_271/jre/lib/charsets.jar file:/C:/Java/jdk1.8.0_271/jre/lib/jfr.jar file:/C:/Java/jdk1.8.0_271/jre/classes
2、扩展类加载器
- java语言编写
- 派生于ClassLoader
- 父类为启动类加载器
- java.ext.dirs系统属性所指定的目录中加载类库,或从jdk安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也就自动有扩展类加载器加载
//通过System.getProperty("java.ext.dirs")获取: C:\Java\jdk1.8.0_271\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
3、应用程序类加载器(系统类加载器,appClassLoader)
- java语言编写,有sun.misc.Launcher$AppclassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类类加载器
- 它负责加载器环境变量classpath(自定义类)或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一百来说,java应用的类都是由它来完成加载
- 通过ClassLoaser#getSystemClassLoader()方法可以获取到该类的类加载器
用户自定义类加载器
为什么要自定义类加载器?
- 隔离加载类
- 修改类加载方式
- 扩展加载源
- 防止源码泄露
如何自定义?
-
继承ClassLoader
- 没有复杂需求,可继承classLoader子类URLClassLoader类
-
jdk1.2之前,要重写loadClass方法,jdk1.2之后不建议覆盖loadClass方法,而是建议把自定义类加载逻辑写在findClass方法中
获ClassLoader方法:
ClassLoader classLoader1 = Class.forName("java.lang.String").getClassLoader();
ClassLoader classLoader2 = Thread.currentThread().getContextClassLoader();
ClassLoader classLoader3 = ClassLoader.getSystemClassLoader();
System.out.println("classLoader1 = " + classLoader1);
System.out.println("classLoader2 = " + classLoader2);
System.out.println("classLoader3 = " + classLoader3);
//classLoader1 = null
// classLoader2 = sun.misc.Launcher$AppClassLoader@18b4aac2
// classLoader3 = sun.misc.Launcher$AppClassLoader@18b4aac2
双亲委派机制
java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类的时候才会将该类的class文件加载到内存生成class对象,而且加载某个类的class文件时,java虚拟机采用的时双亲委派模式,即把请求交由父类处理,他是一种任务委派模式
原理:(向上委托到最顶层之后,依次尝试加载:引导类加载器 -> 扩展类加载器 -> 系统类加载器)
- 如果一个类加载器收到了类加载请求,并不会自己先去加载,而是委托给其父类加载器进行加载
- 如果其父类加载器还存在父类,则会继续向上委托,最终到达引导类加载器
- 如果父类可以完成类加载,则成功返回,否则,子类加载器尝试进行类加载
例子:
- 自定义一个java.lang.String类
- 尝试创建对象
- 发现用不是自定义的String类(未输出"自定义String…")
优点:
- 防止重复加载
- 防止核心api被篡改(例如:不能在java.lang下自定义类,否则会报java.lang.SecurityException)
沙箱安全机制
原文更详细:https://blog.csdn.net/qq_30336433/article/details/83268945
是什么?
- Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。
- 沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且
严格限制代码对本地系统资源访问
,通过这样的措施来保证对代码的有效隔离
,防止对本地系统造成破坏。 - 沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
- 所有的Java程序运行都可以指定沙箱,可以定制安全策略。
核心组件:
- 字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
- 类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式.
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
- 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
- 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
- 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性
包括:安全提供者、消息摘要、数字签名、加密、鉴别
如图:自定义java.lang.String类,自其中写main方法尝试运行
运行结果:
说明使用的不是我们自定义的String
结论:
-
1、自定义String类,在加载的时候使用引导类加载器进行加载
↓
2、引导类加载器在加载的时候会加载jdk自带的文件中的String类
↓
3、自带jdk中的String没有main方法,导致报错 -
这样可以保证对java核心源代码的保护,这就是沙箱安全机制
类的主动使用与被动使用
在jvm中判断两个class对象是否为同一个类存在的两个必要条件:
- 类的完整类名一致
- 加载这个类的Class Loader(Class Loader实例对象)必须相同
jvm必须知道一个类型是由启动类加载器还是用户类加载器加载,如果是用户类加载器,会将其一个应用保存在jvm的方法区中;当解析一个类型到另一个类型时,jvm必须保证类加载器是相同的
主动使用:
- 创建类的实例
- 访问接口静态变量,或者对静态变量赋值
- 调用静态方法
- 反射
- 初始化一个类的子类
- java虚拟机启动时被标明为启动类的类
- jdk7开始提供的静态语言支持
- java.lang.invoke.MethodHandel实例的解析结果
- REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用java类的方式都时对类的被动使用,不会导致类的初始化
idea插件
jclasslib
方法信息:
字节码:
运行时数据区
内存
内存是硬盘和cpu的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。jvm内存布局规定了java在运行过程种内存申请、分配、管理的策略,保证jvm高效稳定运行。不同jvm对于内存的划分方式和管理机制存在着部分差异
内部结构
阿里的图:
有些和虚拟机生命周期一致,有些和线程生命周期一致
- 每个线程:程序计数器、栈、本地栈
- 堆、堆外内存(永久代或元空间、代码缓存)
一个jvm对应一个Runtime
线程
-
线程是一个程序里的运行单元,jvm允许一个应用由多个线程并行的执行
-
在Hotspot JVM里,每个线程与本地线程直接映射
-
操作系统负责所有线程安排调度到一个可用的cpu上,一旦本地线程初始化完成,他会调用java线程中的run()方法
程序计数器(pc寄存器)
用于存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取吓一条指令。
介绍
- 是一个很小的内存空间,几乎可用忽略不记。也是运行速度最快的存储区域
- 在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
- 任何时间一个线程都只有一个方法执行,也就是所谓的当前方法。程序计数器会存储当前方法正在执行的java方法的jvm指令地址;或者,如果是在执行native方法,则是未指定值(undefined)
- 他是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成
- 字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 他是唯一一个在java虚拟机规范中没有定义OutOfMemoryError情况的区域
- 无GC,无OOM
cpu时间片:
- cpu时间片即cpu分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片
- 在宏观上,我们可用同时打开多个应用程序,每个程序并行不悖
- 在微观上,由于只有一个cpu,一次只能处理程序要求的一部分,如何处理公平,一种方法时引入时间片,每个程序轮流执行
串行:一个线程执行
并行:多个线程同时执行
并发:多个线程交替执行
面试题
1、使用pc寄存器存储字节码指令地址由什么用?
答:因为cpu需要不停的切换各个线程,切换回来的时候需要知道从哪开始执行
2、为什么使用pc寄存器记录当前线程的执行地址?
答:JVM字节码解释器通过改变pc寄存器的值来明确下一天执行什么样的字节码指令
虚拟机栈
出现背景:
- 由于跨平台性的设计,java指令都是根据栈设计的。不同平台cpu架构不同,所以不能设计为基于寄存器的
优点:
- 跨平台,指令集小,编译器易实现
缺点:
- 性能下降,实现同样的功能需要更多的指令
内存中的栈与堆:
- 栈时运行时的单位,堆是存储的单位。
- 栈解决程序运行问题,即程序如何运行 、如何处理数据
- 堆解决数据存储问题,即放哪、怎么放
java虚拟机栈:
1、是什么:
- 每个线程创建的时候,都会创建一个虚拟机栈,内部保存一个个的栈帧 (Stack Frame),对应着一次次的java方法调用
- 是线程私有的
2、生命周期:
和线程一致
3、作用
主管java程序运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回
栈的特点:
- 快速有效的分配存储方法,访问速度仅次于程序计数器
- jvm堆java的操作只有两个:
- 每个方法执行,伴随着进栈(入栈、压栈)
- 执行结束后出栈
- 对于栈来说,不存在垃圾回收问题
栈中可能出现的异常:
java虚拟机规范允许java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的java虚拟机栈,那每一个线程的java虚拟机容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机允许的最大容量,java虚拟机就会抛出StackOverflowError异常
- 如果java虚拟机可以动态扩展,并且在尝试扩展的时候无法申请足够的内存,或者在创建新的线程时没有足够的内存区创建对应的虚拟机栈,哪java虚拟机就会抛出OutOfMemoryError异常
设置栈内存的大小:
可以使用-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
栈的存储单位
栈中存什么?
- 每个线程都有自己的栈,栈中的元素都是以栈帧的格式存在
- 线程上每个方法都各自对应一个栈帧。
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
栈运行原理
- jvm栈的操作只有两个,即对栈帧的压栈和出栈
- 在一条活动的线程中,一个时间点上,只会有有一个活动的栈帧,称为当前栈帧,与之对应的方法称当前方法,与之对应的类称当前类
- 执行引擎运行的所有字节码指令都只针对当前栈帧进行操作
- 如果当前方法调用其他方法,就会创建新的栈帧,放在栈的顶部成为新的当前栈帧
- 不同的线程之间的栈帧是不能有相互引用的,即不可能存在一个线程的栈帧引用另一个线程栈帧的情况
- A方法调用了B方法,B方法执行完之后,虚拟机会丢弃B方法对应的栈帧,A方法获得B方法的返回值、并成为当前栈帧
- 两种返回方式:return返回或者出现未处理异常返回,无论哪种都会丢弃栈帧
栈帧
栈帧的内部结构:每个栈帧中存储着:
- 局部变量Local Varables
- 操作数栈Operand Stack(或表达式栈)
- 动态链接Dynamic Linking(或指向运行时常量池的方法引用)
- 方法返回地址Return Address(或方法正常退出或者异常退出的定义)
- 附加信息
1、局部变量表(Local Varables)
- 也被称为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象类型,以及returnAddress(返回值)类型
- 由于局部变量表是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题(不共享)
- 局部变量标是在编译期确定下来的,并保存在方法的code属性maxinum local variable数据项中。在方法运行期间是不会改变局部变量表的大小的
- 最多能够调用方法的次数,由栈的大小决定
- 局部变量表旨在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量也随之销毁
成员变量与局部变量
-
成员变量分为类变量(静态变量)和实例变量,有两次初始化机会:准备阶段(赋零值)、初始化阶段(赋我们定义的值)
-
局部变量,不存在系统初始化过程,意味着使用前必须人为初始化,否则无法使用
局部变量表:
-
在栈帧中,与性能调优关系最为密切的部分就是局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递
-
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
2、操作数栈(Operand Stack)
每一个独立的栈帧中除了包含局部变量表意外,还包含一个后进先出的操作数栈,也可以称之为表达式栈
栈有两种实现方式:基于数组、基于链表,操作数栈基于数组实现
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈、出栈
-
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
-
操作数栈就是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧被创建出来,这个方法的操作数栈是空的
-
每一个操作数栈都会拥有一个明确的占深度用于存储数值,其所需的最大的深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值
-
栈中的任何一个元素都可以是任意的java数据类型
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
-
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
-
如果调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令
-
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
-
另外,我们说java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
栈顶缓存技术
- 操作数存储在内存中,频繁地执行内存读、写此操作影响执行速度,为了解决这个问题,提出了栈顶缓存技术。
- 将栈顶元素缓存在物理cpu寄存器中,以此降低对内存地读写次数,提升执行引擎的执行速度。
3、动态链接(Dynamic Linking)
- 每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法地引用。包含这个引用的目的是为了支持当前方法的代码能够实现动态链接,比如:invokedynamic指令
- 在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件地常量池里。比如:描述一个方法调用另外的其他方法时,就是通过常量池中这些方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为方法的直接引用
常量池的使用就是为了提供一些符合和常量便于指令识别
4、方法返回地址(Return Address)
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将放回值压入调用者栈帧的操作数栈
、设置pc寄存器的值
等,让调用者方法继续执行下去。
方法返回地址
- 存放调用该方法的pc寄存器的值
- 一个方法结束有两种方式:正常执行退出、出现未处理异常退出
- 无论通过那种方式退出,在方法推出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一个指令地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息
当一个方法开始执行后,只有两种方式可以退出这个方法:
- 执行引擎遇到任意一个方法返回的字节码指令(return),会将返回值传递给上层的方法调用者,简称正常完成出口
- 一个方法正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定
- 在字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short、int)、lreturhn(返回值是long)、freturn(返回值是float)、dreturn(返回值是double)、areturn(返回值是引用类型)、return(返回值是void)
- 在方法执行过程中遇到了异常,并且未搜躲到匹配的异常处理器,就会导致方法退出。简称异常完成出口。
- 方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给它的上层调用者产生任何返回值
异常表示例:
- 一个可处理异常的方法
- 对应的字节码:
- jclasslib查看异常表:
5、附加信息
栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息
也可以没有附加信息
Slot(变量槽)
-
参数值的存放总是在局部变量数组的index0开始,到长度-1的索引结束
-
局部变量表,最基本的存储单元是Slot(变量槽)
-
局部变量表存储编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型的变量
-
在局部变量表中,32位以内的只占用一个slot(包括返回值类型),64位的类型(long和double)占用两个slot
- byte、short、char、boolean在存储前转为int,float也只占用一个slot
- long和double占两个slot
-
jvm会位每个slot分配一个访问索引,通过索引可以访问到局部变量表中指定的局部变量值
-
当一个实例方法被调用的时候,它的方法参数和方法内定义的变量会按照顺序被复制到局部变量的每一个slot上
-
如果需要访问局部变量表中一个64bit的局部变量值时,只需使用前一个索引即可(64位的占用两个slot,访问起始索引即可)
-
如果当前帧时由构造方法或者实例方法创建的(非静态的),那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数顺粗继续排列,例如:
存在一个局部变量:
-
局部变量表中的槽位是可重复利用。如果一个局部变量过了作用域,新的变量可以服用该局部变量的槽位,达到节省资源的目的,例如:
发现上图中变量d存储在变量l存储的位置:
方法调用
将符号引用转化为调用方法的直接引用,两种方式:
- 静态链接:调用的方法在编译期就能确定。例如:super(),this()等等
- 动态链接:调用的方法,编译期无法确定,在运行期才转化为直接引用。例如,调用接口(如List)的方法,我们不知道用的哪个实现类(是ArrayList还是LinkedList还是其他的),只有运行期才能确定
对应的绑定机制为早期绑定和晚期绑定:
- 早期绑定,编译期可知,且运行期不变
- 晚期绑定:编译期无法确定,只能在运行期根据实际类型确定相关的方法
虚方法与非虚方法
- 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法
- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法
- 其他方法为虚方法
虚拟机中提供了以下几条方法调用指令:
- 普通调用指令:(固化在虚拟机内部,方法的调用执行不可人为干预)
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
- 动态调用指令:(支持由用户确定方法版本)
- invokedynamic:动态解析出要调用的方法,然后执行
其中,invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法(因为可能被子类重写,不知道调用的是自己的还是子类的方法)
动态类型语言和静态类型语言:
- 区别在于对类型的检查是在编译期还是运行期,满足前者就是静态类型语言,反之是动态类型语言
- 静态类型语言是判断变量的类型信息。动态语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息
方法重写的本质:
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C
- 如果在类型C中找到与常量中的描述符、简单名称都都相符的方法,则进行访问权限校验,如果通过则返回这个方法的的直接引用,查找过程结束;否则返回java.lang.IllegalAccessError异常
- 否则,按照继承关系从下往上依次对C的父类进行第二部的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
虚方法表:(每个虚方法调用哪个类的)
- 为了提升性能,jvm采用在类的方法区建立一个虚方法表(非虚方法不会出现在表中)来实现,使用索引表阿里代替查找
- 每个类中都有一个虚方法表,表中存放着各个方法的实际入口
- 虚方法表在类加载的链接的解析阶段创建并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方法表也初始化完毕
面试题
注意:以下答案可能不完整
举例栈溢出的情况:
- 栈溢出即StackOverflowError
- 栈帧过多导致溢出(递归调用未设置结束条件,导致栈帧不断放入栈,无法释放)
- 栈帧过大导致溢出
解决:
- 使用参数 -Xss 去调整JVM栈的大小
- 动态分配
调整栈大小,就能保证不出现溢出吗?
- 不是
- 固定栈内存,则只要一直递归,总会出现栈溢出
- 动态分配,最终可能会OOM(OutOfMemoryError)
分配的栈内存越大越好吗?
- 不是
- 资源浪费
- 栈内存过大,会使线程数变少,会影响运行效率
垃圾回收是否会涉及到虚拟机栈?
- 不涉及
- 每个方法在出栈后自动释放;垃圾回收针对堆内存中的无用对象进行回收。
方法中定义的局部变量是否线程安全?
- 视情况而定
- 如果方法局部变量没有逃离方法的作用范围,它是线程安全的;
- 如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全;例如:入参或返回值是StringBuilder,而StringBuilder是线程不安全的,可能会被其他线程修改,此时就是线程不安全的
本地方法栈
概念
-
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于
管理本地方法的调用
。 -
本地方法栈,也是线程私有的。
-
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的),如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 stackoverflowError异常。
-
如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError 异常。
-
本地方法是使用c语言实现的。
-
它的具体做法是Native Method stack中登记natilve方法,在Execution Engine执行时加载本地方法库。
-
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
- 它甚至可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存。
-
并不是所有的JVM都支持本地方法。
-
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
本地方法接口
简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如c。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在c++中,你可以用extern "c"告知C++编译器去调用一个c的函数。
"A native method is a Java method whose implementation isprovided by non-java code . "
在定义一个native method时,并不提供实现体(有些像定义一个Javainterface),因为其实现体是由非java语言在外面实现的。
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
标识符native可以与所有其它的java标识符(public、private等等)连用,但是abstract除外。
为什么要使用Native Method ?
Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
-
与Java环境外交互:
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。 -
与操作系统交互:
通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。 -
Sun 's Java
sun的解释器是用c实现的,这使得它能像一些普通的c一样与外部交互。
现状:目前该方法使用的越来越少了,除非是与硬件有关的应用
堆(heap)
概念
-
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
- 堆内存的大小是可以调节的。
-Xms10m -Xmx10m
表示初始堆空间10M,最大堆空间10M
- 堆内存的大小是可以调节的。
-
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
-
所有的线程共享Java堆,在这里还可以划分
线程私有
的缓冲区( ThreadLocal Allocation Buffer,TLAB)。- 所有的堆空间都是所有线程共享的吗?不是
-
对象实例、数组在堆分配
-
堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
-
堆是GC(garbage collection,垃圾收集器)执行垃圾回收的重点区域
堆的内存细分
设置堆内存
1.设置堆空间大小的参数
-Xms
:用来设置堆空间(年轻代+老年代)的初始内存大小- -X是jvm的运行参数
- ms 是memory start
-Xmx
:用来设置堆空间(年轻代+老年代)的最大内存大小- 单位:无(默认字节)、k、m、g,官网:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html
2.默认堆空间的大小
- 初始内存大小:
物理电脑内存大小 / 64
- 最大内存大小:
物理电脑内存大小 / 4
3.手动设置:-Xms60m -Xmx60m
,开发中建议将初始堆内存和最大的堆内存设置成相同的值,防止频繁扩容。
4、查看设置的参数:
-
方式一:
jps
jstat -gc 进程id
- 上图中字母:C - Capacity,U - used
-
方式二:添加运行时参数:
-XX:+PrintGCDetails
运行结果:
OOM
java.lang.OutOfMemoryError
,堆内存被占满,想办法一直往堆里面添加东西即可。我们知道对象、数组放在堆中,因此,可用这么干:
public static void main(String[] args) {
List<Date> list = new ArrayList<>();
while (true){
list.add(new Date());
}
}
最终:就oom了
年轻代与老年代
参数:
- 设置老年代与新生代的比例
-XX:NewRatio=2
是指:老年代 / 新生代 = 2
,默认2
- 设置Eden和两个survivor空间的比例:
-XX:SurvivorRatio=4
:表示eden / survivor = 4
,默认8
-XX:+UseAdaptiveSizePolicy
,开启自适应的内存分配策略(默认开启),如果是-XX:-UseAdaptiveSizePolicy
表示关闭-Xmn256m
,设置新生代的大小未256M,一般不指定
对象分配
一般过程:
- new的对象先放Eden。此区有大小限制。
- 当Eden空间填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(Minor GC),将Eden区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到Eden区
- 然后将Eden中的剩余对象移动到幸存者0区。
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 啥时候能去养老区呢?可以设置次数。默认是15次。
可以设置参数:-XX:MaxTenuringThreshold=<N>
进行设置。 - 在养老区,相对悠闲。当养老区内存不足时,再次触友GC: Major GC,进行养老区的内存清理。
- 若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常:java. lang. OutOfMemoryError: Java heap space
幸存者区满了,不会触发Minor GC;幸存者区只是伴随着Eden区进行Minor GC
常用调优工具
Jprofiler:
MinorGC、MajorGC和FullGC的对比
年轻代
老年代
Full GC:
TLAB
对象分配过程:
常用参数设置
官网:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html
分为以下三种参数:
-
标配参数
- 不随jdk版本变化的参数
-version
-help
java -showversion
-
X参数(了解)
-Xint
:解释执行-Xcomp
:第一次使用就编译成本地代码-Xmixed
:混合模式
-
XX参数(包括Xms、Xmx等,类似于起了别名)
- Boolean类型,-XX:+/-参数,如:
-XX:+PrintFlagsInitial
- KV类型,-XX:参数名=参数值,如:
-XX:MetaspaceSize=21m
- Boolean类型,-XX:+/-参数,如:
XX参数:
-XX:+PrintFlagsInitial
:查看所有的参数的默认初始值(示例在本节最后面)
-XX:+PrintFlagsFinal
:查看所有的参数的最终值(可能会存在修改,不再是初始值;=
表示初始值,:=
表示修改过,修改后的值)
-Xms
:初始堆空间内存(默认为物理内存的1/64),等价于-XX:InitialHeapSize
-Xmx
:最大堆空间内存(默认为物理内存的1/4),等价于-XX:MaxHeapSize
-Xss
:设置单个线程栈的大小,一般为512k~1024k;等价于-XX:ThreadStackSize
-Xmn
:设置新生代的大小。(初始值及最大值)
-XX:MetaspaceSize
:设置元空间大小
-XX:NewRatio
:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio
:设置新生代中Eden和so/s1空间的比例
-XX:MaxTenuringThreshold
:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails
:输出详细的Gc处理日志
打印gc简要信息:1、-XX:+PrintGC
2、-verbose:gc
-XX:HandlePromotionFailure
:是否设置空间分配担保
查看运行中的java程序参数设置:
- jps查进程id
- jinfo -flag 参数名 进程id
关于分配担保:
- 只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
JDK7前后HandlerPromotionFailure参数的变化
查看初始默认参数:
-
java -XX:+PrintFlagsInitial
初始化参数
-
java -XX:+PrintFlagsFinal
用于查看修改后的参数(=
表示默认,:=
表示修改过,修改后的值;但jdk17的话,没有使用=
和:=
,而是在最后面加了一个参数(default、command line)用来表示是默认值还是修改了的值)- jdk 8:
- jdk17:
- jdk 8:
-
java -XX:+PrintCommandLineFlags
打印命令行参数
逃逸分析
- 随着JIT编译期的发展与逃逸分析技术逐渐成熟,
栈上分配、标量替换优化技术
将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。 - 如果经过
逃逸分析
(Escape Analysis)后发现,一个对象并没有逃逸出方法(不会在方法之外使用)
的话,那么就可能被优化成栈上分配。 - JDK7之前可通过
-XX:+DoEscapeAnalysis
开启逃逸分析,jdk8之后默认开启
结论:
开发中能使用局部变量的,就不要使用在方法外定义。
代码优化:
方法区
概念
设置、查看方法区大小:
- 设置初始大小为21M:
-XX:MetaspaceSize=21m
- 设置最大大小:
-XX:MaxMetaspaceSize=256m
- 查看:
- 运行程序
jps
查看进程idjinfo -flag MaxMetaspaceSize 进程id
JDK8以后:
JDK7之前:
如何解决OOM?
方法区存放了哪些内容
类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息
运行时常量池:
静态变量
方法信息
域信息
为什么用常量池:
方法区的演进
jdk1.6、jdk1.7、jdk1.8 方法区的演进
为什么永久代要被元空间替换
StringTable为什么要调整
jdk7中将stringTable放到了堆空间中。因为永久代的回收效率很低
,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。
这就导致stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存
静态变量存在哪
JDK7及之后:
JDK 7及其以后版本的HotSpot虚拟机选择把静态变量 与 类型在Java语言一端的映射class对象
存放在一起,存储于Java堆
之中,例如上图中的staticObj
方法区的垃圾回收
- 要不要回收?规范没规定一定要回收
- 回收什么?常量池中废弃的常量(相对容易)和不再使用的类型(条件比较苛刻)
对象实例化
创建对象的方式
创建对象的步骤
堆中对象内存布局
对象访问方式
句柄访问
- 占用空间
- 好处是,对象实例数据地址改变,reference不用变,只用改变句柄数据
直接访问(Hotspot)
直接内存
概述
执行引擎
重点:
-
两种执行方式:解释器、即时(Just In Time,JIT)编译器,即时编译器又分C1和C2
-
优缺点
- 解释器:程序运行,立即发挥作用,省去编译时间
- 即时编译器:先要编译成本地代码,需要一定时间;但是,编译完成后执行效率高
概述
执行引擎
解释器、即时编译器
解释器与即时编译器的切换:
优缺点
JIT的分类:
机器码、指令、汇编语言、高级语言
汇编语言
高级语言
String
基本特性
StringTable长度
-
例如jdk8中查看默认值:
-
例如再jdk8中设置1000,有如下错误:
String的内存分配
- jdk6及之前放在永久代
- jdk7及以后放在堆空间
- 字符串常量池不会存放相容内容的字符串
字符串常量池不会存放相容内容的字符串:例如,如下代码,通过debug调试,看内存中String的个数变化:
@Test
public void testSameString(){
String s1 = "abc";//6201个String
String s2 = "abcdef";//6202个String
String s3 = "abc";//6202个String,因为已经有abc了
}
字符串拼接操作
5. 字符串拼接不一定使用StringBuilder,当两边都是final变量时,也会再编译期优化
例子:
@Test
public void testStringAdd() {
String s1 = "abc";
final String finalS1 = "abc";
String s2 = "def";
final String finalS2 = "def";
String s3 = "abcdef";
String add = "abc" + "def";//常量拼接
String finalAdd = finalS1 + finalS2;//常量拼接
String s1AddS2 = s1 + s2;//含变量的拼接
System.out.println(s3 == add);//true
System.out.println(s3 == finalAdd);//true
System.out.println(s3 == s1AddS2);//false
}
intern的使用
概述
面试题:
问题一:
答案:
- jdk6中分别为:false、false;
- jdk7/8中分别为:false、true
解析:
- String s = new String(“1”)
- 两个对象:new的String对象、字符串常量池中的“1”,也就是常量池通过此步后已经存在“1”了
- String s3 = new String(“1”) + new String(“1”)
- 首先会创建StringBuilder,因为要拼接字符串
- “1”在常量池已经存在
- 创建两个String对象
- StringBuilder.toString方法内部会new String(“11”)创建对象
重点
:字符串常量池没有“11”;那么下一步s3.intern方法就会在常量池创建“11”;在jdk6中,直接在永久代常量池放一个"11";而jdk7/8是在堆的常量池创建一个 指向刚刚new String(“11”)地址 的量
问题二:
答案及解析:
扩展:
问题三:
答案及解析:
结论:
对于程序中大量存在存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以节省内存空间。
G1的String去重操作
垃圾回收
概述
什么是垃圾:
好处
坏处
垃圾标记阶段
引用计数法(java没用这个算法)
循环应用:A的同学有B,B的同学有A,循环引用
可达性算法(或根搜索算法、追踪性垃圾收集)
- 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地
解决在引用计数算法中循环引用的问题,防止内存泄漏的发生
。 - 相较于引用计数算法,这里的可达性分析就是Java、c#选择的。这种类型的垃圾收集通常也叫作
追踪性垃圾收集
(Tracing Garbage
collection) 。
"GC Roots"根集合是一组必须活跃的引用。
基本思路:
- 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下
的方式搜索被根对象集合所连接的目标对象是否可达。 - 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间
接连接着,搜索所走过的路径称为引用链(Reference Chain) - 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象
才是存活对象。
在Java语言中,GC Roots包括以下几类元素:(熟悉前四种即可)
- 虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 本地方法栈内JNI(通常说的本地方法)引用的对象
- 方法区中类静态属性引用的对象
比如:Java类的引用类型静态变量
- 方法区中常量引用的对象
比如:字符串常量池(stringTable)里的引用、static final修饰的常量
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用。
基本数据类型对应的class对象,一些常驻的异常对象(如:NullPointerException、outofMemoryError),系统类加载器。
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
finalization
例子:
public class Kill {
public static Kill kill;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("刀下留人...");
kill = this;//自救
}
public static void main(String[] args) {
kill = new Kill();
System.out.println("斩!");
// ----------- 第一次自救,成功 ---------------
//垃圾回收器回收
kill = null;
System.out.println("准备GC...");
System.gc();
// finalizer线程优先级低,等待几秒
try {
Thread.sleep(2_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (kill != null) {
System.out.println("没有被杀死!");
}else {
System.out.println("已经被斩杀!");
}
// -------------- 第二次自救,失败,finalize方法只会执行一次 ----------
// 流程同上
kill = null;
System.out.println("准备第二次GC...");
System.gc();
if (kill != null) {
System.out.println("没有被杀死!");
}else {
System.out.println("已经被斩杀!");
}
}
}
结果:
清除阶段
标记清除算法(Mark - Sweep)
标记的可达对象,想象一下,如果要标记不可达对象,为何不直接清除?!
复制算法(Copying)
将存活对象复制到另一个内存块中
标记-压缩算法(Mark - Compact)
标记压缩算法后,可以使用指针碰撞分配新的地址;而标记清除算法之后,要用空闲列表来分配新地址
总结
三种算法对比
分代收集算法
增量收集算法
分区算法
相关概念
System.gc()
内存溢出(OOM)
内存泄漏
STW (Stop The World)
并发、并行
并发:
并行:
安全点与安全区域
(了解)
安全点:
安全区域:
引用
强引用
- 强引用可以直接访问目标对象。
- 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出
OOM异常,也不会回收强引用所指向对象。- 强引用可能导致内存泄漏。
软引用
public static void main(String[] args) throws InterruptedException {
SoftReference<Object> objectSoftReference = new SoftReference<>(new Object());
// 第一次打印
System.out.println(objectSoftReference.get());
// 进行一次gc后打印
System.gc();
Thread.sleep(1000 * 2);
System.out.println(objectSoftReference.get());
//制造OOM,设置参数:-Xms1m -Xmx1m,再打印
try {
// 需要1M空间,于是会OOM
byte[] bytes = new byte[1024 * 1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(objectSoftReference.get());//null
}
}
结果是:前两次都能获取到对象,最后一次获取到为null
弱引用
public static void main(String[] args) throws InterruptedException {
WeakReference<Object> objectSoftReference = new WeakReference<>(new Object());
// 第一次打印
System.out.println(objectSoftReference.get());
// 进行一次gc后打印
System.gc();
Thread.sleep(1000 * 2);
System.out.println(objectSoftReference.get());//null
}
结果:第一次能获取到对象,第二次为null
虚引用
终结器引用
垃圾回收器
分类
按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。
性能指标
吞吐量、暂停时间、内存占用
吞吐量、暂停时间
-
吞吐量:
-
暂停时间
-
对比
总结:在最大吞吐量优先的情况下,降低暂停时间
经典垃圾回收器
即:
Serial收集器:
ParNew收集器:
Parallel Scavenge收集器:
CMS收集器
小结
G1回收器
概念
为什么叫G1?
特点:
参数
使用场景
分区
回收过程
- 年轻代
- 并发标记过程
- 混合回收
- Full GC
优化建议
七种经典垃圾回收算法总结
如何选择?
ZGC
使用:-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
GC日志分析
参数:
-XX:+PrintGCDetails
输出参数及解释:
例如有如下输出结果:
[GC (System.gc()) [PSYoungGen: 2663K->840K(38400K)] 2663K->848K(125952K), 0.0035334 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 840K->0K(38400K)] [ParOldGen: 8K->706K(87552K)] 848K->706K(125952K), [Metaspace: 3328K->3328K(1056768K)], 0.0055251 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 38400K, used 998K [0x00000000d5b80000, 0x00000000d8600000, 0x0000000100000000)
eden space 33280K, 3% used [0x00000000d5b80000,0x00000000d5c79b70,0x00000000d7c00000)
from space 5120K, 0% used [0x00000000d7c00000,0x00000000d7c00000,0x00000000d8100000)
to space 5120K, 0% used [0x00000000d8100000,0x00000000d8100000,0x00000000d8600000)
ParOldGen total 87552K, used 706K [0x0000000081200000, 0x0000000086780000, 0x00000000d5b80000)
object space 87552K, 0% used [0x0000000081200000,0x00000000812b0b90,0x0000000086780000)
Metaspace used 3341K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
参数解释:
GC
、Full GC
等 : GC的类型System.gc()
、Allocation Failure
(内存分配失败) 等 : GC原因PSYoungGen
: 使用了Parallel Scavenge并行垃圾收集器的新生代GC前后大小的变化ParoldGen
: 使用了Parallel Old并行垃圾收集器的老年代GC前后大小的变化Metaspace
: 元数据区cc前后大小的变化,JDK1.8中引入了元数据区以替代永久代xxx secs
: 指GC花费的时间Times
:user
: 指的是垃圾收集器花费的所有CPU时间sys
: 花费在等待系统调用或系统事件的时间real
: GC从开始到结束的时间,包括其他进程占用时间片的实际时间
导出GC日志
通过添加参数-Xloggc:路径
来指定导出GC日志,如果有多级目录需要事先创建目录。
例如:-Xloggc:./gc.log
,表示导出GC日志到当前项目下的gc.log
GCEasy是在线分析的:
- 网址:https://gceasy.io/
- 选择文件、分析即可
更多推荐
所有评论(0)