JVM笔记

1、JVM类加载

类(Class)只有被加载到JVM后才能运行,当程序运行时,JVM会将编译产生的 .Class文件 按照需求和一定的规则加载到内存中,并组织成为一个完整的Java应用程序。

类的生命周期是:从类(.class文件)被加载到虚拟机内存,到从内存中卸载为止。

1.1、类加载的过程

在这里插入图片描述

加载、验证、准备、解析、初始化、使用、卸载。

  • 加载:类的加载是由类加载器完成的。该阶段虚拟机的任务主要是找到需要加载的类,并把类的信息加载到jvm的方法区中,然后中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。
  • 连接:
    • 验证:当一个类被加载后,需要验证下该类是否合法,以保证加载的类能在虚拟机中正常运行。
    • 准备:为静态变量分配内存并设置默认的初始值
      • 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0
      • 引用类型默认值是null
      • 常量的默认值为我们设定的值。比如我们定义final static int a = 10,则在准备阶段中a的初始值就是10。
    • 解析:将符号引用替换为直接引用,也就是具体的内存地址。在这一阶段,jvm会将所有的类、接口名、字段名、方法名等转换为具体的内存地址。
  • 初始化:在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。
  • 使用:主动引用和被动引用
  • 卸载:在使用完类后,需满足下面,类将被卸载:
  1. 该类所有的实例都已经被回收,也就是java队中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收了;
  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

当上面三个条件都满足后,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程本质上就是在方法区中清空类信息,结束整个类的生命周期。

1.2、类加载器

BootStrap ClassLoader(根加载器):用来加载Java核心类库,无法被Java程序员直接引用。

ExtClassLoader (扩展类加载器):用来加载Java的扩展库,Java虚拟机的实现会提供一个扩张类库。

AppClassLocader(应用类加载器):根据Java应用的类路径

UserClassLoader(用户类加载器) : 用户自定义类加载器。

1.3、双亲委派机制

类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。双亲委托机制更好的保证了Java平台的安全性。

双亲委派机制如何保证安全性?

黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用。如果在这这个函数中,黑客加入一些“病毒代码”,并且通过自定义类加载器加入到JVM中。

如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。

如果有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。

1.4、类的加载方式

显示加载:是通过调用Class.forName() 方法来把所需的类加载到JVM中

隐式加载:是程序在使用new等方式创建对象时,会隐式的调用类的加载器把对应的类加载器加载到JVM中。

1.5、类的加载过程总结

  • 1、由.java文件经过编译成为.class(字节码文件)。
  • 2、再通过类加载器将.class文件加载到内存中。
  • 3、再通过创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象
  • 4、再对对象进行验证、准备、解析等。
  • 5、再对类进行初始化,包括: 1) 如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类; 2) 如果类中存在初始化语句,就依次执行这些初始化语句。

类加载指的是:加载,验证,准备,解析,初始化,这5个阶段。

class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static变量。

classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在new Instance才会去执行static块。

2、JVM内存模型

在这里插入图片描述

  • 方法区

专门用来存放已经加载的**类信息、常量、静态变量以及方法代码(class,静态变量、编译代码、运行时常量池)、**的内存区域。

  • 常量池

是方法区的一部分,主要用来存放常量和类中的符号引用等信息。

  • 堆区

用于存放类的对象实例,如new、数组对象 -Xmx和-Xms 控制 。

  • 新生代
    • Eden
    • From
    • To
  • 老年代
  • 永久代

在这里插入图片描述

简述分代垃圾回收器是怎么工作的?

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 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)时,升级为老生代。大对象也会直接进入老生代。

老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

对象优先在 Eden 区分配

多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。

  • Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
  • Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major

GC 的速度通常会比 Minor GC 慢 10 倍以上。

大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。

前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。

长期存活对象将进入老年代

虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。

  • java虚拟栈区

由一个个栈帧组成的后进先出的结构,主要存放方法运行时产生的局部变量、方法出口等信息。
用于存栈帧(局部变量表(对象引用,基本变量)、操作数栈、动态链接、方法出口)等信息;

  • 程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令:分支、跳转、循环、异常处理、线程恢复等基础操作都会依赖这个计数器来完成。

  • 直接内存

  • 本地方法栈(与虚拟机栈类似,为native方法服务)、

3、JVM内存溢出

JVM中除了程序计数器,其他的区域都有可能会发生内存溢出。

3.1、内存溢出

当程序需要申请内存的时候,由于没有足够的内存,此时就会抛出OutOfMemoryError,这就是内存溢出

  • 内存泄漏和内存溢出区别与联系
    内存泄漏:系统分配的内存没有被回收。
    内存溢出:分配的内存空间超过系统内存。

内存泄漏会导致可用的内存减少,进而会导致内存溢出。

3.2、内存溢出的原因分析

