JVM、运行时数据区域、GC、类加载、JMM

2021/11/04

JVM、运行时数据区域、GC、类加载、JMM

参考资料

一、JVM虚拟机

二、JVM运行时数据区域

  • 方法区(永久代Permanet Generation):线程共享 空间不足触发Full GC,JDK8则完全取消永久代
    • 常量池
    • 静态域:存放静态变量
    • 类信息
    • OutOfMemery: PermGen space
  • heap(堆):线程共享,存放 new()的对象实例
    • 新生代Young :空间占比1
      • Eden: 空间占比8 Minor GC也叫Young GC非常频繁
      • From Survivor: 空间占比1
      • To Survivor: 空间占比1
    • 老年代old:空间占比2, 空间不足触发Major GC、以致Full GC
    • OutOfMemery: Java heap space
  • 虚拟机栈: 线程私有,最小单元为栈帧,每个方法的开始和完成就对应着一个栈帧的入栈与出栈操作
    • 基本数据类型、对象引用、局部变量表…
    • 每个线程固定大小
    • 共享堆中的对象
    • StackOverFlow
    • OutOfMemery
  • 本地方法栈:native方法使用的栈
  • 程序计数器:
  • 直接内存:不受GC管理

三、内存溢出异常

3.1 Java堆溢出

如果堆中没有足够内存完成分配实例,且堆也无法再扩展时,会抛出OutOfMemoryError异常。

3.2 虚拟机栈和本地方法栈溢出

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;

虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,就会抛出OutOfMemeryError异常

3.3 运行时常量池溢出

3.4 方法区溢出

3.5 本机直接内存溢出

3.6 内存泄漏

需要回收的对象没有被回收

内存泄露根源

  • 堆内存中的长生命周期对象持有短生命周期对象的强/软引用,尽管短生命周期的对象已经不再需要了,但是长生命周期对象持有它的引用而导致不能被回收,这就是Java内存泄露的根本原因。

3.7 JVM常用参数设置

  • -:标准参数,所有JVM都应该支持
  • X:非标,每个JVM实现都不同
  • XX:不稳定参数,下一个版本可能会取消

  • 将对内存的最小值-Xms参数与最大值-Xmx参数设置为一样可以避免堆自动扩展
  • 通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出时Dump出当前的内存堆转储快照以便事后分析

四、JVM垃圾回收机制、内存分配策略

https://blog.csdn.net/liuxiao723846/article/details/72782636
https://www.cnblogs.com/wtzbk/p/7985156.html

JAVA 四中引用类型

  • 强引用:把一个对象赋给一个引用变量
  • 软引用:通过SoftReference类来实现,内存足够时不被回收,内存不足时会被回收
  • 弱引用:用WeakReference类来实现,垃圾回收机制一运行就回收
  • 虚引用:要PhantomReference类来实现,不能单独使用,必须和引用队列联合使用,主要作用是跟踪对象被垃圾回收的状态。

4.1 回收时机:检测对象已死

某一个时点,一个对象如果有一个以上的引用(Rreference)指向它,那么该对象就为活着的(Live),否则死亡(Dead),视为垃圾,可被垃圾回收器回收再利用。

垃圾回收操作需要消耗CPU、线程、时间等资源,所以容易理解的是垃圾回收操作不是实时的发生(对象死亡马上释放),当内存消耗完或者是达到某一个指标(Threshold,使用内存占总内存的比列,比如0.75)时,触发垃圾回收操作

  • 引用计数算法:给对象添加一个引用计数器 缺点:难以解决对象之间相互循环引用的问题
  • 根搜索算法(可达性分析):从GC Roots节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连,证明该对象是不可用的。
    • 当一个对象到GC Roots没有任何引用链相连,证明该对象是不可用的
    • GC Roots对象
      • 虚拟机栈(栈帧中的本地变量表)中引用的对象
      • 方法区中的静态属性引用的对象
      • 方法区中的常量引用的对象
      • 本地方法栈中的JNI(一般说的Native方法)引用的对象

4.2 垃圾收集算法

4.2.1 标记 -清除算法(标记不压缩)

  • 算法思路: 算法分为标记和清除两个阶段:要遍历两次。第一次先从根集开始访问所有可达对象,并将他们标记为可达状态。第二次遍历整个内存区域,对不达状态的对象进行回收处理。
  • 优缺点:这种回收方式不压缩,不需要额外内存,但要两次遍历,效率低,会产生碎片。

4.2.2 复制算法

  • 算法思路: 将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
  • 优缺点: 一次遍历,效率高,不产生碎片,但系统内存折半使用

