Java核心之JVM

1 JVM

1.1 源码到类文件:

1.2 类文件到JVM:类加载机制

1.2.1 装载(Load)

1.2.2 链接

1.2.3 初始化(Initialize)

1.2.4 类加载器ClassLoader

2 运行时数据区(Run-Time Data Areas)

2.1 方法区(共享——线程不安全)

2.2 堆(共享——线程不安全)

2.3 虚拟机栈(私有——线程安全)

2.3.1 关于虚拟机栈

2.3.2 栈帧

2.4 本地方法栈(私有——线程安全)

2.5 程序计数器(私有——线程安全)

3 JVM内存模型

3.1 堆区

3.2 垃圾回收(GC)

3.2.1 如何确定一个对象是垃圾?

3.2.2 垃圾回收算法

3.2.3 分代收集算法

3.2.4 垃圾收集器

3.2.5 垃圾收集器分类

4 JVM参数与命令

4.1 JVM参数

4.1.1 标准参数

4.1.2 -X参数

4.1.3 -XX参数

4.1.4 其他参数

4.1.5 设置参数的常见方式

4.2 常用命令

4.3 常用工具

4.3.1 参数可视化工具

4.3.2 查看内存相关信息的工具

4.3.3 查看GC日志的工具

5 性能优化

5.1 内存溢出(OOM)

5.2 GC优化(G1调优)

最终目标:高吞吐量 低停顿时间

5.3 常见问题


1 JVM

 最底层的即为JVM。

JDK JRE 与 JVM。

1.1 源码到类文件:

.class文件 为16机制表示的文件。里面的内容包括了JDK格式、源码继承与接口的情况、属性与方法的数量等。

1.2 类文件到JVM:类加载机制

所谓类加载机制即:

虚拟机把Class文件加载到内存
并对数据进行校验,转换解析和初始化
形成可以虚拟机直接使用的Java类型,即java.lang.Class

1.2.1 装载(Load)

查找和导入class文件
(1)通过一个类的全限定名获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

1.2.2 链接

(1)验证(Verify)

保证被加载类的正确性

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


(2)准备(Prepare)
为类的静态变量分配内存,并将其初始化为默认值

(3)解析(Resolve)
把类中的符号引用转换为直接引用

符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

1.2.3 初始化(Initialize)

对类的静态变量,静态代码块执行初始化操作

1.2.4 类加载器ClassLoader

【常问问题:】怎样装载?

使用类加载器装载。

不同的类加载器加载不同目录下的类文件。

