深入理解java虚拟机 (周志明)JVM个人总结
阅读原文时间:2021年04月20日阅读:1

java虚拟机规范 周志明
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够一次编译,到处运行 的原因。

* 执行引擎处于JVM的核心位置,在Java虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规范很详细地说明了当JVM执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java虚拟机支持大约248个字节码。每个字节码执行一种基本的CPU运算,例如,把一个整数加到寄存器,子程序转移等。Java指令集相当于Java程序的汇编语言。Java指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有0个或多个操作数,提供操作所需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。
* java的字节码是由javac所编译的,

Java中,字节码是CPU构架(JVM)的具有可移植性的机器语言

Java中,字节码是CPU构架(JVM)的具有可移植性的机器语言第一章 走近java
* 因为程序员把内存控制的权力交给了java虚拟机,编码的时候享自动内存管理的诸多优势。
* 提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄漏和指针越界问题
* 但是也是会出现内存泄漏。
* Jdk进化史
* jdk 1.1 jdbc jar文件格式 jdk javabeans 语法的内部类 反射
* 1.2 java分为三个方向 j2ee(企业) j2se(桌面开发) j2me(手机移动终端)
* collections集合 math TimerAPI
* 1.3 类库
* 1.4 正则表达式 异常链 nio xml 等
* 1.5 自动装箱 泛型 动态注解 枚举 可变长参数 遍历(foreach) concurrent 并发包
* 1.6 锁 垃圾收集 类加载 算法
* 普通对象指针压缩功能 (-XX:+ userCompressedOops)不建议开启 jvm自动管理开启
* 开启压缩指针会增加执行代码质量,java 堆 指向java堆内对象的指针都会被压缩
* 1.7
* 1.8 lambda 表达式 map


* 第二章
* java 内存区域与内存溢出异常
* 方法区和堆 线程共享
* 剩下的线程隔离
* 程序计数器(program counter register)只占用了一块比较小的内存空间{可以忽略不计}
* 可以看作是当前线程所执行的字节码文件(class)的行号指示器。在虚拟机的世界中,字节码解释器就是通过改变计数器的值来选取下一条执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都需要这玩意来实现的
* 多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的。
* 1个处理器执行一个线程 多核同时多个
* 每条线程都需要有一个独立的程序计数器。各条线程之间计数器互不影响独存储。线程私有的内存。

java虚拟机栈
* 每个方法执行都会创建一个栈帧,用于存放局部变量表,操作栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。
* 会有两种异常StackOverFlowError和 OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError

* 每当一个java方法被执行时都会在虚拟机中新创建一个栈帧,方法调用结束后即被销毁。
* 局部变量表中的变量作用域是当前调用的函数。函数调用结束后,随着函数栈帧的销毁。局部变量表也会随之销毁,释放空间。
* 栈帧存储空间为虚拟机栈,每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类引用。
* 当然方法调用其他的方法 新的栈帧 就会创建且控制权交给新的栈帧
* 而 JVM 的字节码指令是这样的:
* iconst_1 //把整数 1 压入操作数栈
* iconst_2 //把整数 2 压入操作数栈
* iadd //栈顶的两个数相加后出栈,结果入栈
* 局部变量表所需内存空间在编译期间完成分配 当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
* 局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和 double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量 区中占据的索引时3、4项,取值时,指令只需取索引为3的long值即可。
* 单位slot 8个数据类型基本上都是占用一个slot long duble 占用2个 加引用类型的数据指向一条虚拟机指令的操作码 引用指针 或者对象句柄
* 虚拟机规范 boolean 虚拟机中int代替  boolean 数组 oracle 中为byte 数组 
* true 为1 false 为0
* 局部变量表使用索引来进行访问 首个局部变量的索引值为0
* 操作数栈是后进先出的栈


* 本地方法栈
* 什么是Native Method
* 简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制
* 与java环境外交互:
* 有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。
* 堆
* 是虚拟机中最大的一块共享区域 在虚拟机启动的时候创建 它存储了自动内存管理系统 (gc垃圾收集器) 虚拟机实现者根据系统的实际需要来选择自动内存管理技术
* 所有类实例和数组分配内存的区域
* 基本上采用分代收集算法