4.2.3 标记-整理算法(标记压缩)

  • 算法思路: 此算法结合了”标记-清除”和复制算法的优点,分两个阶段,第一个阶段从根节点开始标记所有的被引用的对象;第二个阶段遍历整个堆,清除未标记对象并且把存放对象压缩到堆的其中一块,按顺序排放。将之前占用的内存全部回收。此算法避免了标记-清除的碎片问题,也避免了复制的算法空间问题
  • 优缺点:两次遍历,效率低,不产生碎片,内存不折半

4.3 JVM内存分配、垃圾回收策略

4.3.1 内存分配策略

  • 对象优先在Eden分配
  • 大对象直接进入老年代
  • 长期存活的对象将 年轻代 -> 老年代
  • 永久代:常量池、存放静态变量、类信息

4.3.2 分代垃圾回收策略

分代的垃圾回收策略,是基于这样一个事实,不同的对象生命周期是不一样的,因此可以采用不同的收集算法,以便提高回收效率

年轻代:

  • 存放朝生夕死的对象
  • 采用 复制算法回收
  • 回收频率高,Minor GC
  • 划分两个区域,分别是E 区和 S 区。大多数对象先分配到Eden区,内存大的对象会直接被分配到老年代中。S 区又分Form、To两个小区,一个用来保存对象,另一个是空的;每次进行年轻代垃圾回收的时候,就把E大区和From小区中的可达对象都复制到To区域中,一些生存时间长的就直接复制到了老年代。最后,清理回收E大区和From小区的内存空间,原来的To空间变为From空间,原来的From空间变为To空间。

年老代:

  • 存放生命周期较长的对象、大对象
  • 标记-整理算法、标记-清除算法(不同垃圾收集器不同) 回收
  • 回收频率低,Major GC

持久代:

  • 回收频率 :不会被回收

4.4 垃圾收集器

jvm(17)垃圾回收器

按线程数分:串行回收 和 并行回收

  • 串行回收:是不管系统有多少个CPU,始终只用一个CPU来执行垃圾回收操作;
  • 并行回收:就是把整个回收工作拆分成多个部分,每个部分由一个CPU负责,从而让多个CPU并行回收。并行回收可以缩短GC的停顿时间,执行效率很高,但更复杂,内存会增加。

按照工作模式分:并发式垃圾回收器 和 独占式垃圾回收器。

  • 并发式垃圾回收器:与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。但需要解决和应用程序的执行冲突,因此系统开销比较高,执行时需要更多的堆内存。
  • 独占式垃圾回收器:一旦运行,就停止应用程序中其他所有线程,直到垃圾回收过程结束

  • Serial 串行收集器
    • 新生代和老年代都使用串行回收器,新生代使用复制算法,老年代使用标记-整理算法
    • “Stop the World”机制
  • Serial Old 串行收集器
    • “Stop the World”机制
    • 运行在Client模式下默认的老年代的垃圾回收器
    • Serial 0ld在Server模式下主要有两个用途:
      • 与新生代的Parallel scavenge配合使用
      • 作为老年代CMS收集器的后备垃圾收集方案
  • ParNew 并行收集器
    • Serial收集器的多线程版本,在新生代进行并行回收,老年代仍旧使用串行回收。新生代S区使用复制算法
    • “Stop the World”机制
  • Parallel Scavenge 并行收集器
    • 同样也采用了复制算法、并行回收和”Stop the World”机制
  • Parallel Old 并行收集器
    • 新生代和老年代都使用并行收集器
    • 采用了标记-压缩算法,但同样也是基于并行回收和”stop-the-World”机制
  • CMS 并发收集器
    • 第一次实现了让垃圾收集线程与用户线程同时工作
    • 采用标记-清除算法,并且也会”stop-the-world”
  • G1(Garbage First) 并发收集器
    • Java7 update4之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一
    • 堆被划分成许多连续区域(region),吸收了CMS收集器特点
    • 支持多CPU和垃圾回收线程, 支持很大的堆,高吞吐量

Stop The World(STW)

单线程进行垃圾回收时,必须暂停所有的工作线程,直到它回收结束。这个暂停称之为“Stop The World”

但是这种 STW 带来了恶劣的用户体验,例如:应用每运行一个小时就需要暂停响应 5 分。这个也是早期 JVM 和 java 被 C/C++语言诟病性能差的一个重要原因。所以 JVM 开发团队一直努力消除或降低 STW 的时间。(暂停用户线程的原因,引用的地址改变了,必须回收完,修改指针,再进行用户线程)

五、JVM类加载机制

