JVM虚拟机学习总结

JVM虚拟机

JVM内存区域是怎么划分的?

JVM内存主要有三个部分划分:线程私有的、线程共享的、直接内存

程序计数器

程序计数器在内存中分配了一块较小的区域,它可以看作是当前线程执行行数的指示器。

Java多线程的实现通常采用CPU时间片的方式进行快速切换,为了恢复CPU再次执行该线程时的位置,就需要一个能够记住该线程执行行号的计数器。

虚拟机栈(Stack):

虚拟机栈的生命周期和线程的生命周期是相同的,当程序每调用一个方法时,就会产生一个栈帧;虚拟机栈主要存储的信息有:局部变量表、方法出口等,只有当方法调用完成时,压入虚拟机的栈帧才会从栈中退出。

局部变量表:主要存放的是在编译时已知的基本类型变量和引用类型变量的引用地址,在内部存储是以局部变量槽(Slot)来表示的。

本地方法栈(Native Stack)

与虚拟机栈类似,区别在于本地方法栈执行的是被 native修饰的本地方法,而虚拟机栈为普通的Java程序服务。

堆(heap)

Java的堆内存主要存放的是对象实例和数组,是虚拟机内存区域中最大的一块区域,是线程共享的区域,也是垃圾回收器进行垃圾回收的一块重要区域。

根据GC的角度来划分的话,堆内存又划分为新生代和老年代。

新生代分为eden区、from survivor、to survivor区

方法区

JVM中的方法区并不是一个实在的物理概念,方法区它用于存储已经被虚拟机加载的类型信息、常量、静态变量等数据,但是方法区仅仅是一个逻辑上的概念。这就好比接口与实现类的关系一样,方法区仅仅是一个接口,一种规范,而永久代便是它的实现类。

在JDK1.6时代,使用永久代来实现方法区

到了JDK1.8的时候,摒弃了永久代的概念,采用了元空间的概念,即使用计算机的物理内存作为元空间的内存,同时将永久代的字符串常量池移出到堆内存中。

JDK1.6时期的方法区(永久代)

image-20210816215842307

JDK8时期摒弃永久代的概念,使用元空间实现方法区,同时将常量池里的字符串池也挪到了堆中。

image-20210816220458060

常量池与运行时常量池

常量池是一张表,JVM虚拟机根据常量表来找到需要访问的类名、方法名、变量等信息。

当程序运行时,就会把常量池的信息挪到运行时常量池,此时JVM虚拟机访问的类名、方法名、变量等信息的地址就会变成真实的物理地址,因此把该常量池称为运行时常量池。

StringTable

又称字符串常量池,属于常量池的一部分(jdk1.6时期,1.8时期存放在堆中)

有以下特性:

  • 在常量池内出现的字符串仅仅只是一个‘符号’,只有当变量引用时才会变成一个字符串对象。
  • 字符串拼接时,实际上使用的是StringBuilder对象的append()方法

如以下字符串 a/b,使用+号时实际上调用的是StringBuilder

  String a = "a";
  String b = "b";
  String c = "ab";
  String ab = a+b;
	System.out.println(c == ab);//false

实际上使用的是:

new StringBuilder().append(a).append(b).toString();

但是我们通过源码发现

  @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

在StringBuilder内部的toString()方法中,使用了new String()来得到字符串对象,这也就导致了c==ab为false的原因

该原理是通过编译期优化得到的。

intern

new出来的字符串存放在堆中,而直接引用字符串的变量是引用常量池的,因此可以使用intern方法主动将还没有放入字符串常量池的对象放入该常量池

  • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把字符串常量池中的对象返回
  • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入字符串常量池, 并把该对象返回

说说JVM的运行时内存

根据GC的角度来划分的话,堆内存又划分为新生代和老年代。

image-20210817205817948

新生代

新生代一般存放新生的内存对象,大约占用堆内存的1/3,如果新生的对象内存过大,将会存放在老年代中。在新生代中会触发MinorGC,新生代分为eden区、from survivor、to survivor区

eden区:

中文翻译为伊甸园,即亚当和夏娃的原住地,因此寓意为Java对象的出生地。当新的Java对象过大时,会在老年代产生新对象;当内存不足时,就会触发MinorGC,此时如果内存还无法容纳新对象,就会发生错误。

from survivor:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

to survivo:保留了一次 MinorGC 过程中的幸存者

谈谈垃圾回收算法

image-20210817210701686

确定对象是否能被回收的方法:引用计数法、可达性分析法

引用计数法

在Java中,对象是通过引用进行关联的。当需要操作一个对象时,就对该对象的引用计数+1,当操作完时,就对该对象的计数-1;这种通过引用计数的方式来判断对象是否可以被回收的方法就称为引用计数法。当一个对象计数为0时,则表明该对象可以被垃圾回收。

