JVM总结

1.JVM运行时数据区

在这里插入图片描述

线程私有和线程共享共分为5大区域

1.线程私有内存:程序计数器、虚拟机栈、本地方法栈

线程私有:指的是这三块区域生命周期与线程的生命周期相同,随着线程的创建而创建,线程的销毁而销毁,不同线程间的这三块内存彼此隔离。

1. 程序计数器

可以当做是当前线程正在执行的字节码行号指示器,这是唯一一块不会产生类OOM(OutOfMemoryError) 异常的区域。
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。可以理解为:是用来记录线程执行到哪一步,当重新回到此线程的时候,从那一步开始执行,不用重新开始。
:若执行的是本地方法,程序计数器值为空。

2. 虚拟机栈——Java方法的内存模型

-Xss设置栈的大小
每个方法的调用和执行完成就是对应一个栈帧在栈中的入栈和出栈
每个方法在执行的同时都会创建一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息
每一个方法从调用到完成执行的过程,就对应一个栈帧在JVM中入栈与出栈的过程。
之前我们一直说的栈区域其实就是此处的虚拟机栈,再具体一点就是虚拟机栈中的局部变量部分。

  • 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)

  • 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去

  • 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。

  • 出口:出口正常的话就是return 不正常的话就是抛出异常落

3. 本地方法栈——native方法的内存模型

除了方法类型不同其他与虚拟机栈相同
注:在 HotSpot虚拟机中本地方法栈与虚拟机栈合二为一,并不区分它们。


2. 线程共享内存:Java堆、方法区

线程共享:所有线程共享此三块区域,彼此不隔离。JVM进程完了后才结束,垃圾回收策主要针对于这三大区域。

1.Java堆( GC堆,垃圾回收主要负责的区域)

Java堆(Java Heap)是JVM所管理的最大内存区域,存放所有对象实例以及数组实例
例:

Test test = new Test()new Test(); 在堆中
test 在栈中

程序先右再左,执行 new Test()时先产生了一个放在堆上的对象(对象中都是属性和方法)之后执行 Test test = new Test() 在栈上开辟了一段名称为test的空间,用于保存堆内存地址。
—Xmx设置堆的最大值
—Xms设置堆的最小值
如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OOM。

2.方法区

它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据JDK8以前的HotSpot虚拟机中,方法区也被称为"永久代"(JDK8已经被元空间取代)。
已被虚拟机加载的类信息:类里面有哪些方法, 哪些变量(修饰符、类型等等)。但是其具体的值在堆上存储。

3.常量池

是方法区的一部分,存放字面量与符号引用。
字面量:直接写出来的值。 int i = 10;要让10赋值这个i,这个i需要有地方保存,他就在常量池里面存着。字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
符号引用:(符号引用 -> 找到指定的类,再通过引用变量找到堆空间)
java.util.Test test = new Test();不同包下有可能会有相同的类名。通过包名.类名找到特定的类或者方法的过程叫做符号引用。

2.垃圾回收机制

主要针对线程共享内存(堆,方法区)

程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。

GC是不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行清楚那个对象,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System.gc 方法来"建议"执行垃圾收集器,但是他是否执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。

1. 判断哪些对象需要回收