* 方法区
* 各个线程共享的运行区域
* 存储了每个类的结构信息 运行时常量池 字段 方法数据 构造函数 普通方法的字节码内存  还有特殊方法
* oom
* 方法区并不等于永久代
* hotspot 把gc 分代收集扩展到方法区 使用永久代来实现方法区 跟堆一样管理内存
* 运行时常量池是方法区的一部分 具备动态性 可以编译时候产生 class 文件常量池内容 运行区间产生新的
* 虚拟机指令不依赖类 接口 类实例 数组的布局 而是依赖常量池表中符号信息
* 在HotSpot虚拟机中,用永久代来实现方法区,将GC分代收集扩展至方法区,但是这样容易遇到内存溢出的问题。
*   JDK1.7中,字符串常量池native()
*   JDK1.8撤销永久代,引入元空间。
* 直接内存(堆外内存)并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
* 好处: 这样做有两方面的好处:
* 减少GC管理内存:由于GCIH会从Old区“切出”一块, 因此导致GC管理区域变小, 可以明显降低GC工作量, 提高GC效率, 降低Full GC STW时间(且由于这部分内存仍属于堆, 因此其访问方式/速度不变- 不必付出序列化/反序列化的开销).
* GCIH内容进程间共享:由于这部分区域不再是JVM运行时数据的一部分, 因此GCIH内的对象可供对个JVM实例所共享(如一台Server跑多个MR-Job可共享同一份Cache数据), 这样一台Server也就可以跑更多的VM实例.
* 3、堆外内存的好处是:
* (1)可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;
* (2)理论上能减少GC暂停时间;
* (3)可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;
* (4)它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据
* 本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
* 配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常
* Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
* 基于此种内存模型,便产生了多线程编程中的数据“脏读”等问题。

volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。读取快 修改慢
* 1.volatile保证可见性
* 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
* 2)禁止进行指令重排序。
* 编译出来的只有一条字节码指令,也不意味执行这条指令就是一个原子操作 一条字节码指令在解释
* 执行时,解释器将要运行许多行代码才能实现。
* 什么是指令重排?
* 指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。
* 指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。
* 如何使用volatile呢
* 运算结果并不依赖变量的当前值,后者能够确保只有单一的线程修改变量的值

    非原子操作加锁 ++ 不能保证原子性 需要加synchronized 或者lock 
    第七章虚拟机类加载机制
* Java源代码被编译成class字节码,最终需要加载到虚拟机中才能运行。整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。
*  
* 加载 验证 准备 初始化 卸载5个阶段的顺序是确定的
* 加载
* 1、通过一个类的全限定名获取描述此类的二进制字节流; 
* 2、将这个字节流所代表的静态存储结构保存为方法区的运行时数据结构; 
* 3、在java堆中生成一个代表这个类的java.lang.Class对象,作为访问方法区的入口;
* 类加载器
* 虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,实现这个动作的代码称为¡°类加载器¡±,JVM提供了3种类加载器: 
* 1、启动类加载器(Bootstrap ClassLoader):负责加载 JAVAHOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 
* 2、扩展类加载器(Extension ClassLoader):负责加载 JAVAHOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。 
* 3、应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
* JVM基于上述类加载器,通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。 

* 双亲委派模型工作过程:当一个类加载器收到类加载任务,优先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
* 双亲委派模型有什么好处? 比如位于rt.jar包中的类java.lang.Object,无论哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,确保了Object类在各种加载器环境中都是同一个类。
* 验证
* 为了确保Class文件符合当前虚拟机要求,需要对其字节流数据进行验证,主要包括格式验证、元数据验证、字节码验证和符号引用验证。 
* 格式验证:验证字节流是否符合class文件格式的规范,并且能被当前虚拟机处理,如是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内、常量池是否有不支持的常量类型等。只有经过格式验证的字节流,才会存储到方法区的数据结构,剩余3个验证都基于方法区的数据进行。
* 元数据验证:对字节码描述的数据进行语义分析,以保证符合Java语言规范,如是否继承了final修饰的类、是否实现了父类的抽象方法、是否覆盖了父类的final方法或final字段等。
* 字节码验证:对类的方法体进行分析,确保在方法运行时不会有危害虚拟机的事件发生,如保证操作数栈的数据类型和指令代码序列的匹配、保证跳转指令的正确性、保证类型转换的有效性等。
* 符号引用验证:为了确保后续的解析动作能够正常执行,对符号引用进行验证,如通过字符串描述的全限定名是都能找到对应的类、在指定类中是否存在符合方法的字段描述符等。