5.1 类加载的时机

  • 1.遇到new实例对象、getstatic读取一个静态字段、putstatic设置一个静态字段、invokestatic调用一个类的静态方法 这4条指令且类没有进行过初始化。
  • 2.使用java.lang.reflect包的方法对类进行反射调用且类没有进行过初始化的时候。
  • 3.初始化一个类前,先触发其父类的初始化。
  • 4.虚拟机启动时指定要执行的主类先初始化。
  • 5.当调用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF-getStatic、REF-putStatic、REF-invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

5.2 类的生命周期、类加载

类的生命周期: 加载、连接(验证、准备、解析)、初始化、使用和卸载。
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的。

1.类加载的过程:(一个类必须与类加载器一起确定唯一性)

  • 1、通过一个类的全限定名来获取定义此类的二进制字节流;
  • 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass方法)。

2.验证阶段:(字节流信息符合虚拟机要求,不会危害虚拟机自身安全)

  • 1、文件格式校验,例如魔数、版本号;
  • 2、元数据验证,例如子类是否覆盖了父类的final字段;
  • 3、字节码验证,例如保证方法体中的类型转换是有效的;
  • 4、符号引用验证,确保解析动作能正常执行。

3.准备阶段:

  • 1.为类变量(静态变量)分配内存并设置类变量初始值,这些变量所用的内存都在方法区中进行分配。
  • 2.实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。
  • 3.通常情况下初始值是零值,在初始化阶段才会设为指定的值。但是,常量(static final)的初始化值被设为属性指定的值。

4.解析阶段:

  • 1.虚拟机将常量池内的符号引用替换为直接引用的过程。
    • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
    • 直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
  • 2.解析功能主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

5.初始化阶段:

  • 根据程序员通过程序制定的主观计划去初始化变量和其它资源,或者说初始化阶段是执行类构造器()方法的过程。

5.3 类加载器

类加载器:加载类的工具,当程序需要的某个类,那么需要通过类加载器把类的二进制加载到内存中. 类加载器也是Java类

类加载器主要分为两类,一类是 JDK 默认提供的,一类是用户自定义的

JDK 默认提供三种类加载器:

  • 1.Bootstrap ClassLoader 启动类加载器:每次执行 java 命令时都会使用该加载器为虚拟机加载核心类。该加载器是由 native code 实现,而不是 Java 代码,加载类的路径为 /jre/lib。特别的 /jre/lib/rt.jar 中包含了 sun.misc.Launcher 类, 而 sun.misc.Launcher$ExtClassLoader 和 sun.misc.Launcher$AppClassLoader 都是 sun.misc.Launcher 的内部类,所以拓展类加载器和系统类加载器都是由启动类加载器加载的。
  • 2.Extension ClassLoader 拓展类加载器:用于加载拓展库中的类。拓展库路径为 /jre/lib/ext/。实现类为 sun.misc.Launcher$ExtClassLoader
  • 3.System ClassLoader 系统类加载器:用于加载 CLASSPATH 中的类。实现类为 sun.misc.Launcher$AppClassLoader

用户自定义的类加载器:

  • 1.Custom ClassLoader 一般都是 java.lang.ClassLoder 的子类

在实例化一个新的类加载器时,我们可以为其指定一个 parent,即双亲,若未显式指定,则 System ClassLoader 就作为默认双亲。

5.4 双亲委派模型

如果一个类加载器收到了类加载请求,并不会自己先去加载,而是委托给父类的加载器去执行,如父类加载器还存在其父类加载器,则递归向上委托,最终到达顶层的启动类加载器
若父加载器可完成类加载,成功返回,若父加载器无法完成加载,子加载器才尝试加载

优点:可以避免类的重复加载,也避免了java的核心API被篡改

具体的说,类加载任务是由 ClassLoader 的 loadClass() 方法来执行的,按照以下顺序加载类:

  • 通过 findLoadedClass() 看该类是否已经加载, 该方法为 native code 实现,若已加载则返回。
  • 若未加载则委派给双亲,parent.loadClass(),若成功则返回。
  • 若未成功,则调用 findClass() 方法加载类。java.lang.ClassLoader 中该方法只是简单的抛出一个 ClassNotFoundException 所以,自定义的 ClassLoader 都需要 Override findClass() 方法

5.5 Tomcat 类加载机制

Tomcat 使用正统的类加载机制(双亲委派),但部分地方做了改动。