内存溢出是由于没被引用的对象(垃圾)过多造成JVM没有及时回收,造成的内存溢出。如果出现这种现象可行代码排查:

  • 1、是否App中的类中和引用变量过多使用了Static修饰 如public staitc Student s; 在类中的属性中使用 static修饰的最好只用基本类型或字符串。如public static int i = 0;
  • 2、是否App中使用了大量的递归或无限递归(递归中用到了大量的建新的对象)
  • 3、是否App中使用了大量循环或死循环(循环中用到了大量的新建的对象)
  • 4、检查App中是否使用了向数据库查询所有记录的方法。即一次性全部查询的方法,如果数据量超过10万多条了,就可能会造成内存溢出。所以在查询时应采用“分页查询”。
  • 5、检查是否有数组,List,Map中存放的是对象的引用而不是对象,因为这些引用会让对应的对象不能被释放。会大量存储在内存中。
  • 6、检查是否使用了“非字面量字符串进行+”的操作。因为String类的内容是不可变的,每次运行"+"就会产生新的对象,如果过多会造成新String对象过多,从而导致JVM没有及时回收而出现内存溢出。

3.3、常见的四种内存溢出情况

  • 堆溢出(OutOfMemoryError:java heap space)
  • 持久代溢出(OutOfMemoryError: PermGen space)
  • 栈溢出(StackOverflowError)
  • OutOfMemoryError:unable to create native thread

1)堆溢出:JVM Heap :java.lang.OutOfMemoryError: Java heap space

JVM在启动的时候会自动设置JVM Heap的值, 可以利用JVM提供的 -Xmn -Xms -Xmx 等选项可进行设置。Heap的大小是Young Generation 和Tenured Generaion 之和。在JVM中如果98%的时间是用于GC,且可用的Heap size 不足2%的时候将抛出此异常信息。

解决方法 :手动设置JVM Heap(堆)的大小。

2)持久代溢出:PermGen space : java.lang.OutOfMemoryError: PermGen space

PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。为什么会内存溢出,这是由于这块内存主要是被JVM存放Class和Meta信息的,Class在被Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同,sun的 GC不会在主程序运行期对PermGen space进行清理,所以如果你的APP会载入很多CLASS的话,就很可能出现PermGen space溢出。一般发生在程序的启动阶段。

解决方法 : 通过-XX:PermSize和-XX:MaxPermSize设置永久代大小即可。

3)栈溢出: java.lang.StackOverflowError : Thread Stack space

栈溢出了,JVM依然是采用栈式的虚拟机,这个和C和Pascal都是一样的。函数的调用过程都体现在堆栈和退栈上了。调用构造函数的 “层”太多了,以致于把栈区溢出了。 通常来讲,一般栈区远远小于堆区的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K的空间(这个大约相当于在一个C函数内声明了256个int类型的变量),那么栈区也不过是需要1MB的空间。通常栈的大小是1-2MB的。通俗一点讲就是单线程的程序需要的内存太大了。 通常递归也不要递归的层次过多,很容易溢出。

解决方法 :1:修改程序。2:通过 -Xss: 来设置每个线程的Stack大小即可。

4)OutOfMemoryError:unable to create native thread

OutOfMemoryError:unable to create native thread:字面意思是内存溢出:无法创建新的线程。字面意思已经很明显了,出现这种情况的原因基本下面2点:

  • 程序创建的线程数超过操作系统的限制。
  • JVM占用的内存太多,导致创建线程的内存空间太小。

我们都知道操作系统对每个进程的内存是有限制的,我们启动Jvm,相当于启动了一个进程,假如我们一个进程占用了4G的内存,那么通过下面的公式计算出来的剩余内存就是建立线程栈的时候可以用的内存。 线程栈总可用内存=4G-(-Xmx的值)- (-XX:MaxPermSize的值)- 程序计数器占用的内存 通过上面的公式我们可以看出,-Xmx 和 MaxPermSize的值越大,那么留给线程栈可用的空间就越小,在-Xss参数配置的栈容量不变的情况下,可以创建的线程数也就越小。因此如果是因为这种情况导致的unable to create native thread,

解决方法:1:增大进程所占用的总内存。2:减少-Xmx或者-Xss来达到创建更多线程的目的。

5)小结

栈内存溢出:程序所要求的栈深度过大导致。
堆内存溢出: 分清内存泄露还是内存容量不足。泄露则看对象如何被 GC Root 引用。不足则通过 调大 -Xms,-Xmx参数。
持久带内存溢出:Class对象未被释放,Class对象占用信息过多,有过多的Class对象。
无法创建本地线程:总容量不变,堆内存,非堆内存设置过大,会导致能给线程的内存不足。

4、JVM垃圾回收

4.1、如何判断对象已死?

4.1.1 引用计数法

算法的描述为 : 给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。