准备准备阶段是正式为类变量分配内存并设置类变量初始值得阶段,这些变量所使用的内存都讲在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
* 在准备阶段,为类变量(static修饰)在方法区中分配内存并设置初始值。
* private static int var = 100;
* 准备阶段完成后,var 值为0,而不是100。在初始化阶段,才会把100赋值给val,但是有个特殊情况:
* private static final int VAL= 100;
* 在编译阶段会为VAL生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将VAL赋值为100。
* 初始化
* 初始化阶段是执行类构造器方法的过程,方法由类变量的赋值动作和静态语句块按照在源文件出现的顺序合并而成,该合并操作由编译器完成。
* 开始执行java代码(或者说字节码)


查看JVM使用的默认的垃圾收集器

查看步骤
cmd执行命令:
java -XX:+PrintCommandLineFlags -version

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器G1

上边新生代 下边老年代

标记-清除算法
复制算法
标记-整理算法
新生代 单线程
ParNew
新生代多线程
Parallel ScaVenge
新生代 复制算法 可控制的吞吐量 吞吐量优先
Serial old
老年代 单线程标记-整理算法
Parallel Old 多线程 标记-整理算法
cms
最短停顿时间为目标(快)
标记-清除 再整理
Serial old是cms的后备方案

算法过程:

  1. Eden+S0可分配新生对象;
  2. 对Eden+S0进行垃圾收集,存活对象复制到S1。清理Eden+S0。一次新生代GC结束。
  3. Eden+S1可分配新生对象;
  4. 对Eden+S1进行垃圾收集,存活对象复制到S0。清理Eden+S1。二次新生代GC结束。
  5. goto 1。
    1.S0与S1的区间明显较小,有效新生代空间为Eden+S0/S1,因此有效空间就大,增加了内存使用率
    2.有利于对象代的计算,当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后,会将其分到老年代中,设想一下,如果没有S0/S1,直接分成两个区,该如何计算对象经过了多少次GC还没被释放,你可能会说,在对象里加一个计数器记录经过的GC次数,或者存在一张映射表记录对象和GC次数的关系,是的,可以,但是这样的话,会扫描整个新生代中的对象, 有了S0/S1我们就可以只扫描S0/S1区了

在年轻代中经历了N次(可配置)垃圾回收后仍然存活的对象,就会被复制到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
针对年老代的垃圾回收即Full GC。

Jvm查看
对象的内布局
对象头 实例数据 对象填充
对象头 mark word 存储 64位或者32位指针
25 哈希吗
4 分代年龄
2 锁标志
轻量级 重量级 gc标志 可偏向
top
ps -ef | grep java
jps 条件 pid -q –m –l -v
jstat 条件 pid
-class -gc –gccapacity –gcutil –gccause -gcnew
-gcnewcapacity –gcold –gcoldcapacity -gcpermcapacity
-compiler -printcompilation
jmap 条件 pid
-dump –finalizerinfo –heap –histo –permstat -F

jvm dump 分析

mat 英文 memory analyzer tool
jvm dump 分析工具 (MAT)
Memory Analyzer tool
Histogram 所有实例的分配情况
Dominator Tree 堆的最大对象
Leak Suspecks 列出怀疑的内存泄漏处

