赞
踩
本文目录
类加载
1.1 类加步骤
1.1.1 加载
1.1.2 链接
1.1.3 初始化
1.1.4 使用
1.1.5 卸载
1.2 类加载器
1.2.1 启动类加载器(Bootstrap ClassLoader)
1.2.2 扩展类加载器(extensions class loader)
1.2.3 Apps(系统)类加载器(system class loader)
1.2.4 用户自定义类加载器
1.3 双亲委派机制
1.3.1 双亲委派机制
1.3.2 双亲委派模型工作工程:
1.3.3 双亲委派好处
1.3.4 双亲委派特例(Thread Context ClassLoader)
内存模型(JMM)
运行时数据区
2.1 什么是内存模型?
释义
2.1.1 JMM 解决问题
2.2 JMM 实现
2.1 原子性
2.2可见性(缓存一致性)
2.3有序性
2.4 Happens-Before
内存结构
3.1 堆(Heap)线程共享
3.2 方法区(Method Area)线程共享
MetaSpace 元空间
运行时常量池
3.3 虚拟机栈(JVM stack)线程私有
栈帧组成
3.4 Native本地方法栈 线程私有
3.5 程序计数器 (pc registeer)线程私有
3.6 直接内存
堆外内存使用
3.7 小总结:
对象
4.1 对象生命周期
4.1.1 创建阶段(Creation)
4.1.2 应用阶段(Using)
4.1.3 不可视阶段(Invisible)
4.1.4 不可到达阶段(Unreachable)
4.1.5 可收集阶段(Collected)
4.1.6 终结阶段(Finalized)与释放阶段(Free)
4.2 对象的访问
4.2.1 值传递
4.2.1 引用传递
4.2.3 对象访问定位
4.3 对象内存结构
4.3.1 对象头
4.3.2 实例数据
4.3.3 对齐填充
GC 如何确定一个对象是垃圾
5.1. 引用计数法
5.2 可达性分析(根搜索法)
5.2.1 GC ROOT 对象
5.2.2 可达性分析为什么要停止STW?
5.2.3 OopMap结构(safe point)
5.2.4 真正宣告死亡(两次标记)
5.3 什么时候会垃圾回收
GC 垃圾收集算法
6.1 标记-清除(Mark-Sweep)
6.2 标记-复制((Mark-Copying)
6.3 标记-整理(Mark-Compact)
6.4 分代收集算法
6.4.1 为什么要分代
6.4.2 对象的创建与GC
6.5 GC 类别
垃圾收集器
7.1 Serial 收集器
7.2 ParNew 收集器
7.3 Parallel Scavenge 收集器
7.4 Serial Old 收集器
7.5 Parallel Old 收集器
7.6 CMS收集器(Concurrent Mark Sweep)
7.6.1 使用条件
7.6.2 CMS收集的方法是:
7.7 G1收集器 (garbage frist)
7.8 ZGC全并发(Z Garbage Collector)
7.9 垃圾收集器总结:
7.9.1 吞吐量和停顿时间
7.9.2 如何开启需要的垃圾收集器
常用参数:
jvm 优化
性能优化 (突破现有瓶颈)
jvm 层面性能优化
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
Java 语言是一种具有动态性的解释型语言,类(Class)只有被加载到 JVM 后才能运行。一次编译,到处运行。当运行指定程序时,JVM 会将编译生成的 .class 文件按照需求和一定的规则加载到内存中,并组织成为一个完整的 Java 应用程序。这个加载过程是由类加载器完成,具体来说,就是由 ClassLoader 和它的子类来实现的。类加载器本身也是一个类,其实质是把类文件从硬盘读取到内存中。类的加载方式分为隐式加载和显示加载。隐式加载指的是程序在使用 new 等方式创建对象时,会隐式地调用类的加载器把对应的类加载到 JVM 中。显示加载指的是通过直接调用 class.forName() 方法来把所需的类加载到 JVM 中。
任何一个工程项目都是由许多类组成的,当程序启动时,只把需要的类加载到 JVM 中,其他类只有被使用到的时候才会被加载,采用这种方法一方面可以加快加载速度,另一方面可以节约程序运行时对内存的开销。此外,在 Java 语言中,每个类或接口都对应一个 .class 文件,这些文件可以被看成是一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。
在 Java 语言中,类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(例如基类)完全加载到 JVM 中,至于其他类,则在需要的时候才加载。
总结:
虚拟机把Class文件加载到内存 。并对数据进行校验,转换解析和初始化 。形成可以虚拟机直接使用的Java类型,即java.lang.Class
查找和导入class文件
此阶段使用类加载器。通过一个类的全限定名获取定义此类的二进制字节流(.class文件),通过全称限定名获取类的二进制流。将二进制流中的静态存储结构化方法转还为方法区运行时数据结构也就是方法区中存储的类信息。在堆内存中生成该类的 java.lang.Class 对象,作为该类的数据访问入口。(这里只是一个引用,并不是把类放入堆中)
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。在 Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
堆指向方法区
验证:保证被加载类的 class 文件格式的正确性 ,为了确保class文件字节流中的信息不会危害到虚拟机
准备:在方法区中为静态变量分配内存,并将其初**始化为默认值。(此时默认值皆为空,在初始化阶段赋值)**准备阶段不分配类中的实例变量内存。实例变量将会在对象实例化时随着对象分配在Java堆中。
解析:将符号引用转化为值引用。因为准备阶段已经分配内存了,所以这一步是将 符号引用(符号引用是指class 中的字面量。) 转换为 直接引用,就是直接指向目标的指针(内存地址)、相对偏移量或一个间接定位到目标的句柄。
对静态变量和静态代码块执行初始化工作。也就是为静态变量赋值。是初始化是类加载的最后一步。
前面的类加载过程,除了在加载时可以选择加载器,其他阶段完全由虚拟机主导,初始化阶段,才开始执行类中定义的Java程序代码
用来加载 java 核心类库,无法被 java 程序直接引用。
启动类加载器(Bootstrap ClassLoader),它是属于虚拟机自身的一部分,用C++实现的,主要负责加载<JAVA_HOME>\lib\rt.jar
目录中或被-Xbootclasspath指定的路径中的并且文件名是被虚拟机识别的文件。它等于是所有类加载器的爸爸。
用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
扩展类加载器(Extension ClassLoader),它是Java实现的,独立于虚拟机,主要负责加载<JAVA_HOME>\lib\ext
目录中或被java.ext.dirs系统变量所指定的路径的类库。
它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它应用程序类加载器(Application ClassLoader),它是Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那这玩意就是我们程序中的默认加载器。
通过继承 java.lang.ClassLoader 类的方式实现
双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。
1.当 Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
2.当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
3.如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
4.如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
5.如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
6.如果均加载失败,就会抛出ClassNotFoundException异常
双亲委派有啥好处呢?
它使得类有了层次的划分。就拿java.lang.Object
来说,你加载它经过一层层委托最终是由Bootstrap ClassLoader
来加载的,也就是最终都是由Bootstrap ClassLoader
去找<JAVA_HOME>\lib
中rt.jar里面的java.lang.Object
加载到JVM中。
这样如果有不法分子自己造了个java.lang.Object
,里面嵌了不好的代码,如果我们是按照双亲委派模型来实现的话,最终加载到JVM中的只会是我们rt.jar里面的东西,也就是这些核心的基础类代码得到了保护
如果出现 基础类又要调用用户的代码情况会破坏双亲委派
典型的例子便是JDBC,你先得知道SPI(Service Provider Interface),这玩意和API不一样,它是面向拓展的,也就是我定义了这个SPI,具体如何实现由**扩展者(各个厂商)**实现。我就是定了个规矩。
JDBC就是如此,在rt.jar里面定义了这个SPI,那mysql有mysql的jdbc实现,oracle有oracle的jdbc实现,java不管内部如何实现的,反正都得统一按我这个SPI来,这样java开发者才能容易的调用数据库操作。所以因为这样那就不得不违反双亲委派,Bootstrap ClassLoader
就得委托子类来加载数据库厂商们提供的具体实现。因为 Bootstrap ClassLoader 只能加载 <JAVA_HOME>\lib
中的类,其他的它无能为力。这就违反了自下而上的委托机制了。
为了解决这个困境,Java设计团队引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置类加载器,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。
有了线程上下文类加载器,JDBC服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
OSGi、tomcat 等也都违反了双亲委派机制
Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题。
JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
Java内存模型 其实是保证了Java程序在各种平台下对内存的访问都能够得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题。
除此之外,Java内存模型还提供了一系列原语,封装了底层实现后,供开发者直接使用。如我们常用的一些关键字:synchronized、volatile以及并发包等。
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性即程序执行的顺序按照代码的先后顺序执行。
缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题。所以,后文将不再提起硬件层面的那些概念,而是直接使用大家熟悉的原子性、可见性和有序性。
线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。所以在多线程场景下,就会发生原子性问题。因为线程在执行一个读改写操作时,在执行完读改之后,时间片耗完,就会被要求放弃CPU,并等待重新调度。这种情况下,读改写就不是一个原子操作。即存在原子性问题。
在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter
和monitorexit
。在synchronized的实现原理文章中,介绍过,这两个字节码,在Java中对应的关键字就是synchronized
。
因此,在Java中可以使用synchronized
来保证方法和代码块内的操作是原子性的。
在多核CPU,多线程的场景中,每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。
在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。
Java中的volatile
关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile
来保证多线程操作时变量的可见性。
除了volatile
,Java中的synchronized
和final
两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。
synchronized
是通过加锁,使线程顺序执行。所以保证了可见性。而volatile 是通过打破内存屏障实现的可见性。
除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是有序性问题。
为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化。
除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排。
在Java中,可以使用synchronized
和volatile
来保证多线程之间操作的有序性。实现方式有所区别:
volatile
关键字会禁止指令重排。synchronized
关键字加锁保证同一时刻只允许一条线程操作。
因为jvm会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性
我们编写的程序都要经过优化后(编译器和处理器会对我们的程序进行优化以提高运行效率)才会被运行,优化分为很多种,其中有一种优化叫做重排序,重排序需要遵守happens-before规则
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
原则定义:
1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
坦白来说:
前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能>知道a已经变成了1。
happens-before原则规则:
在程序运行过程中,所有的变更会先在寄存器或本地cache中完成,然后才会被拷贝到主存以跨越内存栅栏(本地或工作内存到主存之间的拷贝动作),此种跨越序列或顺序称为happens-before。
注:happens-before本质是顺序,重点是跨越内存栅栏
通常情况下,写操作必须要happens-before读操作,即写线程需要在所有读线程跨越内存栅栏之前完成自己的跨越动作,其所做的变更才能对其他线程可见。
借用大神的话
再有人问你Java内存模型是什么,就把这篇文章发给他:https://www.hollischuang.com/archives/2550?spm=a2c6h.12873639.0.0.48a823a8pgML6K
既生synchronized,何生volatile:https://developer.aliyun.com/article/715256
运行时数据区,其实重点存储数据的是堆和方法区(非堆),所以内存的设计也着重从这两方面展开(注意这两块区域都是线程共享的)。对于虚拟机栈,本地方法栈,程序计数器都是线程私有的。
可以这样理解,JVM运行时数据区是一种规范,而JVM内存结构是对该规范的实现
Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建。Java对象实例以及数组都在堆上分配。由 Java 虚拟机自动垃圾回收器管理,存取速度慢,在虚拟机启动时创建,
jdk1.8后常量池和静态变量放在堆内存中,类信息和编译代码放在元空间
在虚拟机启动时创建,保存jvm加载的类信息(包括接口、成员变量,方法信息)常量,静态变量,字节码、即时编译器(JIN)编译之后的代码。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非 堆),目的是与Java堆区分开来。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
JVM运行时方法区是一种规范,真正的实现。在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space
方法区规范再 jdk8 通过元空间(Metaspace)实现。并移出了jvm内存,使用机器直接内存。改进是 为了防止程序类太多,程序启动占用太多的堆内存,导致程序启动便 内存溢出了。
是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。
字面量:final 修饰、文本、字符串
符号引用:类信息(包括字段)接口、方法、明细数据、描述。
虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈是线程私有的,独有的,随着线程的创建而创建。每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。
栈的结构是由多个栈帧(invoke)组成的,调用一个方法就压入一帧,一个方法调用完成,就会把该栈帧从栈中弹出。帧上面存储局部变量表,操作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所以还是一个指向地址的指针
栈中的栈帧随着方法的进入顺序的执行的入栈和出栈的操作,一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定下来【忽略JIT编译器做的优化,基本当成编译期间可知】,当方法或线程执行完毕后,内存就随着回收
当出现了异常,就会出现我们经常见到堆栈跟踪信息printStackTrace(); 而栈帧stack frame包含当前执行方法所属类的本地变量组,操纵数栈,以及运行时常量池引用。
JDK 5以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。-Xss128k:设置每个线程的堆栈大小。 一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000
Java中每个线程都有自己的线程栈,如上所述,线程栈包含了当前线程调用方法当前执行的相关信息。每个线程只能访问自己的线程栈,线程创建的本地变量对其它线程不可见。
值得提及的是,如果一个本地变量(即线程中的变量)是原始类型,则它会创建在线程栈上;但如果一个本地变量是指向一个对象的一个引用,则此时,这个本地变量引用会在线程栈,而且引用的对象本身则是放在堆上的(无论是哪个线程创建的)。当多个线程同时访问一个对象的成员变量时,每一个线程都会复制这个本地变量的私有版本。
共享对象可见性:多个线程操作一个共享对象,一个线程的更新有可能对其它线程不可见。如,跑在cpu1的线程更新了一个共享对象;只要cpu缓存没有刷新回主存(此时在cpu寄存器中存储),则其更改对其它cpu线程不可见。从而引出了我们后即将介绍的volatile关键字。
局部变量:
存储:方法中定义的局部变量以及方法的参数存放在这张表中
局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使
用。
如果一个本地变量(即线程中的变量)是原始类型,则它会创建在线程栈上;但如果一个本地变量是指向一个对象的一个引用,则此时,这个本地变量引用会在线程栈,而且引用的对象本身则是放在堆上的(无论是哪个线程创建的)。当多个线程同时访问一个对象的成员变量时,每一个线程都会复制这个本地变量的私有版本
线程间数据操作不可见是因为,每个CPU执行时只能执行一个线程,所以会将线程的数据加载在CPU的缓存,CPU操作时会直接操作CPU一级缓存的数据,当计算结束后,将缓存中的数据刷入主存储器堆中。多核CPU同时执行多线程时就会出现脏数据问题,所以引入了volatile 关键字,打破内存屏障。
操作数栈
以压栈和出栈的方式存储操作数的
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,是符号引用转化为直接引用。是Java 多态特性的实现,类在运行阶段,才会确定调用接口的哪一个子类。此时将符号引用转化为直接引用。也就是将 符号变量转化为 实际调用 实现类的地址 。 为了实现动态的调用过程。
返回地址
当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。
附加信息
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
本地方法栈是与虚拟机栈发挥的作用十分相似, 区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。
程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,则这个计数器为空。
用于解决,cpu 时间片到期后,线程下次获取到cpu执行权后知道上次执行到那一条语句了,然后从那一条语句开始继续执行。
Object obj=new Object() 栈中元素指向堆中对象。obj 存在栈中,new Object() 实例存储在堆中。
private static Object obj=new Object(); 方法区中元素指向堆中对象。obj 存在方法区,new Object() 实例存储在堆中。
不要忘记加载类 时,堆指向方法区的案例“java.lang.class” 的类信息入口。
对象生命周期分为6个阶段分别为 创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。
创建阶段为对象分配内存,调用构造函数,初始化属性。当对象创建后会存储在JVM的heap堆中等待使用
一旦对象被创建,并分派给某些变量赋值,这个对象的状态就切换到了应用状态
对象至少被一个强引用持有称为处于应用阶段,一个对象可以有多个强引用,GC不会回收处于应用状态的对象
强引用(Reference)
如果一个对象具有强引用,那就类似于必不可少的物品,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
Dog dog = new Dog();
软引用(SoftReference)
MyObject aRef = new MyObject();
SoftReference aSoftRef = new SoftReference(aRef);
弱引用(WeakReference)
虚引用(PhantomReference)
引用队列(ReferenceQueue)
我们希望当一个对象被gc掉的时候通知用户线程,进行额外的处理时,就需要使用引用队列了。ReferenceQueue即这样的一个对象,当一个obj被gc掉之后,其相应的包装类,即ref对象会被放入queue中。我们可以从queue中获取到相应的对象信息,同时进行额外的处理。比如反向操作,数据清理等
强引用对象超出其作用域之后就变为不可见,当一个对象处于不可见阶段是,说明程序本身不再持有该对象的不论强弱引用,尽管这些引用仍然是存在的。
在虚拟机所管理的对象引用根集合再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变量或者本地代码接口的引用。这些对象都是要被垃圾回收回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。
当垃圾回收器发现该对象已经处于“不可达阶段”而且垃圾回收器已经对该对象内存空间的又一次分配做好了准备是,则对象进入了“收集阶段”。假设该对象已经重写了finalize方法,则回去运行该方法的终端操作。
注意:不要重载finazlie()方法!
会影响JVM的对象分配与回收速度在分配该对象时,JVM要在垃圾回收器上注册该对象,以便在回收时可以运行该重载方法;在该方法的运行时要消耗CPU时间且在运行完该方法后才会又一次运行回收操作,即至少需要垃圾回收器对该对象运行两次GC
可能造成该对象的再次“复活”在finzlize()方法中,**假设有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又一次变为“应用阶段”。**这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利于代码管理。
当对象运行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
垃圾回收器对该对象的所占内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间的又一次分配阶段”。
值传递是在程序设计中,对于函数调用的一种方法,值引用只是把值传递到新的变量,修改新的变量,不会修改原来的参数。
基本数据类型的传递:基本数据类型的值就保存在变量中,传递的是基本类型的字面量值的拷贝,当发生传递时并不会改变原来的变量值。
所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数
变量中保存的是实际对象的地址,传递的是堆中对象的一个拷贝(也就是引用),操作的并不是堆中的对象本身,在数据传递时原来的引用地址会被覆盖,赋值运算会改变原来引用中保存的地址,但是堆中的对象本身不会被改变。
java程序需要通过引用(ref)数据来操作堆上面的对象,那么如何通过引用定位、访问到对象的具体位置
Object object = new Object();
设这句代码出现在方法体中,"Object object” 这部分将会反映到Java栈的本地变量中,作为一个reference类型数据出现
reference类型在java虚拟机规范里面只规定了一个指向对象的引用地址
,并没有定义这个引用应该通过那种方式去定位,访问到java堆中的对象位置,因此不同的虚拟机实现的访问方式可能不同,主流的方式有两种:使用句柄和直接指针
简单来说就是java堆划出一块内存作为句柄池, ref引用中存储对象的句柄地址 ,句柄中包含对象实例数据、类型数据的地址信息。
优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用【ref】本身
就虚拟机而言,它使用的是第二种方式(直接指针访问)
与句柄访问不同的是,ref中直接存储的就是对象的实例数据地址,但是对象类型数据跟句柄访问方式一样。
优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【可能是出于Java中对象的访问时十分频繁的,平时我们常用的JVM HotSpot采用此种方式】就虚拟机而言,它使用的是第二种方式(直接指针访问)
一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充
在32位系统下,对象头8字节,64位则是16个字节【未开启压缩指针,开启后12字节】。对象头中存储了对象的hashcode、分代年龄、锁状态、锁标志位对于普通对象而言,其对象头中有两类信息:mark word和类型指针。另外对于数组而言还会有一份记录数组长度的数据。
上图为32位系统上各状态的格式
当对象状态为**偏向锁(**biasable)时,mark word存储的是偏向的线程ID;
当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;
当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。
在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
存放对象程序中各种类型的字段类型,不管是从父类中继承下来的还是在子类中定义的。
分配策略:相同宽度的字段总是放在一起,比如double和long
这部分没有特殊的含义,仅仅起到占位符的作用满足JVM要求。
由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符对齐填充。最早是因为 cpu 存储是规定 8u 8(8个8位无符号存储) 64 位。以8位保存
听大神的话:
Java对象的生命周期:https://www.jianshu.com/p/af1edcaa3e89
GC 关心区域 是堆,
GC 的重点是
早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
优点: 实现简单效率高,被广泛使用与如python何游戏脚本语言上。
缺点: 难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。
目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。
它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的
即使可达性算法中不可达的对象,也不是一定要马上被回收,还有可能被抢救一下
可作为GC Roots的对象有四种:
①虚拟机栈(栈桢中的本地变量表)中的对象引用,就是平时所指的java对象,存放在堆中
②方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
③方法区中的常量引用的对象,
④本地方法栈中JNI(native方法)引用的对象
为了避免该回收的没回收,不该回收的回收了。不该回收的回收了不可饶恕。
应用线程运行时,对象的引用会不停的发生变化,可能会导致已被可达性分析后的可达对象,不被引用了造成该回收的没回收。已被可达性分析后的不可达对象,因为系统运行,变为可达。但是已经被标记为回收对象了,造成不该回收的回收了。
问题:
1.如果方法区几百兆,一个个检查里面的引用,将耗费大量资源。
2.在分析时,需保证这个对象引用关系不再变化,否则结果将不准确。【因此GC进行时需停掉其它所有java执行线程(Sun把这种行为称为‘Stop the World’),即使是号称几乎不会停顿的CMS收集器,枚举根节点时也需停掉线程】
解决:
实际上当系统停下来后JVM不需要一个个检查引用,而是通过OopMap数据结构【HotSpot的叫法】来标记对象引用。 在类加载完时。HotSpot把对象内什么偏移量什么类型的数据算出来,在jit编译过程中,也会在特定位置记录下栈和寄存器哪些位置是引用,这样GC在扫描时就可以知道这些信息。【目前主流JVM使用准确式GC】
OopMap可以帮助HotSpot快速且准确完成GC Roots枚举以及确定相关信息。但是也存在一个问题,可能导致引用关系变化。
这个时候有个safepoint(安全点)的概念。safepoint 安全点是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定,比如记录OopMap的状态,从而确定GC Root的信息,使JVM可以安全的进行一些操作,比如开始GC。HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。GC的标记阶段需要stop the world,让所有Java线程挂起,这样JVM才可以安全地来标记对象。safepoint 可以用来实现让所有Java线程挂起的需求。
当JVM需要让Java线程进入safepoint的时候,只需要设置一个标志位,让Java线程运行到safepoint的时候主动检查这个标志位,如果标志被设置,那么线程停顿,如果没有被设置,那么继续执行。
safepoint不能太少,否则GC等待的时间会很久
safepoint不能太多,否则将增加运行GC的负担
1:循环的末尾
2:方法临返回前/调用方法的call指令后
3:可能抛异常的位置
要真正宣告对象死亡需经过两个过程。
1.可达性分析后没有发现引用链
2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。
[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会使F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。]
(1)当Eden区或者S区不够用了
(2)老年代空间不够用了
(3)方法区空间不够用了
(4)System.gc()
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。
当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次full gc,但是
具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决
定。但是不建议手动调用该方法,因为GC消耗的资源比较大
听听大神的话
聊聊JVM(九)理解进入safepoint时如何让Java线程全部阻塞:https://blog.csdn.net/ITer_ZC/article/details/41892567
此算法需要暂停整个应用。此算法执行分两阶段。
第一阶段标记:从引用根节点开始标记所有被引用的对象,递归遍历。此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
第二阶段清除:遍历整个堆,把未标记的对象清除,同时,会产生内存碎片。
缺点:
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程 序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1) 标记和清除两个过程都比较耗时,效率不高
(2) 会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无
法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法:将内存划分为两块相等的区域,每次只使用其中一块。(先复制再回收)把存活的对象,复制到新的区域然后干掉之前区域。
特点:浪费一半内存,因为对象死得快,不需要太大的空间,但是需要效率快。空间换时间思想。
应用:
新生代中因为对象都是"朝生夕死的",【深入理解JVM虚拟机上说98%(98这个数是统计学测算出来的)的对象,总之就是存活率很低,当内存使用超过98%以上时,内存就应该被minor gc时回收一次。】,适用于复制算法【复制算法比较适合用于存活率低的内存区域】。
它优化了标记/清除算法的效率和内存碎片问题,且JVM不以5:5分配内存【由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1【原有区域:保留空间】划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者 HotSpot 默认比例为8:1:1【换句话说当内存使用达到98%时才GC 就有点晚了,应该是多一些预留10%内存空间,这预留下来的空间我们称为S区,因为要保证一个s区是空的,所以两个S区应为20%,Eden则为80%】,
优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年内存进行分配】(担保策略)。
GC开始时,对象只会存于Eden和From Survivor区域,To Survivor【保留空间】为空。
GC进行时,Eden区所有存活的对象都被复制到To Survivor区,而From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认15是因为对象头中年龄占4bit,新生代每熬过一次垃圾回收,年龄+1),则移到老年代,没有达到则复制到To Survivor。
缺点:
空间利用率降低
老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效,浪费内存。一般,老年代用的算法是标记-整理算法,即:从根节点开始标记所有被引用对象,然后遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设 置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。
缺点:
效率慢最慢,遍历完成后进行整理。
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-19P0RdiB-1604235610630)(/Users/yangyibo/Desktop/GC生长.png)]
年轻代 GC称为 Minor GC/Young GC
老年代 GC称为 Major GC/Old GC
永久代 GC称为 MetaSpace GC
Full GC = Minor GC + Major GC + MetaSpace GC
Major GC 和 MetaSpace 绑定,任何一个触发都话导致其他的触发,并触发full GC。minor GC 满了会导致 Major GC 然后导致 MetaSpace GC 然后 full GC。
MetaSpace GC 和 eden 区GC 一样。 都是内存不够了自行GC
新生代收集器,是最基本、发展最久的收集器,在JDK3以前是gc收集器的唯一选择,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)
对于Client模式下的jvm来说是个好的选择。适用于单核CPU【现在基本都是多核了】
收集时要暂停其它线程,有点浪费资源,多核下显得。
虽然Serial看起来很坑,但是也不是完全没用的,比如说Serial在运行在Client模式下优于其它收集器 [简单高效,不过一般都是用Server模式,64bit的jvm甚至没Client模式](这也是虚拟机在Client模式下运行的默认值)
新生代收集器,使用停止复制算法,Serial收集器的多线程版,“收集算法、Stop The World、回收策略”和Serial一样,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同【几核就是几个】
使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;
使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
1.支持多线程,多核CPU下可以充分的利用CPU资源,比Serial效率高。
2.运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】
在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。
Parallel Scavenge 收集器:新生代收集器,使用停止复制算法,和ParNew一样支持多线程。关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。高吞吐量可以高效率的利用cpu尽快完成程序运算任务,适合后台运算,Parallel Scavenge注重吞吐量,所以也成为"吞吐量优先"收集器。
使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);
使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)
适用于中等规模和大规模数据的应用程序
老年代收集器,和新生代的Serial一样为单线程,不过它使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存 的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标 记整理算法清理,也需要暂停其它线程)这个模式主要是给Client模式下的JVM使用。
在JDK1.5之前,Serial Old收集器与Parallel Scavenge搭配使用。
如果是Server模式有两大用途
1.在JDK1.5之前,Serial Old收集器与Parallel Scavenge搭配使用。JDK5前也只有这个老年代收集器可以和它搭配。
2.作为CMS收集器的后备。
老年代收集器,多线程,多线程机制与Parallel Scavenge差不错,JDK 1.6开始出现。使用标记整理【可理解为标记-复制-整理】(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清 理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。
cpu 需要4核以上。jdk1.7以下适合使用。
老年代收集器,致力于获取最短回收停顿时间(被sun称为并发低停顿收集器),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC 进行 ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见后面),当用户线程内存不足时,采用备用方案Serial Old收集。
先3次标记,再1次清除,3次标记中前两次是初始标记和重新标记(此时仍然需要停止(stop the world))。
1.初始标记(Initial Remark)是标记GC Roots能关联到的对象(即有引用的对象),速度很快停顿时间很短;
2.并发标记(Concurrent remark)是执行GC Roots查找引用的过程,不需要用户线程停顿;即可达性分析
3.重新标记(Remark)为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,停顿时间比并发标记小得多,但比初始标记稍长。使用并发标记时开启的多线程资源。
4.并发清除 (不需要用户线程停顿)。
所以在CMS清理过程中,只有初始标记和重新标记需要短暂停顿,并发标记和并发清除都不需要暂停用户线程,因此效率很高,很适合高交互的场合。
优点:
并发收集、低停顿
缺点:
产生大量空间碎片、并发阶段会 消耗cpu 资源, 降低吞吐量
CMS也有缺点,它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担(CMS默认启动线程数为(CPU数量+3)/4)。
另外,在并发收集过程中,用户线程仍然在运行,仍然产生内存垃圾,所以可能产生“浮动垃圾”,本次无法清理,只能下一次Full GC才清理,因此在GC期间,需要预留足够的内存给用户线程使用。所以使用CMS的收集器并不是老年代满了才触发Full GC,而是在使用了一大半(默认68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction来设置)的时候就要进行Full GC,
如果用户线程消耗内存不是特别大,可以适当调高-XX:CMSInitiatingOccupancyFraction以降低GC次数,提高性能,如果预留的用户线程内存不够,则会触发Concurrent Mode Failure,此时,将触发备用方案:使用Serial Old 收集器进行收集,但这样停顿时间就长了,因此-XX:CMSInitiatingOccupancyFraction不宜设的过大。
还有,CMS采用的是标记清除算法,会导致内存碎片的产生,可以使用-XX:+UseCMSCompactAtFullCollection来设置是否在Full GC之后进行碎片整理,用-XX:CMSFullGCsBeforeCompaction来设置在执行多少次不压缩的Full GC之后,来一次带压缩的Full GC。
CMS 并发失败:
当产生垃圾的速度 大于清除的速度,CMS 会并发失败。此时会 STW ,之后使用 serial old 收集器进行垃圾回收。同时进行一个 full gc
jdk 1.8 推荐使用,1.9默认使用
G1(garbage first:尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),适用于 Java HotSpot VM 的低暂停、服务器(server)风格的分代式垃圾回收器。同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合**。利用 空间分治思想将 内存 逻辑划分 2048 块内存**。每块1-32M每个Region大小都是一样的,可以是1M-32M之间的数值,但是必须保证是2的n次幂如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到Humongous中
(1)分代收集(仍然保留了分代的概念)
(2)空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消 耗在垃圾收集上的时间不得超过N毫秒)
设置Region大小:-XX:G1HeapRegionSize=M
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域
当堆内存使用大于 50% 或 GC 频繁或者 垃圾收集时间长在 500ms - 1000ms时可以切换G1垃圾收集器。
但是使用G1垃圾收集器时堆内存需要有 6G 以上内存,也就是服务器内存最小8G
注意:不要手动设置新老年代大小,会放弃官方调优。G1 设置 stw参数 在 100-200ms之间。设置太小的话会收集不完垃圾,退化为full gc。可通过2分法找到合适的值
CMS新生代的Young GC、G1和ZGC都基于标记-复制算法,但算法具体实现的不同就导致了巨大的性能差异。标记-复制算法应用在CMS新生代(ParNew是CMS默认的新生代垃圾回收器)和G1垃圾回收器中
G1 GC 使用并发和并行阶段实现其目标暂停时间,并保持良好的吞吐量。当 G1 GC 确定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先)
G1垃圾回收器中。标记-复制算法可以分为三个阶段:
1.标记阶段停顿分析
初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
**再标记阶段:**重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。
2.清理阶段停顿分析
清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。
3.复制阶段停顿分析
复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。
四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。
G1的Young GC和CMS的Young GC,其标记-复制全过程STW,这里不再详细阐述。
JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题。只能在64位的linux上使用,目前用得还比较少
(1)可以达到10ms以内的停顿时间要求
(2)支持TB级别的内存
(3)停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
从设计目标来看,我们知道ZGC适用于大内存低延迟服务的内存管理和回收。ZGC通过着色指针和读屏障技术**,解决了转移过程中准确访问对象的问题,实现了并发转移**。
大致原理描述如下:
并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么**“读屏障”会把读出来的指针更新到对象的新地址上**,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。
算法:标记-复制算法(重大改进,ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因)
**关键技术:**着色指针、读屏障
ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是通过对象引用的地址,即着色指针,判断对象是否被移动。
ZGC垃圾回收周期
ZGC只有三个STW阶段:
其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。
着色指针:
当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。ZGC之所以设置三个虚拟地址空间,是因为它使用“空间换时间”思想,去降低GC停顿时间。“空间换时间”中的空间是虚拟空间,而不是真正的物理空间。后续章节将详细介绍这三个空间的切换过程。
其中,[0~4TB) 对应Java堆,[4TB ~ 8TB) 称为M0地址空间,[8TB ~ 12TB) 称为M1地址空间,[12TB ~ 16TB) 预留未使用,[16TB ~ 20TB) 称为Remapped空间。
与上述地址空间划分相对应,ZGC实际仅使用64位地址空间的第041位**,**而第4245位存储元数据,第47~63位固定为0。
内存屏障:读屏障:
读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。
ZGC中读屏障的代码作用:在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作。
读屏障示例:
Object o = obj.FieldA // 从堆中读取引用,需要加入屏障
<Load barrier>
Object p = o // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i = obj.FieldB //无需加入屏障,因为不是对象引用
ZGC并发处理演示
接下来详细介绍ZGC一次垃圾回收周期中地址视图的切换过程:
其实,在标记阶段存在两个地址视图M0和M1,上面的过程显示只用了一个地址视图。之所以设计成两个,是为了区别前一次标记和当前标记。也即,第二次进入并发标记阶段后,地址视图调整为M1,而非M0。
着色指针和读屏障技术不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在ZGC中,只需要设置指针地址的第42~45位即可,并且因为是寄存器访问,所以速度比访问内存更快。
听大神说:
新一代垃圾回收器ZGC的探索与实践:https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html
** 串行收集器:**Serial和Serial Old 。只能有一个垃圾回收线程执行,用户线程暂停。适用于内存比较小的嵌入式设备 。
并行收集器[吞吐量优先]: Parallel Scanvenge、Parallel Old。多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适用于科学计算、后台处理等若交互场景 。
并发收集器[停顿时间优先]:CMS、G1用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。适用于相对时间有要求的场景,比如Web 。
新生代收集器 :Serial、ParNew、Parallel Scavenge,都是复制算法
老生代收集器 Serial Old(标记整理)、Parallel Old (标记整理)、CMS收集器(Concurrent Mark Sweep)(标记清除)
新老生代收集器 :G1、garbage frist
jdk 8 默认使用 ParNew、Parallel Scavenge、Parallel Old
停顿时间->垃圾收集器 进行 垃圾回收终端应用执行响应的时间
吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互 的任务。
小结 :这两个指标也是评价垃圾回收器好处的标准。
(1)串行 -XX:+UseSerialGC -XX:+UseSerialOldGC (2)并行(吞吐量优先): -XX:+UseParallelGC -XX:+UseParallelOldGC (3)并发收集器(响应时间优先) -XX:+UseConcMarkSweepGC -XX:+UseG1GC
标准参数,不随着jdk 版本变化而变化
-X 参数 非标准参数,解释执行、编译执行、混合执行(解释+编译)
-XX 参数 非标准参数 随jdk版本变化而变化 ,
1 boolean
-XX[+/-] value
-XX:+UseG1GC 启用 G1GC
2 name = value
-XX:initialHeapSize=100M
-XX:MaxHeapSize=100M
3 其他参数:非标参数 (简写)
-Xms 100 等价于 -XX:initialHeapSize=100M
-Xmx 100 等价于 -XX:MaxHeapSize=100M
-Xss 128 等价于 -XX:ThreadStackSize=128
输出 参数修改过的参数:java -XX:+PrintFlagsFinal -version
manage 可实时修改的
product 不可实时修改
top 查看 cpu 和磁盘
jinfo -flag UseG1GC pid (查看是否启用G1垃圾收集器)
jinfo -flags pid 查看所有修改过的值
jstat pid 500 10(查看类加载的信息,500毫米一次。总共打印10次)
jstat -gc pid 500 10 (查看Gc的信息,500毫米一次。总共打印10次)
jstack pid (查看线程的信息 ,查看 jstack 14443 线程死锁等)
运行时数据区,堆的一个情况,对象使用率
jmap 打印堆存储快照
jmap -heap pid
当 cpu, 内存使用率高,吞吐量变小 分析性能
目标:低停顿,高吞吐
代码层面 通过分析优化代码, Tprofile (看代码创建对象数量前十 的方法)
OOM 内存 方法区,堆内存不够。
dump gc 日志查看
大部分情况是 内存泄漏
内存分配导致 OOM
大并发场景(内存瞬间被用完)解决:限流,防刷
高并发解决方案,突破现有瓶颈
根据业务情况,调整jvm内存分配,解决秒杀问题,短期时间,大量请求,调大新生代内存
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。