但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因是引用计数法无法解决对象的循环引用问题

4.1.2 可达性分析算法

Java并不采用引用计数法来判断对象是否已“死”,而采用可达性分析来判断对象是否存活。

此算法的核心思想:通过一系列称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时,证明此对象不可用。

GC Roots的对象包含以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中(Native方法)引用的对象

由于方法区、虚拟机栈和本地方法栈中保存了类中和方法中定义的变量的引用,因为是自己定义的变量,所以肯定是有用的。

我还不想死,哈哈哈“”

但是,在可达性分析后发现一些对象没有跟GC root相连接的引用链,该对象会被进行一次标记,然后进行筛选,筛选的条件是判断该对象有没有必要执行finalize()方法,但如果对象没有重写finalize()方法或者对象的finalize方法已经被虚拟机调用过一次了,则都将视为“没有必要执行”,垃圾回收器可以直接回收。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法)。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。

4.2、四种引用?

  • 强引用: 如在方法中定义:Object obj = new Object();真正的对象“new Object()”保存在java堆中,其中“obj”代表了一个引用,存放的是java堆中“new Object()”的起始地址。只要引用还在,垃圾收集器就不会回收掉被引用的对象。

  • 软引用: 是用来描述一些有用但非必须的对象,我们可以使用SoftReference类来实现软引用。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中。如果回收之后内存还是不足,才会报内存溢出的异常。

  • 弱引用: 是用来描述非必须的对象,使用WeakReference类来实现弱引用。它只能生存到下一次垃圾回收发生之前,当垃圾回收机制开始时,无论是否会内存溢出,都将回收掉被弱引用关联的对象。

  • 虚引用: 最没有存在感的一种引用关系,可以通过PhantomReference类来实现。存在不存在几乎没影响,也不能通过虚引用来获取一个对象实例,存在的唯一目的是被垃圾收集器回收后可以收到一条系统通知。

4.3、垃圾回收算法

4.3.1、标记-清除

“标记-清除”算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

“标记-清除”算法的不足主要有两个:

  • 效率问题: 标记和清除这两个过程的效率都不高
  • 空间问题: 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

4.3.2、复制算法(新生代回收算法)

“复制”算法是为了解决“标记-清除”的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等的复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图:

4.3.3、标记整理算法(老年代回收算法)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。针对老年代的特点,提出了一种称之为“标记-整理算法”。标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。流程图如下:

4.3.4、分代收集算法

Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。

Full GC 又称为老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

对象晋升老年代

1、年龄 >= 阈值   

2、大对象

3、from与to区,如果相同年龄的对象>from区一半,那么>=该年龄,进入老年代     

Full GC

1、老年代满

2、永久代内存不足

3、system.gc

FullGC内存泄露排查 jstack(查看线程)、jmap(查看内存)和jstat(性能分析)

4、JVM性能优化

JVM 调优的工具?

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控;
  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

常用的 JVM 调优的参数都有哪些?

-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。

5、其他内容

说一下 JVM 有哪些垃圾回收器?

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了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收集器的老年代版本;

  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

  • JDK1.8( Parallel Scavenge(多线程复制) + Parallel Old(多线程标记整理))

  • CMS垃圾回收器(老年代) 并发+标记清除 - 最小的停顿时间

  • G1垃圾回收器(同时在新老生代工作)- 可预测垃圾回收的停顿时间

  • JDK11带来的全新的 ZGC

New 对象的过程

1、找到类路径的全名,判断是否字节码是否加载过了,没有就去加载

2、JVM为对象分配内存,两种方式(不同垃圾回收算法不同的分配方式)

1、指针碰撞 (复制与标记算法、内存规整、分界指针-----分界指针+对象大小)

2、空闲列表 (标记清除、内存碎片化、找到空闲区域给对象,更新记录)

3、设置对象头

4、调用对象init()方法,初始化

5、线程栈中新建对象引用,并指向堆中刚刚新建的对象实例

对象

对象头

Mark Word(hashcode,分代年龄、锁标志位信息)、

Klass point(对象指向类元数据的指针,找到对象是那个类的实例)、

Monitor

实例数据(类的数据信息、父类信息)

对其(必须是8字节的整数倍)

Stop - The - World机制

(当minor gc 或者 full gc 垃圾回收线程工作,其他线程都被挂起)

JMM

(q:磁盘跟不上cpu速度读写、缓存、本地内存)

逃逸分析

JVM逃逸分析(决定对象范围,再确定在哪分配)就是分析出对象的作用域。当一个对象在方法体内声明后,该对象的引用被其他外部所引用时该对象就发生了逃逸,反之就会在栈帧中为对象分配内存空间。

深入理解JVM的垃圾回收机制
JVM初探 -JVM内存模型
Java虚拟机(JVM)面试题(2020最新版)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值