-gc (jstat -gc pid 1000 5 )
S0C: Survivor0(幸存区0)大小(KB)
S1C: Survivor1(幸存区1)1大小(KB)
S0U: Survivor0(幸存区0)已使用大小(KB)
S1U: Survivor1(幸存区1)已使用大小(KB)
EC : Eden(伊甸区)大小(KB)
EU : Eden(伊甸区)已使用大小(KB)
OC :老年代大小(KB)
OU : 老年代已使用大小(KB)
PC : Perm永久代大小(KB)
PU : Perm永久代已使用大小(KB)
YGC:新生代GC个数
YGCT:新生代GC的耗时(秒)
FGC :Full GC次数
FGCT:Full GC耗时(秒)
GCT :GC总耗时(秒)

常用 jvm tomcat 配置参数

  1. 与串行回收器相关的参数
    -XX:+UseSerialGC:在新生代和老年代使用串行回收器。
    -XX:+SuivivorRatio:设置 eden 区大小和 survivor 区大小的比例。
    -XX:+PretenureSize :设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配 默认15。
    -XX:MaxTenuringThreshold:设置对象进入老年代的年龄的最大值。每一次 Minor GC 后,对象年龄就加 1。任何大于这个年龄的对象,一定会进入老年代。
  2. 与并行 GC 相关的参数
    -XX:+UseParNewGC: 在新生代使用并行收集器。
    -XX:+UseParallelOldGC: 老年代使用并行回收收集器。
    -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。
    -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。
    -XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。
    -XX:+UseAdaptiveSizePolicy:打开自适应 GC 策略。在这种模式下,新生代的大小,eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
  3. 与 CMS 回收器相关的参数
    -XX:+UseConcMarkSweepGC: 新生代使用并行收集器,老年代使用 CMS+串行收集器。
    -XX:+ParallelCMSThreads: 设定 CMS 的线程数量。

滴滴面试问到的 68 是什么的比例

XX:+CMSInitiatingOccupancyFraction:设置 CMS 收集器在老年代空间被使用多少后触发,默认为 68%。

XX:+UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收。
-XX:+CMSParallelRemarkEndable:启用并行重标记。
-XX:CMSInitatingPermOccupancyFraction:当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。
-XX:UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。
-XX:+CMSIncrementalMode:使用增量模式,比较适合单 CPU。
4. 与 G1 回收器相关的参数
-XX:+UseG1GC:使用 G1 回收器。
-XX:+UnlockExperimentalVMOptions:允许使用实验性参数。
-XX:+MaxGCPauseMills:设置最大垃圾收集停顿时间。
-XX:+GCPauseIntervalMills:设置停顿间隔时间。

1.手动调整

Xmn
-Xms
-XX:NewRatio=N
手动指定堆内存大小和代空间比例,一般要多次试验

2.自动参数调整

XX:MaxGCPauseMillis=N 可接受最大停顿时间
-XX:GCTimeRatio=N 可接受GC时间占比(目标吞吐量) 吞吐量=1-1/(1+N)
步骤:
1.MaxGCPauseMillis优先级高,JVM会自动调整堆大小和代空间值,以期满足MaxGCPauseMillis
2.当MaxGCPauseMillis满足后,JVM会增大堆大小,直到满足GCTimeRatio
3.当MaxGCPauseMillis和GCTimeRadio都满足后,JVM会尽可能以最小堆大小来实现这两个指标参数

Full GC原因:

并发模式失效:新生代发生GC时,老年代没有足够内存容纳晋升对象
晋升失败:老年代虽然有足够容纳晋升对象的内存,但内存都是碎片,导致晋升失败
*参数调整:避免并发模式失效和晋升失败
-XX:+UseCMSInitiatingOccupancyOnly 根据Old内存使用阈值决定何时CMS, 默认是false,会用更复杂的算法决定何时CMS
-XX:CMSInitingOccupancyFraction=N default N=70,老年代内存使用70%时就发生CMS
N设置太大,容易并发模式失效;N太小,CMS过于频繁,而CMS也会导致stop-the-world
-XX:ConGCThreads=N GC的线程会100%占用CPU,如果发生并发模式失败,而N还小于CPU核心数,此时可以增加N。
如果没有发生并发模式失败,此时可以减少N,以让应用程序有更多CPU执行
Perm持久代GC调优
持久代内存满了会引发Full GC
持久代GC调优主要是让持久代也进行CMS收集
-XX:+CMSPermGenSweepingEnable 使持久代使用CMS收集器
-XX:+CMSClassUnloadingEnable 使持久代能真正释放不再被使用的类。默认是不会释放类的元数据的
增量式CMS:普通CMS线程会占用100%的cpu负载,增量式CMS会让出一定CPU负载给应用线程
这适合在单核CPU使用,显然已经没啥用处了

