一、JVM简述
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
二、JRE/JDK/JVM是什么关系
**JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。**所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
**JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。**JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
**JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。**它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
如下图,可从Java平台的逻辑结构上来了解JVM、JDK、JRE、JavaSE:
三、JVM原理
JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。
Java程序运行机制步骤
- 首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
- 再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
- 运行字节码的工作是由解释器(java命令)来完成的。
从上图可以看,java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。
其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
四、JVM体系结构
1.体系结构简述
JVM包含两个子系统和两个组件,两个子系统为Class loader Subsystem(类加载器子系统)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地库接口)。
- Class loaderSubsystem(类加载器子系统):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
- Execution engine(执行引擎):执行classes中的指令。
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
工作流程 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
2.类加载机制
Java类加载机制就是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
2.1类加载方式
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
2.2类加载器
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
类加载器分为启动类加载器,扩展类加载器,应用程序类加载器,自定义类加载器。各种类加载器之间存在着逻辑上的父子关系,但不是真正意义上的父子关系,因为它们直接没有从属关系。除了启动类加载器(Bootstrap ClassLoader)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承自类java.lang.ClassLoader。
- 启动类加载器(BootstrapClassLoader):负责加载$JAVA_HOME/jre/lib目录下的核心类库,比如rt.jar,charsets.jar,用来加载java核心类库,无法被java程序直接引用;
- 扩展类加载器(ExtClassLoader):负责加载支撑J$JAVA_HOME/jre/lib/ext目录下的JAR类包。父加载器是启动类加载器。用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类;
- 应用类加载器(AppClassLoader):负责加载ClassPath路径下的类包,主要就是加载我们自己写的那些类。父加载器是扩展类加载器。即根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
- 自定义类加载器(CustomClassLoader):负责加载用户自定义目录下的类包。父加载器是应用类加载器,即继承 java.lang.ClassLoader类的方式实现。
2.3类加载过程
java类加载分为5个过程,加载–>验证–>准备–>解析–>初始化。这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。
- 加载:将字节码从不同的数据源(可能是 class 文件,也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;并在堆中生成一个代表该类的 java.lang.Class 对象,作为对方法区这个类的各种数据的访问入口。
- 验证:验证的目的是为了确保加载进来的class文件符合JVM的规范,一般是进行文件格式的验证、元数据的验证、字节码验证和符号引用验证。
文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
元数据的验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。 - 准备:给类的静态变量分配空间,并赋予默认值。
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块。
2.4双亲委派机制
双亲委派机制就是当某个类加载器收到加载类的请求,如果这个类没有被加载过,该类加载器不会直接加载,会先为委派给父加载器,如果父加载器没有加载过,依次往上传递,直到顶层启动类加载器。如果父加载器可以完成加载任务,则父加载器加载返回;如果父加载器不能完成加载任务,才会自己去进行加载。一句话概述双亲委派机制加载流程就是,从下往上检查类是否已经被加载,从上往下尝试去加载。
- 双亲委派机制的优点:
沙箱安全机制:避免核心API被篡改。自己写的java.lang.String.class类不会被加载。
避免重复加载:如果父加载器已经加载过该类,子类加载器就没有必要再去加载。 - 双亲委派机制加载类的核心代码
ClassLoader类的loadClass()方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先会检查该类是否已经被本类加载器加载,如果已经被加载则直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
// 如果没有被加载,则委托父加载器去加载
//加入Java开发交流君样:593142328一起吹水聊天
long t0 = System.nanoTime();
try {
if (parent != null) {
// 让父加载器对象去调用loadClass方法
c = parent.loadClass(name, false);
} else {
// parent==null,说明父加载器是启动类加载器。启动类加载器是C++编写的,这里去调用本地方法区尝试加载该类。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 如果父加载器没有加载到该类,则自己去加载。这里会调用URLClassLoader类的findClass()方法
//加入Java开发交流君样:593142328一起吹水聊天
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
2.5全盘负责委托机制
全盘负责委托机制就是当一个Classloader加载一个Class的时候,这个Class所依赖的和引用的其它Class通常也由这个Classloader负责加载。
2.6打破双亲委派机制
打破双亲委派机制就是我们希望自定义类加载器去直接加载指定类,而不是先委托父加载器去加载或者是自定义类加载器加载不到才让父加载器去进行加载。
2.7自定义类加载器实现
自定义类加载器实现思路:
如果使用双亲委派机制就是重写findClass()方法(类加载器具体去加载类的方法),
如果要打破双亲委派机制,在重写findClass()方法基础上,还需要重新loadClass()方法,这里我们可以改写逻辑,先让该类加载器去加载类,加载不到再让父加载器去进行加载。
- loadClass(String name,boolean resolve):根据指定的二进制名称加载类
- findClass(String name): 根据二进制名称来查找类
- 直接使用或继承已有的ClassLoader实现:java.net.URLClassLoader、java.security.SecureClassLoader、 java.rmi.server.RMIClassLoader
- 在调用loadClass(),会先根据委派模型在父加载器中加载,如果加载失败,则会调用自己的findClass方法来完成加载
3.JVM 运行时数据区
(1)概述
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:
-
程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
-
Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
-
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
-
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。
-
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
(2).堆和栈的区别
https://blog.csdn.net/qq_35275233/article/details/87863137#:~:text=%E5%9C%A8%E6%95%B4%E4%B8%AA%E7%A8%8B%E5%BA%8F%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E4%B8%AD%EF%BC%8CJVM%E4%BC%9A%E7%94%A8%E4%B8%80%E6%AE%B5%E7%A9%BA%E9%97%B4%E6%9D%A5%E5%AD%98%E5%82%A8%E7%A8%8B%E5%BA%8F%E6%89%A7%E8%A1%8C%E6%9C%9F%E9%97%B4%E9%9C%80%E8%A6%81%E7%94%A8%E5%88%B0%E7%9A%84%E6%95%B0%E6%8D%AE%E5%92%8C%E7%9B%B8%E5%85%B3%E4%BF%A1%E6%81%AF%EF%BC%8C%E8%BF%99%E6%AE%B5%E7%A9%BA%E9%97%B4%E4%B8%80%E8%88%AC%E8%A2%AB%E7%A7%B0%E4%BD%9C%E4%B8%BARuntime%20Data%20Area%EF%BC%88%20%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%20%EF%BC%89%EF%BC%8C%E4%B9%9F%E5%B0%B1%E6%98%AF%E6%88%91%E4%BB%AC%E5%B8%B8%E8%AF%B4%E7%9A%84JVM%E5%86%85%E5%AD%98,1.%E6%A0%88%20%28stack%29%E4%B8%8E%E5%A0%86%20%28heap%29%20%E9%83%BD%E6%98%AFJava%E7%94%A8%E6%9D%A5%E5%9C%A8Ram%20%28%E9%9A%8F%E6%9C%BA%E5%AD%98%E5%82%A8%E5%86%85%E5%AD%98%29%E4%B8%AD%E5%AD%98%E6%94%BE%E6%95%B0%E6%8D%AE%E7%9A%84%E5%9C%B0%E6%96%B9%E3%80%82
-
物理地址
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。 -
内存分别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。 -
存放内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
PS:
静态变量放在方法区
静态的对象还是放在堆。 -
程序的可见度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
(3)深拷贝与浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象
4.垃圾回收机制
4.1垃圾回收机制概述
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
4.2 GC介绍
(1)GC概述
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存
回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动
回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
(2)GC对象判定方法
- 引用计数法:为每个对象创建一个引用计数器,有对象引用时计数器+1,引用被释放时计数器-1,当计数器为0时就可以被回收。弊端:A->B,B->A,那么 AB 将永远不会被回收了,也就是引用有环的情况。
- 可达性算法(引用链法):从GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是可以被回收的。Roots 包括:java 虚拟机栈中引用的对象,本地方法栈中引用的对象,方法区中常量引用的对象,方法区中静态属性引用的对象。在Java语言里,可作为GC Roots的对象包括以下几种:
虚拟机栈(栈帧中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)的引用的对象。
4.3垃圾回收算法
(1)标记-清除算法
标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,标记无用对象,然后进行清除回收。它将垃圾收集分为两个阶段:
**标记阶段:**标记出可以回收的对象。
**清除阶段:**回收被标记的对象所占用的空间。
标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。
- 优点:实现简单,不需要对象进行移动。
- 缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
标记-清除算法的执行的过程如下图所示
(2)复制算法
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
- 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
- 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
复制算法的执行过程如下图所示
(3)标记-整理算法
标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。
- 优点:解决了标记-清理算法存在的内存碎片问题。
- 缺点:仍需要进行局部对象移动,一定程度上降低了效率。
标记-整理算法的执行过程如下图所示
(4)分代算法
根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
- 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
· 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
· 清空 Eden 和 From Survivor 分区;
· From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
· 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
- 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
4.4 垃圾收集器
(1)垃圾收集器概述
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
- Serial 收集器(标记—复制算法):新生代单线程收集器,标记和清理都是单线程,[优点是简单高效]
- ParNew 收集器(标记—复制算法):新生代并行收集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
- Parallel Scavenge 收集器(标记—复制算法):新生代并行收集器,追求高吞吐量,高效利用CPU。吞吐量=用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景。
- Serial Old 收集器(标记—整理算法):老年代单线程收集器,Serial收集器的老年代版本。
- Parallel Old 收集器(标记—整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本。
- https://blog.csdn.net/syilt/article/details/107525396
- https://cloud.tencent.com/developer/article/1614156
- Concurrent Mark Sweep(CMS)收集器(标记—清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。 - Garbage First(G1)收集器(标记—整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记—整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或者老年代。
(2)新生代垃圾回收器和老年代垃圾回收器
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
4.5 MinorGC&FullGC
(1)概述
- Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快,一般采用复制-回收算法
- Full GC/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,所采用的是标记-清除算法。Full GC 的速度通常会比 Minor GC 慢 10 倍以上。
(2)触发GC的条件
除直接调用System.gc外,触发Full GC执行的情况有如下四种。
-
旧生代空间不足
老生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 -
Permanet Generation空间满
PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation
可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 -
CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行老生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入老生代,而此时老生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老生代,而此时老生代空间不足造成的。
应对措施为:增大survivorspace、老生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。 -
统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc
4.7内存分配策略
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值对象进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC
4.8Java 中都有哪些引用类型
1)强引用
我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
2)软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
- 用处: 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
如下代码:
Browser prev = new Browser(); // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null){
rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
}else{
prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference(prev); // 重新构建
}
3)弱引用
具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中
String str=new String("abc");
WeakReference<String> abcWeakRef = new WeakReference<String>(str);
str=null;
等价于
str = null;
System.gc();
4)虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,被回收时会收到一个系统通知。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
作者:MigrationUK
链接:https://www.jianshu.com/p/54eb60cfa7bd
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
5.JVM调优机制
5.1调优工具
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。第三方有:MAT(Memory AnalyzerTool)、GChisto。
jconsole:Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对 JVM 中的内存、线程和类等进行监控;
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等;
MAT:Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Javaheap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗;
GChisto:一款专业分析gc日志的工具
5.2调优参数
1)堆栈配置相关
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=0
-Xmx3550m: 最大堆大小为3550m。
-Xms3550m: 设置初始堆大小为3550m。
-Xmn2g: 设置年轻代大小为2g。
-Xss128k: 每个线程的堆栈大小为128k。
-XX:MaxPermSize: 设置持久代大小为16m
-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。
-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
2)垃圾收集器相关
-XX:+UseParallelGC-XX:ParallelGCThreads=20-XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5-XX:+UseCMSCompactAtFullCollection:
-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20: 配置并行收集器的线程数
-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
3)辅助信息相关
-XX:+PrintGC-XX:+PrintGCDetails
-XX:+PrintGC 开启打印 gc 信息,输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 打印 gc 详细信息,输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs
5.3调优命令
Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
- jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
- jstat:JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
- jmap:JVM Memory Map命令用于生成heap dump文件
- jhat:JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
- jstack:用于生成java虚拟机当前时刻的线程快照。
- jinfo:JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数
5.4 打出线程栈信息
思路:jps,top ,jstack这几个命令,再配合一次排查线上问题进行解答
- 输入jps,获得进程号。
- top -Hp pid 获取本进程中所有线程的CPU耗时性能
- jstack pid命令查看当前java进程的堆栈状态
- 或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件。
- 可以使用fastthread 堆栈定位,fastthread.io/
六、参考资料
1.https://baijiahao.baidu.com/s?id=1605937053950156833&wfr=spider&for=pc
2.https://blog.csdn.net/qq_43658155/article/details/105471859
3.https://www.jianshu.com/p/54eb60cfa7bd
4.https://blog.csdn.net/crazymakercircle/article/details/113587338
5.https://blog.csdn.net/wj1314250/article/details/118974620?utm_medium=distribute.pc_feed_v2.none-task-blog-personrec_tag-10.pc_personrecdepth_1-utm_source=distribute.pc_feed_v2.none-task-blog-personrec_tag-10.pc_personrec