Webapp classLoader 的默认行为会与正常的双亲委派模式不同:

  • 从 Bootstrap classloader 加载。
  • 若没有,则依次从 System、Common、shared 加载。
  • 若没有,从 /WEB-INF/classes 加载。
  • 若没有,从 /WEB-INF/lib/*.jar 加载

也可以通过配置来使 Webapp classLoader 严格按照双亲委派模式加载类:

  • 在工程的 META-INF/context.xml(和 WEB-INF/classes 在同一目录下) 配置文件中添加

因为 Webapp classLoader 的实现类是 org.apache.catalina.loader.WebappLoader,他有一个属性叫 delegate, 用来控制类加载器的加载行为,默认为 false,我们可以使用 set 方法,将其设为 true 来启用严格双亲委派加载模式。

5.6 破坏双亲委派模型

自定义类加载器,重写loadClass方法;
使用线程上下文类加载器

六、JVM内存模型JMM

6.1 Java内存模型(主内存与工作内存)

java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)或者叫工作内存(Work Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

这三者之间的交互关系如下:

主内存-工作内存交互操作(原子操作):

  • lock:作用于主内存变量,把一个变量标识为一条线程独占的状态。
  • unclock:作用于主内存变量,把处于锁定状态的变量释放,释放后的变量才可以被其他线程锁定。
  • read:作用于主内存变量,把变量的值从主内存传输到线程的工作内存,以便随后load动作使用。
  • load:作用于工作内存变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use:作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎。
  • assign(赋值):作用于工作内存变量,把执行引擎接收到的值赋给工作内存的变量。
  • store(存储):作用于工作内存变量,把工作内存中变量的传送给主内存变量,以便随后的write操作。
  • write(写入):作用于主内存变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

6.2 volatile型变量

volatile禁止指令重排序,每次修改volatile变量的值后必须立刻同步回主内存,每次使用volatile变量前必须从主内存中刷新最新值

通过加入 内存屏障和 禁止重排序优化来实现可见性。具体实现过程:

  • volatile 写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执
  • 对volatile变量写操作时,会在写操作后加入一条 store屏障指令,将本地内存中的共享变量值刷新到主内存
  • 对volatile变量读操作时,会在读操作前加入一条 load屏障指令,从主内存中读取共享变量
  • volatile 的读性能消耗与普通变量几乎相同
  • volatile 不能保证操作的原子性,也就是不能保证线程安全性, 如果需要使用 volatile 必须满足以下两个条件:
  • 对变量的写操作不依赖与变量当前的值。
  • 该变量没有包含在具有其他变量的不变的式子中。

所以 volatile修饰的变量适合作为状态标记量。
访问volatile变量不会加锁,不会使线程阻塞,是比sychronized关键字更轻量的同步机制

6.3 原子性、可见性与有序性

原子性(Atomicity):
原子操作、提供互斥访问,同一时刻只能有一个线程对数据进行操作(Atomic、CAS算法、synchronized、Lock),非原子操作都会存在失去cpu时间片的可能,存在线程安全问题

  • 8种操作是原子操作:lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)
  • synchronized、lock:如果要实现更大范围操作的原子性,可以通过synchronized或Lock来实现
  • 对基本数据类型的变量的 读取和赋值 操作是原子性操作:如x = 10; //原子操作,直接将数值10写入到工作内存

可见性(Visibility):
一个主内存的线程如果进行了修改,可以及时被其他线程观察到(synchronized、volatile)

  • volatile 修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的
  • synchronize
  • lock
  • final也可以保证可见性,因为一旦初始化遍不可修改

有序性(Ordering):

  • 指令重排序和工作内存与主内存同步延迟。这两个原因造成程序的书写顺序(单线程下串行的有序)与实际CPU的执行顺序(多线程下顺序是不可预测的)是不一致的;更深层次的原因就是硬件原因了:CPU为了优化性能,缓存与主内存的访问速度差异导致最终的顺序产生变化,最终造成有序性问题。
  • 如果两个线程不能从 happens-before原则 观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序

volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,

  • volatile 是因为其本身包含“禁止指令重排序”的语义,
  • synchronized “一个变量在同一个时刻只允许一条线程对其进行 lock 操作”,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
  • lock
  • 先天的“有序性”:happens-before原则

6.4 先行发生原则

这8条原则摘自《深入理解Java虚拟机》,前4条规则是比较重要的,后4条规则都是显而易见的

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个 unLock操作先行发生于后面对同一个锁的 Lock()操作,也就是说只有先解锁才能对下面的线程进行加锁
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生与操作B,而操作B先行发生于操作C,则操作A先行发生于操作C
  • 线程启动规则: Thread对象的 start()方法先行发生于此线程的每一个动作,一个线程只有执行了 start()方法后才能做其他的操作
  • 线程终端规则:对线程 interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生(只有执行了 interrupt()方法才可以检测到中断事件的发生)
  • 线程终结规则:线程中所有操作都先行发生于线程的终止检测,我们可以通过 Thread.join()方法结束, Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始

6.5 缓存一致性问题

Post Directory

扫码关注公众号:暂无公众号
发送 290992
即可立即永久解锁本站全部文章