什么是JVM?
Java虚拟机。执行java字节码(二进制的形式)的虚拟计算机。
1)编写:编写代码,形成.java后缀的源文件;
2)编译:通过编译器(javac命令)进行错误排查后,编译生成以.class为后缀名的字节码文件,即二进制文件;
3)运行:获取二进制文件后,通过解释器(javap命令),根据不同的系统编译成相应的机器代码。
内部结构
类装载器
加载所有的类,被载入内存中的类生成一个java.lang.Class实例对象。一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
其中类加载器分三类:
- 启动类加载器:用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar 、resources.jar 、charsets.jar等 jar 包和类))以及被 -Xbootclasspath参数指定的路径下的所有类。
- 扩展类加载器: 加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
- 应用程序类加载器:也为系统类加载器,加载用户类路径(Classpath)上所指定的类库(用于存放应用程序类的路径)。
加载机制
1)全盘负责:加载当前的类所有引用及依赖都由这个类加载器加载(比如创建A类,在A类中声明匿名类部类B,A跟B都是由同一个类加载器加载)。
2)双亲委派:首先不会自己去加载这个请求,会委托给父加载器,如果父加载器也找不到,才会自己去加载。
3)缓存机制: 会保证所有加载过的类都被缓存,当程序需要使用该类时,先从缓存区获取,缓存区 不存在时,系统才去加载该类并存入缓存区。
运行时数据区
- 堆:虚拟机中只有一个堆,所有的线程都共享他。存放对象和数组;
- 栈:每个线程有独立的线程栈(特性:先进后出),存放栈帧,每个栈帧对应一个被调用的方法,执行一个方法就创建一个栈帧并压栈,方法执行完就出栈,栈帧:局部变量表(方法入参以及方法内部声明的变量)、方法返回地址(被调用方法完成后的指向)、动态连接等;
- 方法区:虚拟机中只有一个方法区,被所有线程共享,如:类信息、常量、静态变量(jdk1.8开始存在方法区,jdk1.7存在堆中);
- 本地方法栈:和Java栈作用相同,栈运行方法,本地方法栈运行本地方法;
- 程序计数器:用于存储当前指令地址,控制指令顺序执行。即线程丢失CPU执行权后,再次获取都执行权时,程序计数器使线程回到之前执行位置继续执行;
- PC寄存器:用于保存程序计数器的值。即线程执行丢失CPU执行权,保存当前线程执行位置、状态等信息。
执行引擎
执行字节码或本地方法。即根据PC寄存器调配的指令顺序,依次执行,将字节码解析/编译为对应运行系统机器指令。
垃圾回收
什么是垃圾?
可以被销毁,占用空间可被回收的,不可能再被任何途径使用的对象。
垃圾判断回收算法
-
引用计数法
假设堆中每个对象都有一个引用计数器,对象被创建赋值后,计数器+1,反之-1,如果计数器为0时,则为清除对象。优点:实现简单,程序不被长时间打断的实时环境比较有利。 缺点:需要额外空间存储计数器,难以检测对象之间的循环引用。
-
可达性分析法
也称根搜索法,可达性指,对象被一个程序中的变量直接或间接的方式被其他可达的对象引用,称该对象是可达的。(引用如果能够达到GCRoot(活跃对象,例如静态变量、未死亡的线程)就代表这个引用是可达)。优点:可解决循环引用问题,不需要占用额外空间。 缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用。
什么是引用?
一个抽象的概念或者是一个定义,功能就是通过引用对实例实现操作。如:看电视用遥控器换自己喜欢的电视台,遥控器就是一个引用。
强度比较:强引用>软引用>弱引用>虚引用
- 强引用:如Object obj = new Object(),这类引用是 Java 程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用:描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2 之后提供了SoftReference类来实现软引用。
- 弱引用:描述非必须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK1.2 之后,提供了WeakReference类来实现弱引用。
- 虚引用:也称幻引用,最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2 之后提供了PhantomReference类来实现虚引用。
垃圾回收算法
-
标记清除算法
通过对象根节点,标记所有从根节点可达的对象,未被标记的对象就是垃圾对象,清除所有未标记对象。优点:不需要进行对象移动,只对存活对象进行处理,存活对象较多情况下高效。 缺点:标记、清除效率不高,需要一个空闲列表记录所有空闲区域以及大小,对空闲列表需要管理,标记清除后会产生大量内存碎片。
-
标记整理算法(老年代回收算法)
标记所有存活的对象,按内存地址顺序排列移动所有存活对象,将末端内存地址以后的内存全部回收。优点:经整理后,新对象分配只需要通过指针碰撞完成,空闲区域位置是可知的,不存在内存碎片问题。 缺点:GC暂停时间长,需要拷贝对象到一个新的地方,还要更新内存地址。
-
复制算法(新生代回收算法)
将内存空间一分为二,每次只使用一块,将使用中内存里的存活对象拷贝到未使用的内存中,清除使用中内存所有对象,最后交换两个内存角度。优点:标记、复制阶段可同时进行,只对一块内存进行回收,运行高效,不存在内存碎片问题。 缺点:内存空间区域小。
-
分代收集算法
1)新生代:所有创建的对象都在eden区,当eden区内存已满,使用复制算法进行一次GC;
2)老年代:当新生代存活对象达到一定年龄后(一次GC算一岁),会进入到老年代(默认15次GC,但可以自定义),当老年代内存已满,触发GC,使用标记整理算法(将所有标记存活对象移动到内存最前端,删除内存末端地址所有遗留碎片);
3)永久代:java8开始,永久代被移除,使用的是元空间(即本地内存)。
JVM问题
内存溢出
出现OutOfMemoryError异常,俗称内存不够。通常在运行大型程序时发生,当程序所需要的内存远远超出了JVM内存所承受大小,就出现OutOfMemoryError异常(称为OOM异常)。
栈溢出
场景①
问题:无穷递归,导致线程栈溢出,抛出stackoverflower异常。
解决:设置栈大小(-Xss),默认1024KB,不能设置过大,耗时、第二个可采用for循环方式处理。
场景②
问题:不断创建线程,同时跑,内存不够,抛出outofmemoryerror异常。
解决:减小线程栈的大小,减小资源竞争。
堆溢出
场景①
问题:对象创建过多,都是强引用,垃圾无法回收,抛出outofmemoryerror异常。
解决:设置堆的初始化大小 (-Xms )设置堆的最大空间(-Xmx)。
方法区溢出
jdk1.8开始使用的是元空间(即本机内存),出现溢出情况几乎为0,若出现了,可设置方法区初始化大小(-XX:PermSize)和最大空间(-XX:MaxPermSize )。
排查
(补充:cpu飙高排查也是通过如下方法)
①查看错误日志,针对到类,检查对应方法;
②使用top命令,查看占用内存过高的进程,根据pid(进程id)查看详情;
③使用jdk自带jmap命令,导出dump;
④使用阿里的Arthas等。
内存泄露
内存泄漏应该被GC回收的无用对象没有被回收,导致的内存空间不足。当内存泄露严重时会导致OOM(内存溢出)。
根本原因:长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被GC回收。
场景:
1. 资源没关闭
2. 尽量避免使用的static成员变量
3. 等。。。
JVM调优
何时进行JVM调优
- Heap(堆内存)内存持续上涨达到设置的最大内存值;
- Full GC 次数频繁;
- GC 停顿时间过长(超过1秒);
- 应用出现OutOfMemory 等内存异常;
- 应用中有使用本地缓存且占用大量内存空间;
- 系统吞吐量与响应性能不高或下降。
性能调优
- 线程池:解决用户响应时间长的问题;
- 连接池:配置连接池参数;
- JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量;
- 程序算法:改进程序逻辑算法提高性能。
调优方法
减少创建对象的数量;
减少使用全局变量和大对象;