弊端:

image-20210817211446649

当对象循环引用时会造成对象无法被垃圾回收,从而发送内存泄漏。因此,Java虚拟机没有采用引用计数法。

可达性分析法

以GC Roots对象作为起始点开始向下搜索,如果一些对象没有被GC Roots所引用,那么就说这些对象是不可达的,因此可以判定该对象为可回收对象。

就好比盘子里的葡萄,用手提起葡萄根,连在根上没有掉落的葡萄就是可达的,散落在盘子上的就称为不可达的,因此在盘子上的葡萄就是可以被回收的。

哪些对象可以作为GC Roots对象呢?

  • Java虚拟机栈帧内所引用的对象
  • 方法区中被static修饰的对象
  • 本地方法栈的对象
  • 被上锁的对象

垃圾回收算法:标记清除、标记整理、copy算法

标记清除

该垃圾回收算法分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。

image-20210818205938224

标记:如上图,先对需要回收的垃圾对象标记为黑色()

image-20210818210034658

清除:随后将该内存清除

弊端:内存碎片化问题严重

标记整理

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

该算法分为两步:标记、整理

image-20210818205938224

如图,先对可回收的对象进行标记

与标记清除算法不同的是,标记整理第二步会将之前的对象往前挪动,使得对象之间的间隔更为紧凑,从而减少内存碎片的问题。

image-20210818210827484

弊端:虽然减少了内存碎片,但是因为需要挪动对象,造成了性能上的损耗。

复制Copy算法

为了解决效率问题,。它可以将内存分为大小相同两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

步骤:

  • 将内存区域分为两块,分别为from,to
  • 对象一开始在from区域,该区域内存使用完发起垃圾回收时,将存活的对象复制到to区域
  • 清除from区域的全部垃圾,同时将from和to的内存区域对调
  • 原来的to改为from,from改为to

分代回收算法

分代回收算法的特点就是根据对象的生命周期不同,将存放对象的区域划分为新生代和老年代。

在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择**“标记-清除”或“标记-整理”**算法进行垃圾收集

老生代

特点是每次垃圾回收时只有少量对象需要被回收,存放在老年代的对象一般为频繁使用或占用内存较大的对象

新生代

特点是每次垃圾回收时会有大量的对象需要被回收,绝大部分新生对象在新生代。

新生代划分了三个区域,分别是Eden、from survivor、to survivor


新生代使用了Copy算法对内存对象进行回收。
流程:

  • 新生对象在伊甸园出生
  • 当触发MinorGc时,会将幸存下来的对象放入from,未被回收的对象的年龄+1
  • 对调from和to的内存地址,把原来的from变为to,to变为from

四种引用类型

强引用

在Java中,我们最长见到的就是强引用。所谓强引用,就是把一个对象赋给一个引用变量,该对象就称为被强引用的对象。一个强引用对象,它是处于可达的状态,因此该对象是不能被垃圾回收的。

软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。 虚引用的主要作用是跟踪对象被垃圾回收的状态

总结:

强软弱虚,强的不能被回收,软的当系统内存不足时被被回收,弱的不管内存够不够都会被回收,虚的还需要结合引用队列来回收

引用类型被回收时间用途生存时间
强引用不会被回收对象的一般状态JVM停止运行时
软引用内存不足时被回收对象缓存内存不足时
弱引用无论内存是否不足都被回收对象缓存垃圾回收后
虚引用未知-需要结合引用队列使用未知未知

垃圾回收器

吞吐量
CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务

常见的垃圾回收器有:

Serial,翻译为连续的,是最基本的新生代垃圾收集器,单线程,使用了复制算法,当发生垃圾回收的时候会暂停其他线程的活动(stop the world)

ParNew,Serial垃圾收集器的多线程版本,使用了复制算法

Parallel Scavenge**,是一个新生代垃圾收集器,使用了复制算法,是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量。

Serial Old,与Serial一样是单线程的垃圾收集器,作用于老年代,使用标记整理算法

Parallel Old ,是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供,提供一个关注吞吐量的模式。

CMS ,是一种老年代垃圾收集器,其最主要目标是获取最短垃圾
回收停顿时间,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验

G1基于标记-整理算法,不产生内存碎片,可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域

Full GC 触发条件

  1. 调用 System.gc():只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟
    机管理内存。
  2. 老年代空间不足: 老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代
    等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟
    机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold
    调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
  3. 空间分配担保失败: 使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
  4. JDK1.7 以及以前的永久代空间不足:在 JDK 1.7 及以前, HotSpot 虚拟机中的方法区是用永久代实现的,永久代
    中存放的为一些 Class 的信息、常量、静态变量等数据。 当系统中要加载的类、反射的类和调用的方法较多时,
    永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,
    那么虚拟机会抛出 java.lang.OutOfMemoryError。 为避免以上原因引起的 Full GC,可采用的方法为增大永久代空
    间或转为使用 CMS GC。
  5. Concurrent Mode Failure: 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC
    过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC

对象创建过程

  1. 类加载检验: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号
    引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载
    过程。
  2. 分配内存:分配方式有“指针碰撞”和“空闲列表”两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是
    否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 标记-清理不规整,标记-整理以及复制算法是规整
    的。在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,
    作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
    CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项
    操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
    TLAB: 为每一个线程预先在 Eden 区分配一块儿内存, JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,
    当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
  3. 初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间的对象都初始化为零值(不包括对象头),这一步
    操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对
    应的零值。85
  4. 设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、 如何才能找
    到类的元数据信息、 对象的哈希码、 对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟
    机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  5. 执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角
    来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会
    接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来

对象已经死亡

  1. 引用计数法: 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减
    1;任何时候计数器为 0 的对象就是不可能再被使用的。 很难解决对象之间相互循环引用的问题。
  2. 可达性分析法: 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜
    索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用
    的。
    两次标记过程:即第一次标记可达性分析法中不可达的对象。第二次的话就要先判断该对象有没有实现 finalize()方
    法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优
    先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。

类的加载过程

类的加载过程分别为:加载 -> 连接 -> 初始化 -> 使用 -> 卸载

在连接过程中又分为:验证 -> 准备 ->解析

image-20210822211637036

加载:主要完成下面三件事情:通过全类名获得定义此类的二进制字节流,在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。

验证:文件格式验证、元数据验证、字节码验证、符号引用验证

准备: 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。 进行内存分配
的对象仅包括类变量(static),不包括实例变量;设置的初始值“通常情况”下是数据类型默认的零值,但是加上 final 之
后,就会在这个阶段赋值具体值。

解析: 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 符号引用就是一组符号来描述目标,可以是
任何字面量。 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化:必须进行初始化的五种情况:

  1. 当遇到 new 、 getstatic、 putstatic 或 invokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静
    态字段(未被 final 修饰)、或调用一个类的静态方法时。

  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("…"),newInstance()

  3. 初始化一个类, 如果其父类还未初始化,则先触发该父类的初始化。

  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类), 虚拟机会先初始化这个类。

  5. MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用
    findStaticVarHandle 来初始化要调用的类

谈谈类加载过程的双亲委托机制?

类加载器
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自
java.lang.ClassLoader:

BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib 目录下的 jar 包
和类或者或被 -Xbootclasspath 参数指定的路径中的所有类。 加载一些通用的类: Object 类等。

ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs
系统变量所指定的路径下的 jar 包。

AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
Main 类是在 AppClassLoader 里面加载的

双亲委派模型
每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当应当有自己的父类加载器。这里的类加载
器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父类加载器的代码。

AppClassLoader 的父类加载器为 ExtClassLoader

ExtClassLoader 的父类加载器为 null, null 并不代表 ExtClassLoader 没有父类加载器,而是 BootstrapClassLoader。
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。

加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载
器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加
载器 BootstrapClassLoader 作为父类加载器

双亲委派机制的好处

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载

保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型 ,而是每个类加载器加载自己的话就会出现一些问题 ,比如我们编写一个称为java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

如何破坏

自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法

JVM的内存模型

Java 内存区域和内存模型是不一样的东西,内存区域是指 Jvm 运行时将数据分区域存储,强调对内存空间的划分。

内存模型(Java Memory Model,简称 JMM )是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。

Java 内存模型(Java Memory Model,JMM)是 java 虚拟机规范定义的,用来屏蔽掉 java 程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现 java 程序在各种不同的平台上都能达到内存访问的一致性。可以避免像 c等直接使用物理硬件和操作系统的内存模型在不同操作系统和硬件平台下表现不同,比如有些 c/c程序可能

在 windows 平台运行正常,而在 linux 平台却运行有问题

JVM性能调优常用命令

工具: JConsole: Java 监视与管理控制台

Visual VM:多合一故障处理工具

命令: jps (JVM Process Status) : 类似 UNIX 的 ps 命令。用户查看所有 Java 进程的启动类、传入参数和 Java 虚拟
机参数等信息;

jstat( JVM Statistics Monitoring Tool) : 用于收集 HotSpot 虚拟机各方面的运行数据;

jinfo (Configuration Info for Java) : Configuration Info forJava,显示虚拟机配置信息;

jmap (Memory Map for Java) :生成堆转储快照;

jhat (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在
浏览器上查看分析结果;

jstack (Stack Trace for Java):生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的
方法堆栈的集合

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值