1. -Xms128m  
2. -Xmx128m  
3. -XX:NewSize=64m  
4. -XX:PermSize=64m  
5. -XX:+UseConcMarkSweepGC  
6. -XX:CMSInitiatingOccupancyFraction=78 
7. -XX:ThreadStackSize=128-Xloggc:logs/gc.log  
8. -Dsun.rmi.dgc.server.gcInterval=3600000 
9. -Dsun.rmi.dgc.client.gcInterval=3600000 
10. -Dsun.rmi.server.exceptionTrace=true 

老年代(Old Generation)

老年代(Old Generation)老年代的GC实现要复杂得多。老年代内存空间通常会更大,里面的对象是垃圾的概率也更小。
老年代GC发生的频率比年轻代小很多。同时, 因为预期老年代中的对象大部分是存活的, 所以不再使用标记和复制(Mark and Copy)算法。而是采用移动对象的方式来实现最小化内存碎片。老年代空间的清理算法通常是建立在不同的基础上的。原则上,会执行以下这些步骤:

通过标志位(marked bit),标记所有通过 GC roots 可达的对象.
删除所有不可达对象
整理老年代空间中的内容,方法是将所有的存活对象复制,从老年代空间开始的地方,依次存放。

Minor GC vs Major GC vs Full GC

垃圾收集事件(Garbage Collection events)通常分为: 小型GC(Minor GC) - 大型GC(Major GC) - 和完全GC(Full GC) 。

小型GC(Minor GC)

年轻代内存的垃圾收集事件称为小型GC。这个定义既清晰又得到广泛共识。对于小型GC事件,有一些有趣的事情你应该了解一下:
1. 当JVM无法为新对象分配内存空间时总会触发 Minor GC,比如 Eden 区占满时。所以(新对象)分配频率越高, Minor GC 的频率就越高。
2. Minor GC 事件实际上忽略了老年代。从老年代指向年轻代的引用都被认为是GC Root。而从年轻代指向老年代的引用在标记阶段全部被忽略。
3. 与一般的认识相反, Minor GC 每次都会引起全线停顿(stop-the-world ), 暂停所有的应用线程。对大多数程序而言,暂停时长基本上是可以忽略不计的, 因为 Eden 区的对象基本上都是垃圾, 也不怎么复制到存活区/老年代。如果情况不是这样, 大部分新创建的对象不能被垃圾回收清理掉, 则 Minor GC的停顿就会持续更长的时间。
所以 Minor GC 的定义很简单 —— Minor GC 清理的就是年轻代。

Major GC vs Full GC

没有明确的定义
Major GC(大型GC) 清理的是老年代空间(Old space)。
Full GC(完全GC)清理的是整个堆, 包括年轻代和老年代空间。

很多 Major GC 是由 Minor GC 触发的, 所以很多情况下这两者是不可分离的。另一方面, 像G1这样的垃圾收集算法执行的是部分区域垃圾回收 回收区分也不是很明确
这也让我们认识到,不应该去操心是叫 Major GC 呢还是叫 Full GC, 我们应该关注的是: 某次GC事件 是否停止所有线程,或者是与其他线程并发执行。

一个线程OOM,进程里其他线程还能运行么?

正常linux 项目启动 oom error 就会造成项目停止
这问题正常也是回答 其他线程也会停止,总感觉oom 出问题都会停止

但是这个线程有时候真是特殊,一个线程oom并不一定其他的线程也停止。
既然一个线程oom,那它就会触发gc,gc回收后如果有足够的空间,并不会造成其他的线程停止。