在回收前先需要判断对象是否存活

  1. 引用计数法(Python,C++智能指针都用到了这些)
    给每个对象附加一个引用计数器,每当有一个引用指向当前对象,计数器 + 1,每当有引用不再指向当前对象,计数器值 - 1,任意时刻引用计数器值为 0 的对象就被标记为不再“存活”。
    缺点:无法解决循环引用问题(我中有你,你中有我)

  2. 可达性分析算法(Java、C#、Lisp)
    通过一系列被称为GC Roots的对象开始向下搜寻,若到指定对象有路可走(“可达”),认为此对象存活,若从任意一个GC Roots对象到目标对象均不可达,认为目标对象已经不在存活。

哪些对象可以作为GC Roots:

  • 虚拟机栈和本地方法中的临时变量指向的对象
  • 类中静态变量引用的对象
  • 类中常量引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象

例:

public class Test {
	public Object instance = null;
	private static int _1MB = 1024 * 1024;
	private byte[] bigSize = new byte[2 * _1MB];
	public static void testGC() {
		//1
		Test test1 = new Test();
		//1
		Test test2 = new Test();
		//2
		test1.instance = test2;
		//2
		test2.instance = test1;
		test1 = null;
		test2 = null;
		// 强制jvm进行垃圾回收
		System.gc();
	}
	
	public static void main(String[] args) {
		testGC();
	}
}

从结果可以看出,GC日志包含" 6092K->856K(125952K)",意味着虚拟机并没有因为这两个对象互相引用就不回收他们。即JVM并不使用引用计数法来判断对象是否存活。
test1,test2属于虚拟机栈中的临时变量指向的对象可以作为GC Roots
但是instance作为一个类中普通属性不能作为GC Roots所以即使他指向了也没有意义。(同图中的object 5,6,7)
在这里插入图片描述

2. 对象自我拯救(finalize)

JVM在进行GC之前,需要判断即将回收的对象所在的类是否覆写了finalize()?
a. 若没有被覆写,此对象直接被回收
b. 若对象所在的类覆写了finalize

  • 若finalize() 未被JVM调用果,则会调用finalize(),若对象在此次调用过程中与GC Roots有路可走,此对象将不在被回收
  • 若finalize()被JVM调用过,那么此对象直接被回收。
/**
 * @program: jvmTest
 * @description: 自我拯救
 * @author: fwb
 * @create: 2019-07-27 17:14
 **/
public class FinalizeTest {
    public static FinalizeTest finalizeTest;
    public void isAlive() {
        System.out.println("I am alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        finalizeTest = this;
    }

    public static void main(String[] args)throws Exception {
        finalizeTest = new FinalizeTest();
        finalizeTest = null;
        System.gc();
        Thread.sleep(500);
        if (finalizeTest != null) {
            finalizeTest.isAlive();
        }else {
            System.out.println("now,I am dead :(");
        }
        // 下面代码与上面完全一致,但是此次自救失败
        finalizeTest = null;
        System.gc();
        Thread.sleep(500);
        if (finalizeTest != null) {
            finalizeTest.isAlive();
        }else {
            System.out.println("now,I am dead :(");
        }
    }
}

结果:
在这里插入图片描述
从上面代码示例我们发现,finalize方法确实被JVM触发,并且对象在被收集前成功逃脱。但是从结果上我们发现,两个完全一样的代码片段,结果是一次逃脱成功,一次失败。这是因为,任何一个对象的finalize()方法都只会被系统自动调用一次,如果相同的对象在逃脱一次后又面临一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败。

final、finally、finalize的区别
final:终结器
1.被final修饰的类不能有子类
2.被final修饰的值不能更改
3.被final修饰的方法不能被覆写

finally:用在异常体系中
作用:保证重点代码一定会被执行

finalize:
Object类提供的一个方法,对象的自我拯救。

3.已经确定死亡了的对象如何进行垃圾回收

1.方法区的回收(永久代回收)

方法区(永久代)的垃圾回收主要收集两部分内容 : 废弃常量和无用的类。

  1. 回收废弃常量和回收Java堆中的对象十分类似。以常量池中字面量(直接量)的回收为例,假如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象引用常量池的"abc"常量,也没有在其他地方引用这个字面量,如果此时发生GC并且有必要的话,这个"abc"常量会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
  2. 判定一个类是否是"无用类"则相对复杂很多。类需要同时满足下面三个条件才会被算是"无用的类" :
    a.该类所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
    b.加载该类的ClassLoader已经被回收
    c.该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法

JVM可以对同时满足上述3个条件的无用类进行回收,也仅仅是"可以"而不是必然。在大量使用反射、动态代理等场景都需要JVM具备类卸载的功能来防止永久代的溢出。方法区的gc频率非常低。

2.堆的垃圾回收算法:

1.标记清除算法:
算法分为标记与清除两个阶段:

  • 标记阶段首先将需要回收的对象打上回收标记。
  • 清除阶段一次性回收所有被打上标记的对象空间。
    不足:
    1.效率低
    2.主要问题:标记清除会产生大量不连续空间碎片,导致gc频繁发生。

2.复制算法(新生代垃圾回收算法)
首先需要了解堆分为新生代与老年代。
对象的分配策略:
新生代
对象默认先在新生代产生,大部分对象在此区域存放,该区域对象特点是“对象朝生夕死”(存活率很低)
老年代

  1. 大对象直接进入老年代,虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的在于避免Eden区以及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)
  2. 长期存活对象(默认新生代对象在From和To之间移动15次(默认值可修改))进入老年代
  3. 动态年龄判断:若From和To区相同年龄对象的和超过Survivor空间的一半,将所有此年龄的对象直接晋升老年代。

由于新生代中98%的对象都是"朝生夕死"的,所以不需要按照1:1的比例来分配空间,而是将新生代内存分为一块较大的Eden(伊甸园)与两块大小相等的Survivor(幸存者区),默认比例为8:1:1,每次使用Eden与其中一块Survivor区域(一个叫From区,一个叫To区)
Step1.
对象默认都在Eden区产生,当Eden区即将满时,触发一次Minor GC(新生代GC),将Eden区所有存活对象(一般只有2%)复制到From区,然后一次性清理掉Eden区的所有空间。
Step2.
(此时From已经有存活对象了)当Eden区域再次即将满的时候,触发Minor GC,此时需要将Eden与From区的所有存活对象复制到To区,然后一次性清理掉Eden与From的所有空间。
Step3.
当Eden区域再次即将满的时候,触发Minor GC,此时需要将Eden与To区的所有存活对象复制到From区,然后一次性清理掉Eden与To的所有空间。
之后的新生代Gc重复Step2,3。
:
某些对象来回在From与To区交换若干次以上(默认15次)以上,将其置入老年代空间。

3. 标记整理算法(老年代的垃圾回收算法)
相较于标记清除:整理阶段先让存活对象向一端移动,而后清理掉存活对象边界之外的所有空间。避免产生不连续的空间碎片。
由于老年代存活率很高所以老年代不使用复制算法。

3.分代收集策略(JavaGC)

将堆空间分为新生代(-Xmn)与老年代空间,其中新生代采用复制算法,老年代采用标记整理算法。

  1. Minor GC:
    又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
    触发条件:
    eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
    ​或新创建的对象大小 > Eden所剩空间
  2. Major GC:
    又称为老年代GC: 当新生代空间不够用时需要从老年代借助空间(老年代的分配担保) ,当借助后还是不够用的时候,就需要清理一些老年代空间出来,所以出现了Major GC,经常会伴随至少一次的Minor GC。
  3. FullGC:
    如果理解Major GC 是清理OldGen,Full GC 是清理整个堆空间—包括年轻代和永久代,但是Major GC,又经常会伴随至少一次的Minor GC,这不是互相冲突嘛,所以不用纠结二者区别,可以认为二者没有区别。
    触发条件:
    每次晋升到老年代的对象平均大小>老年代剩余空间
    MinorGC后存活的对象超过了老年代剩余空间
    永久代空间不足
    执行System.gc()
    CMS GC异常
    堆内存分配很大的对象
    详解

4.频繁full gc/cpu飙升/服务器重启等问题

相关工具:
Spring Boot Admin 2.1.0 全攻略
Intellij IDEA集成JProfiler性能分析神器

  1. top/jps/ps 命令找到相关进程
  2. jstat(虚拟机统计数据监控工具)官方文档,1000ms刷新一次
    jstat -gcutil 164435 1000jstat -gccause 164435 1000
    日志,XX:+PrintGCDetails -Xloggc:dcc_gc.log找到原因。
  3. 如果是full gc,可以使用jmap(打印指定进程的详细信息)官方文档
    jmap -dump:format=b,file=文件名 [pid]
  4. 使用jvisualvm来分析dump文件
  5. java中内存泄露8种情况的总结

5.常见垃圾回收机器

详解

新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
jdk7、jdk8 使用Parallel Scavenge与Parallel Old
在这里插入图片描述

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

3.JVM参数设置

在这里插入图片描述
在这里插入图片描述
常见配置汇总:

堆设置

  • -Xms: 初始堆大小
  • -Xmx: 最大堆大小
  • -XX:NewSize=n: 设置年轻代大小
  • -XX:NewRatio=n: 设置年轻代和年老代的比值。如:为 3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1/4
  • -XX:SurvivorRatio=n: 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两
    个。如: 3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5
  • -XX:MaxPermSize=n: 设置持久代大小

收集器设置

  • -XX:+UseSerialGC: 设置串行收集器
  • -XX:+UseParallelGC: 设置并行收集器
  • -XX:+UseParalledlOldGC: 设置并行年老代收集器
  • -XX:+UseConcMarkSweepGC: 设置并发收集器

垃圾回收统计信息

  • -XX:+PrintGC
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -Xloggc:filename

并行收集器设置

  • -XX:ParallelGCThreads=n: 设置并行收集器收集时使用的 CPU 数。并行收集线程数。
  • -XX:MaxGCPauseMillis=n: 设置并行收集最大暂停时间
  • -XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n)