在装载(Load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的。

(1)分类(不同的类加载器负责不同的目录层级)

(2)类加载原则【双亲委派】

双亲委派机制:解决处于不同位置的同一个类只被加载一次的问题。

(1)检查某个类是否已经加载
自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个Classloader已加载,就视为已加载此类,保证此类只所有ClassLoader加载一次。
(2)加载的顺序
自顶向下,也就是由上层来逐层尝试加载此类。

当一个类加载器收到类加载请求时,先送到最上层的父类处进行加载。

如果父类已经加载了,那么不加载,

如果没有加载,则交给下一层的孩子去加载。

保证了一个权限名目录的一个类在系统内只会被加载一次。

(3)破坏双亲委派机制

可重写ClassLoader中的类加载方法,改变类加载顺序。即可破坏双亲委派机制。

2 运行时数据区(Run-Time Data Areas)

运行时数据区的生命周期一部分与JVM进程一致(方法区与堆),

另一部分与线程一致(虚拟机栈、本地方法栈、程序计数器)。

运行时常量池是在方法区里面进行分配的。

因此放在方法区里面。运行时数据区由上面5部分组成。

2.1 方法区(共享——线程不安全)

(1)只有一个方法区,方法区是各个线程共享的内存区域,在虚拟机启动时创建

(2)虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来

(3)用于存储已被虚拟机加载的类信息(类的结构信息常量,包括静态变量、即时编译器编译后的代码等数据)

(4)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

(5)生命周期与JVM绑定在一起。

(6)运行时常量池(Run-Time Constant Pool)在方法区分配。

【另】:在JDK8中称为 Metaspace  (元空间); JDK6或7中称为Perm Space(永久代)。

方法区是线程不安全的。

2.2 堆(共享——线程不安全)

(1)Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。

(2)Java对象实例以及数组都在堆上分配。

(3)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

(4)生命周期与JVM绑定在一起。

2.3 虚拟机栈(私有——线程安全)

2.3.1 关于虚拟机栈

表示每一个线程执行的状态。

每一个Java线程都会有一个自己私有的Java虚拟机栈。

(1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。(线程----->虚拟机栈)

(2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法的执行对应一个栈帧的产生。(虚拟机栈------>栈帧(frames))

(3)调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

(4)其生命周期与线程绑定在一起。

(5)当调用的深度较大的,会导致StackOverflowError.

例如如下代码的一个线程:

void a(){
b();
}
void b(){
c();
}
void c(){
}

方法的调用对应着栈帧的进栈与出栈。

2.3.2 栈帧

栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
每个栈帧中包括四部分。

(1)局部变量表(Local Variables)

(2)操作数栈(Operand Stack)

(3)动态链接——指向运行时常量池的引用(A reference to the run-time constant pool)

(4)方法返回地址(Return Address)和附加信息。

栈帧即为了更好的表示一个方法的执行。

局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。
操作数栈:以压栈和出栈的方式存储操作数的
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。

 

2.4 本地方法栈(私有——线程安全)

Native Method Stacks。

如果当前线程执行的方法是Native(本地方法库的方法)类型的,这些方法就会在本地方法栈中执行。
那如果在Java方法执行的时候调用native的方法呢?

2.5 程序计数器(私有——线程安全)

the PC register

用于指向当前线程正在执行方法所在的位置。

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。
假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。

(1)程序计数器所占空间很小。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器(线程私有)。

(2)如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
(3)如果正在执行的是Native方法,则这个计数器为空。

3 JVM内存模型

JVM内存模型即包含 非堆区(方法区)和堆区。

3.1 堆区

(1)堆区分为两大块,一个是Old区,一个是Young区
(2)Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区
         S0和S1一样大,也可以叫From和To。

在同一个时间点上,S0和S1只能有一个区是有数据,另外一个是空的

如果S区存不下了,就会触发担保机制。S区和Old区借一些空间暂时存放对象。

Eden:s0:s1=8:1:1

①为什么需要Survivor区?只有Eden不行吗?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。
这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。
执行时间长有什么坏处?频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。

假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长。
假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

 ②为什么需要两个Survivor区?

最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些
存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。

③堆内存中都是线程共享的区域吗?

JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:ThreadLocal Allocation Buffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。

④为什么是8:1:1.如果设计为6:2:2会怎样?

新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1区为8:1
即新生代中Eden:S1:S2 = 8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是“朝生夕死”的。

如果是6:2:2的话,Eden区只有60%。而大多数在Young区的对象是朝生夕死的,这样的话就没有足够的空闲分配新对象,这样就会有20%的空间浪费。因此不合理。(Survivor区的作用是保证Eden区的连续性,减少空间碎片,从而存下更多的大对象。否则就会过多的触发GC)

Young区的对象每经过一次GC,年龄就会+1。当S区的年龄>阈值(默认为15)时,就会从Young区到Old区。

如果堆总大小为3000M,old区的大小为2000M,Young区的大小为1000M,一个新对象的大小过大(大对象),为600M,那么它有可能被直接分配到Old区。

如上图,GC一共分为3种。

如何理解Minor/Major/Full GC

Minor GC:新生代
Major GC:老年代
Full GC:新生代+老年代

3.2 垃圾回收(GC)

自动垃圾回收机制就是寻找Java堆中的对象,并对对象进行分类判别,
寻找出正在使用的对象和已经不会使用的对象,然后把那些不会使用的对象从堆上清除 。

3.2.1 如何确定一个对象是垃圾?

要想进行垃圾回收,得先知道什么样的对象是垃圾。

(1) 引用计数法

对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。
弊端 :如果AB相互持有引用,导致对象永远不能被回收。

(2) 可达性分析

通过GC Root(通常不止一个)的对象,开始向下寻找,看某个对象是否可达。

哪些能作为GC Root:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。

3.2.2 垃圾回收算法

(1)标记-清除(Mark-Sweep)

标记
找出内存中需要回收的对象,并且把它们标记出来
此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时

清除
清除掉被标记需要回收的对象,释放出对应的内存空间

弊端:

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

(2)标记-复制(Mark-Copying)

将内存划分为两块相等的区域,每次只使用其中一块,如下图所示:

 当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。

弊端:

 空间利用率降低。

(3)标记-整理(Mark-Compact)

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况,所以老年代一般不能直接选用这种算法。

标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。其实上述过程相对"复制算法"来讲,少了一个"保留区"

让所有存活的对象都向一端移动,清理掉边界意外的内存。

3.2.3 分代收集算法

Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)

3.2.4 垃圾收集器

不同的垃圾收集器可以回收不同区域。

G1比较特殊,既可以应用于新生代,也可以应用于老年代。

新生代实现的垃圾回收算法即复制算法。

老年代实现的垃圾回收算法即清除或整理算法。

(1)Serial

优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器

(2)Serial Old

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算法",运行过程和Serial收集器一样。

(3)ParNew
[可以把这个收集器理解为Serial收集器的多线程版本。]
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器

(4)Parallel Scavenge

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量。


吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。

(5)Parallel Old
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回
收,也是更加关注系统的吞吐量。

(6)CMS(并发类的收集器)

CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间(STW) 为目标的收集器。

采用的是"标记-清除算法"。

由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来
说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量

(7)G1(Garbage-First)——(更关注停顿时间)
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

根据用户设置的(预期的)停顿时间,有选择地回收。

每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中

设置Region大小:-XX:G1HeapRegionSize=M
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域

3.2.5 垃圾收集器分类

(1)串行收集器->Serial和Serial Old
只能有一个垃圾回收线程执行,用户 线程暂停。(单线程)
适用于内存比较小的嵌入式设备 。

(2)并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old
多条垃圾收集线程 并行工作,但此时用户线程仍然处于等待状态。 
适用于科学计算、后台处理等若交互场景 。

(3)并发收集器[停顿时间优先]->CMS、G1
用户线程和垃圾收集线程 同时执行【并发】(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。
适用于相对时间有要求的场景,比如Web 。

(4)吞吐量与停顿时间

吞吐量和停顿时间
停顿时间->垃圾收集器  进行 垃圾回收终端应用执行响应的时间
吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;
高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

小结 :这两个指标也是评价垃圾回收器好处的标准。

4 JVM参数与命令

4.1 JVM参数

4.1.1 标准参数

即参数不随JDK版本的不同而不同。

-version
-help
-server
-cp

4.1.2 -X参数

非标准参数,也就是在JDK各个版本中可能会变动

-Xint   解释执行
-Xcomp  第一次使用就编译成本地代码
-Xmixed  混合模式,JVM自己来决定

4.1.3 -XX参数

使用得最多的参数类型
非标准化参数,相对不稳定,主要用于JVM调优和Debug

a.Boolean类型
格式:-XX:[+-]<name>      +或-表示启用或者禁用name属性

比如:-XX:+UseConcMarkSweepGC  表示启用CMS类型的垃圾回收器
-XX:+UseG1GC       表示启用G1类型的垃圾回收器

b.非Boolean类型
格式:-XX<name>=<value>表示name属性的值是value

比如:-XX:MaxGCPauseMillis=500

-XX:InitialHeapSize=100M

4.1.4 其他参数

-Xms1000M等价于-XX:InitialHeapSize=1000M
-Xmx1000M等价于-XX:MaxHeapSize=1000M
-Xss100等价于-XX:ThreadStackSize=100

4.1.5 设置参数的常见方式

开发工具中设置比如IDEA,eclipse
运行jar包的时候:java -XX:+UseG1GC xxx.jar
web容器比如tomcat,可以在脚本中的进行设置
通过jinfo实时调整某个java进程的参数(参数只有被标记为manageable的flags可以被实时修改)

4.2 常用命令

(1) jps:查看java进程

(2)jinfo:查看某个java进程目前的参数设置的情况

①实时查看和调整JVM配置参数

②查看用法
jinfo -flag name PID 查看某个java进程的name属性的值

jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID

③修改
参数只有被标记为manageable的flags可以被实时修改

jinfo -flag [+|-] PID
jinfo -flag <name>=<value> PID

④查看曾经赋过值的一些参数

jinfo -flags PID

(3) jstat:查看当前java进程的统计信息

jstat -gc PID 1000 10   //查看垃圾收集信息

(4) jstack:检查当前java进程的堆栈信息

可以用于排查死锁案例

用法:jstack PID

DeadLockDemo:

//运行主类
public class DeadLockDemo
{
  public static void main(String[] args)
 {
    DeadLock d1=new DeadLock(true);
    DeadLock d2=new DeadLock(false);
    Thread t1=new Thread(d1);
    Thread t2=new Thread(d2);
    t1.start();
    t2.start();
 }
}
//定义锁对象
class MyLock{
  public static Object obj1=new Object();
  public static Object obj2=new Object();
}
//死锁代码
class DeadLock implements Runnable{
  private boolean flag;
  DeadLock(boolean flag){
    this.flag=flag;
 }
  public void run() {
    if(flag) {
      while(true) {
        synchronized(MyLock.obj1) {
          System.out.println(Thread.currentThread().getName()+"----if
获得obj1锁");
          synchronized(MyLock.obj2) {
            System.out.println(Thread.currentThread().getName()+"---
-if获得obj2锁");
         }
       }
     }
   }
    else {
      while(true){
        synchronized(MyLock.obj2) {
          System.out.println(Thread.currentThread().getName()+"----否则
获得obj2锁");
          synchronized(MyLock.obj1) {
            System.out.println(Thread.currentThread().getName()+"---
-否则获得obj1锁");
         }
       }
     }
   }
 }
}

(5) jmap:

打印出堆转存储的快照:

jmap -heap PID

dump出堆内存相关信息:

jmap -dump:format=b,file=heap.hprof PID

4.3 常用工具

4.3.1 参数可视化工具

jconsole

jvisualvm

arthas

4.3.2 查看内存相关信息的工具

mat

perfma(在线工具)

4.3.3 查看GC日志的工具

gceasy.io

gcviewer

5 性能优化

5.1 内存溢出(OOM)

一般会有两个原因:
(1)大并发情况下
(2)内存泄露导致内存溢出

5.2 GC优化(G1调优)

(1)使用G1GC垃圾收集器: -XX:+UseG1GC
修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

Throughput    Min Pause    Max Pause   Avg Pause    GC count
99.16%             0.00016s     0.0137s         0.00559s       12

(2)调整内存大小再获取gc日志分析

-XX:MetaspaceSize=100M
-Xms300M
-Xmx300M

比如设置堆内存的大小,获取到gc日志,使用GCViewer分析吞吐量和响应时间

Throughput    Min Pause    Max Pause   Avg Pause    GC count
98.89%              0.00021s    0.01531s       0.00538s      12

(3)调整最大停顿时间

-XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标

比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间

hroughput    Min Pause    Max Pause   Avg Pause    GC count
98.96%          0.00015s      0.01737s      0.00574s       12

(4)启动并发GC时堆内存占用百分比

-XX:InitiatingHeapOccupancyPercent=45
G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行
GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).

性能优化指南:

最终目标:高吞吐量 低停顿时间

5.3 常见问题

(1)内存泄漏与内存溢出的区别

内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。

(2)young gc会有stw吗?

不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时间相对来说就小很多了。

(3)major gc和full gc的区别

Major GC在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC和 Full GC。一般情况下,一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触发 Full GC 的原因有很多:当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大时,会触发 Full GC;当老年代的空间使用率超过某阈值时,会触发 Full GC;当元空间不足时(JDK1.7永久代不足),也会触发 Full GC;当调用 System.gc() 也会安排一次 Full GC。

(4)什么是直接内存

Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

(5)垃圾判断的方式

引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况。
引用链法: 通过一种GC ROOT的对象(方法区中静态变量引用的对象等-static变量)来判断,如果有一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收。

(6)不可达的对象一定要被回收吗?

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

(7)为什么要区分新生代和老年代?

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。

而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

(8)G1与CMS的区别是什么

CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 MixGC;

G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生;

在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。

(9)方法区中的无用类回收

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。

类需要同时满足下面 3 个条件才能算是 “无用的类” :

a-该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
b-加载该类的 ClassLoader 已经被回收。
c-该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

猜你喜欢

转载自blog.csdn.net/paranior/article/details/114982680