并发收集器设置

  • -XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况。
  • -XX:ParallelGCThreads=n: 设置并发收集器年轻代收集方式为并行收集时,使用的 CPU
    数。并行收集线程数。

参考文章

4.类加载机制

1.类装载的执行过程

类装载分为以下 5 个步骤:

  • 加载:根据查找路径找到相应的 class 文件然后导入;
  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。

2.双亲委派模型:

双亲委派模型的工作流程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此。因此,所有的加载请求都应当传送到顶层的BootStrap加载器中,只有当父加载器反馈无法完成这个加载请求时(在自己搜索范围中没有找到此类),子加载器才会尝试自己去加载。
目的与作用
保证Java安全性
例:
如果没有双亲委派模型,如果在代码中写了一个类叫String,如果AppLoader的ClassLoader加载成功则意味着程序中将会有两个String类。
在这里插入图片描述

3.类加载器

  • Bootstrap(启动类加载器):这个类加载器使用C++实现,是虚拟机自身的一部分;其他的类加载器都由Java语言实现,独立于JVM外部并且都继承于java.lang.ClassLoader.BootStrap类加载器负责将存放于<Java_HOME>\lib目录中(或者被-Xbootclasspath参数指定路径中)能被虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到JVM内存中。启动类加载器无法被Java程序直接引用。

  • ExtensionClassLoader(扩展类加载器):它负责加载<Java_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量指定的路径中的类库。开发者可以直接使用扩展类加载器。

  • AppClassLoader(应用程序类加载器):负责加载用户类路径(ClassPath)上指定的类库,如果应用程序中没有自定义自己的类加载器,则此加载器就是程序中默认的类加载器。

  • 自定义类加载器,用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

5.Java内存模型

Java的内存模型主要定义JVM如何将变量存储到内存中,又如何将内存中的变量取回等细节。
此处的变量包括实例属性、静态属性以及数组元素但不包括局部变量和方法参数,因为后两者是线程私有的,不会被线程共享。

Java内存模型规定了:

  1. 所有变量必须都存储在主内存中
  2. 每个线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。
  3. 线程对于变量的所有操作(读取、赋值等)必须在工作内存中进行,而不能直接操作主内存。不同线程之间也无法做到访问彼此的工作内存,变量之间的值传递均通过主内存来实现。

Java内存模型的三大特性:
并发程序同时满足以下三个特性才是线程安全的,任意一个不满足都不是线程安全。

  1. 原子性:
    即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。基本数据类型的访问读写是具备原子性的。如若需要更大范围的原子性,需要synchronized约束。
  2. 可见性:
    可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile、synchronized、final三个关键字可以实现可见性。
    final:修饰的变量无法修改,所以其他线程看到的时候一定是最新的。
    synchronized:一个时刻只有一个线程能访问。
  3. 有序性 :
    如果在本线程内观察,所有的操作都是有序的;如果在线程中观察另外一个线程,所有的操作都是无序的。前半句是指"线程内表现为串行",后半句是指"指令重排序"和"工作内存与主内存同步延迟"现象。
    Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before 原则。

6.JVM内置的检测工具

一般使用如下几种:
jps: 返回当前操作系统中的所有JVM进程ID jps -l(输出包名.类名)
jmap:查看当前JVM的内存情况 jmap -heap PID(查看PID的JVM的堆情况)
jstack :查看当前JVM的线程栈情况,常用于解决线程卡死问题

具体使用可参考:上文频繁full gc/cpu飙升/服务器重启等问题


参考:一篇文章掌握整个JVM,JVM超详细解析!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我顶得了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值