JVM重点

  • Post author:
  • Post category:其他



目录


一、介绍JVM内存区域(运行时数据区)


二、如何判断对象已经死亡


三、强、软、弱、虚引用


四、垃圾收集算法及各自特点


五、常见的垃圾收集器


六、内存分配与回收策略


七、Class类文件结构


八、类加载机制


九、类加载器和双亲委派机制


十、静态分派和动态分派


十一、启动模式之client与server


十二、JVM进程有哪些线程启动


十三、Java8的Metaspace(元空间)


十四、执行引擎


十五、String


一、介绍JVM内存区域(运行时数据区)

JVM中内存分为若干部分:堆、方法区、虚拟机栈、本地方法栈、程序计数器。其中堆和方法区是线程共享的部分,其他是线程私有的。

1、堆

Java堆是用来存放

实例对象和数组对象

的,由于存在逃逸分析技术(分析这个对象会不会被其他方法或者线程调用),也可以把对象的属性打散分配在栈上,前提是开启标量替换,随着出栈而销毁,同时,

Java堆也是垃圾回收的主要区域

,由于现在垃圾收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代。Java堆在物理上可以是不连续的,只要逻辑上连续就好。在对上分配对象的方法有:指针碰撞和空闲列表,前者是在堆内存规整的情况下,所有用过和空闲的内存中间有明确的分界线,而后者采用空闲列表来记录内存的使用情况,规整是由垃圾回收器是否压缩决定。空间不足时抛出OOM。

堆上对象的访问方式:通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。句柄的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。直接指针的话,reference中存储的直接就是对象的地址。

2、方法区

方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类的信息:常量、静态变量(jdk7移到堆中)、即时编译器编译后的代码、运行时常量池(jdk7将字符串常量池移到堆中)等数据,其中运行时常量池用于存放编译器生成的各种字面量和符号引用(字面量:1.文本字符串 2.八种基本数据类型的值 3.被声明为final的常量等。符号引用:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符)运行时常量池对于Class常量池具有动态性,可以在运行期间利用intern方法将常量放入池中。空间不足抛出OOM。

3、虚拟机栈

虚拟机栈是Java方法执行的内存模型,线程私有,每个方法执行都会创建一个栈帧,用于存储

局部变量表、操作数栈

、动态链接(运行时将方法区的符号引用转化为直接引用,可以直接调用)和方法返回地址以及附加信息(用于调试的信息),每一个方法从调用结束直至执行结束,就对应着一个栈桢从虚拟机栈中入栈到出栈的过程。StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。OutOfMemoryError:如果虚拟机可以动态扩展,而扩展时无法申请到足够的内存。

4、本地方法栈

JVM调用本地方法(可以为其他语言提供接口)

5、程序计数器

记录当前线程所执行到的字节码行号。每个线程都有一个程序计数器,唯一没有OOM的内存区域。


运行时内存区域外规定的堆外内存

:直接使用Nativ函数库直接分配堆外内存,然后通过DirectByteBuffer对象作为这块内存引用进行操作。这样就能在一些场景中显著提高性能,

因为避免了Java堆和Native堆之间来回复制数据

二、如何判断对象已经死亡

引用计数法和可达性分析法

1、引用计数法

每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。目前主流的虚拟机并没有选择这个算法来管理内存,其最主要的原因就是很难解决对象之间的循环引用问题。而python支持,通过手动解除或者使用弱引用。

2、可达性分析法

这个算法的基本思想就是通过一系列的称为“

GC Roots

”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到“

GC Roots

”没有任何引用链的时候,则证明这个对象是不可用的。

可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法中引用的对象。

3、真的死了?

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候他们暂时处于“缓刑阶段”,要真正宣告

一个对象死亡,至少要经历两次标记过程

:可达性分析法中对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有重写finalize()方法,或者已经执行过了,那么虚拟机将这两种情况视为没有必要执行,就被回收了。而被判定为需要执行的对象将会放入到一个队列中进行

第二次标记

,除非在finalize()方法中这个对象又与引用链上的任何一个对象建立联系(复活了,假如再一次GC,那么会直接被回收),否则就会被真的回收。

三、强、软、弱、虚引用

1、强引用

如果一个对象具有强引用,垃圾回收器绝对不会回收它。当空间不足时,Java虚拟机宁愿抛出OOM错误,使程序异常终止也不会回收。

2、软引用

如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象。重要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可以用来实现内存敏感的高速缓存。

3、弱引用

相对于软引用,弱引用关联的对象只能生存到下一次垃圾回收之前。

4、虚引用

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,任何时候都可能被垃圾回收,而且调用其get()方法返回的是null。其主要作用是来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(

ReferenceQueue 构造方法)联合使用

。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前把这个虚引用加入到与之关联的引用队列中,这样就可以通过判断引用队列是否加入虚引用来了解对象是否将要被垃圾收集器回收,可以做一些在回收之前的必要活动。

虚与软、弱引用的区别:软、弱引用都是在引用对象被垃圾回收后,Java虚拟机才把引用加到与之关联的引用队列总,而虚引用在垃圾回收器前,Java虚拟机就把引用加入到与之关联的引用队列中。

四、垃圾收集算法及各自特点

1、标记-清除算法

首先

标记所有从GC Roots可以到达的对象(对象头中),在标记完成后遍历堆,统一回收所有没有被标记的对象

。它是最基础的收集算法,但是会带来两个明显的问题:1、效率问题 2、空间问题(标记-清除后会产生内存碎片)。

2、标记-复制算法

将内存分为大小的两块,每次只用其中的一块。主要针对Java虚拟机中新生代,进行一次GC的时候,先标记Eden区和from区中间接或者直接与GC Roots具有引用链的对象,然后将他们复制到to区,然后Eden和from区回收,此时from成为to,to成为from(谁空谁是to);如果需要复制的对象大于to区剩余空间,则可以让其直接进入老年代(分配担保策略)。

3、标记-整理算法

标记过程都是一样的,后续就是将标记了的对象进行移动填补碎片区域,所以最后效果就是没有内存碎片。

4、分代收集算法(结合以上)

比如在新生代中,大多数对象都是朝生夕死,所以标记-复制最合适不过了,因为每次只需要复制很少的对象就可以了。

而老年代的对象存活几率时比较高的,所以可以选择“标记-清除”或者“标记-整理”算法进行收集。

五、常见的垃圾收集器

垃圾收集器主要有:

Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS(Concurrent-Mark-Sweep)、G1(Garbage First

)。

1、Serial收集器:单线程收集器,不仅仅意味着它只会使用一条垃圾收集线程区完成垃圾收集工作,重要的是它在进行垃圾收集工作的时候必须暂停其他所有线程(

STW-Stop The World

),直到收集结束。

2、ParNew收集器其实就是Serial收集器的多线程版本,随着CPU核心数增加,可以显示出优势(并行)。

3、Parallel Scavenge收集器使用标记-复制算法,并行的多线程收集器。与ParNew类似,但是侧重点是吞吐量(CPU运行用户代码时间/CPU运行总时间)。可以设置参数来调整最大垃圾收集停顿时间和吞吐量的大小(-XX:MaxGCPauseMillis—–>设置最大停顿时间,-XX:GCTimeRatio—–>设置垃圾收集占总时间比例,以此调整吞吐量大小)。

//以上是新生代收集器,一般采用标记-复制算法

4、Serial Old是Serial收集器的老年代版本,它同样是单线程收集器(标记-整理)。

5、Parallel Old是Parallel Scavenge收集器的老年代版本。使用多线程和

标记-整理

算法。在

注重吞吐量以及CPU资源配合

的场合,都可以优先考虑Parallel Scavenge + Paralle Old搭配。

6、CMS(Concurrent-Mark-Sweep)收集器,老年代收集器,采用

标记-清除

算法,是一种以

获取最短回收停顿时间为目标的收集器

。它非常符合注重用户体验的应用上使用(jdk9废弃,jdk14移除)。这个过程有四个步骤:初始标记、并发标记、重新标记和并发清除。

初始标记:暂停所有用户线程,并记录下直接与GC Roots相连的对象,速度很快。

并发标记:同时开启GC线程和用户线程,从GC Roots继续向下进行标记,但是用户线程会同时运行。

重新标记:重新标记阶段就是为了修正并发标记期间因为用户线程运行而导致标记产生变动。这个阶段的停顿时间一般会比初始标记阶段的时间稍长,但是比并发标记阶段时间短。

并发清除:GC线程开始对没有标记的对象进行清理,释放空间,此阶段与用户线程并发执行。

CMS优点:1、并发收集 2、低延迟

缺点:1、

会产生内存碎片

,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC 2、

对CPU资源非常敏感

,在并发阶段,它虽然不会导致用户线程停止,但是会占用一部分线程导致应用程序变慢,吞吐量下降 3、

无法处理浮动垃圾

(在并发标记阶段由于用户线程也在并发运行,可能会产生新的垃圾),那就只能等下次GC了,因此在jdk9废弃,jdk14移除了。

7、G1收集器:唯一一个同时可以用于新生代和老年代的垃圾收集器(延迟可控的情况下获得尽可能高的吞吐量),采用标记-整理算法(整体,但因为其将内存划分为一个个大小不等的Region,Region之间采用复制算法),并且维护了一个

优先列表

,每次根据允许的收集时间,优先选择回收价值最大的一个或多个Region,把内存化整为零,但是由于引用关系的存在(可能新生代的对象引用了老年代的对象),仍然存在如何避免全局扫描的问题,这里采用一个Region用一个Remembered Set进行记录引用关系,避免可达性分析阶段进行全堆扫描。

G1大致分为四个步骤:初始标记、并发标记、最终标记、筛选回收。

初始标记、并发标记和CMS收集器相似,最终标记将并发阶段对象变化记录在线程Remembered Set Logs中,最终把Remembered Set Logs中的数据合并到Remembered Set中,这一阶段用户线程会停止,但是GC线程可以并行执行。筛选回收对每一个Region的回收价值加和成本进行筛选,根据用户期望的GC停顿时间,得到最好的回收方案并回收。

特点:并发性强、分代收集、标记-整理进行空间整合,可以预测停顿时间。


因此在选择垃圾收集器时

1、单CPU环境下的Client模式(响应度优先),选择Serial + Serial Old

2、多CPU环境并且注重用户体验(响应度优先),ParNew + CMS

3、后台计算较多不需要太多的交互(吞吐量优先),Parallel Scavenge + Parallel Old

4、面向服务端应用,G1

六、内存分配与回收策略

大多数情况下,对象在新生代的Eden区分配。当Eden区没有足够的空间分配时,Java虚拟机将会进行一次Minor GC。

1、Minor GC和Full GC有什么不同

Minor GC:指的是发生在新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。

Major GC:指的是发生在老年代的GC。

Full GC:清理整个堆甚至方法区(许多Major GC是由于Minor GC触发的,Major GC比Minor GC慢10倍以上)。

2、什么时候对象进入老年代

  • 大对象(如大数组,需要大量内存连续的)直接进入老年代
  • 空间分配担保(前提是老年代具有足够的空间):当进行一次Minor GC后,Eden区存活的对象和from区存活的对象将会复制到to区,如果to区填满了,则多余的对象直接进入老年代;安全的Minor GC:老年代中最大可用的连续空间大于新生代所有对象的空间;冒险的Minor GC:老年代中最大可用的连续空间大于历代晋升到老年代的平均水平且允许担保失败,如果小于平均值,则直接进行一次Full GC,让老年代腾出空间
  • 年龄计数器会为对象记录年龄,每次经过一次GC仍然存活的,年龄加一,当超过设定的值(默认是15,最大值也只能是15,因为对象头中用四位bit记录),直接进入老年代;或者动态对象年龄判断,如果幸存区空间中相同年龄对象的对象总和大于幸存区的一半,则大于或者等于这个年龄的对象可以直接进入老年代,无需等待达到设定的值。

七、Class类文件结构

1、Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础构成之一。

包括:魔术和版本号、常量池(注意和运行时常量池的区别,存放字面量(文本字符串和被声明为final的常量等)和符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符))、访问标志(类定义标志)、类索引、父类索引和接口索引的集合(即确定一个类的继承关系)、字段表集合、方法表集合、属性表。

2、字段表包含什么信息(定义一个字段的格式)

字段的作用域(public、private、protected修饰符),是实例变量还是类变量(static修饰符),可变性(final),并发可见性(volatile),是否可被序列化(transient修饰符),字段数据类型和字段名称(引用常量池中常量表示)。

八、类加载机制

Java虚拟机把描述类的数据从

class文件加载到内存

,并对数据进行校验、转换解析和初始化。类加载过程:加载、验证、准备、解析、初始化。

1、加载

通过类型的全限定名,产生一个代表该类型的二进制数据流;解析这个二进制数据流的静态存储结构转化为方法区内的运行时数据结构;创建一个表示该类型的java.lang.Class类的实例,

作为方法区这个类的各种数据的访问入口

2、验证

为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。

3、准备

正式为类变量(static修饰的)分配内存并设置变量初始值(默认赋值),这些变量所使用的内存在jdk1.7开始已经在堆中分配了,方法区只是引用。

4、解析

虚拟机将常量池内的符号引用替换为直接引用的过程。主要针对类或接口、字段、类方法、接口方法的解析,主要是静态链接,方法主要是静态方法和私有方法。

5、初始化

开始真正执行定义的Java代码。执行Clinit()方法,该方法会收集所有类变量的赋值动作和静态语句合并产生,首先会执行父类的。

九、类加载器和双亲委派机制

1、启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在/lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。

2、扩展类加载器(Extension ClassLoader):这个类加载器是由sun.misc.Launcher$ExtClassLoader实现,它负责加载/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。

3、应用程序类加载器(Application ClassLoader):这个类由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上指定的类库。

4、双亲委派机制:如果一个类加载器收到了类加载的请求,先把这个请求委派给父类去完成(所以所有的加载请求都应该传送到顶层的

启动类加载器

中),只有当父类加载器无法完成加载时,子类才会尝试去自己加载(原因就是为了保护Java体系,防止恶意加载)。

十、静态分派和动态分派

1、静态分派:依赖静态类型定位方法的分派,发生在编译时期,典型应用为方法的重载(重载的参数是通过静态类型确定的,直接调用父类)。

2、动态分配:在运行时期根据

实际类型确定方法的分派

,发生在程序运行时,典型应用是方法的重写,也是堕胎的一种体现。根据转型来确定是调用父类还是子类的方法。

虚方法和非虚方法:

  • 非虚方法(所有static方法+final/private方法)通过invokespecial指令调用,对这个非虚方法的符号引用将转为对应的直接引用,即转为直接引用方法,在编译期完成时就确定的唯一调用方法。
  • 虚方法是通过invokevirtual指令调用,且会有

    静态或者动态分派

    。具体根据编译期时方法接收者和方法参数的静态类型来分派,再在运行期只根据方法接收者的实际类型来分派。

十一、启动模式之client与server

1、指定jvm启动模式

jvm启动时,通过-server或-client参数指定启动模式。

2、client模式和server模式的区别

  • 编译器方面:当虚拟机运行在client模式时,使用的是一个代号为c1的轻量级编译器,而server模式启动时,虚拟机采用的是相对重量级,代号为c2的编译器;c2编译器比c1编译器编译的相对彻底。服务器起来之后,性能更高。
  • GC方面:client模式下的新生代(Serial收集器)和老年代(Serial Old)选择的是

    串行GC

    ,server模式下的新生代和老年代都选择

    并行GC

  • 启动方面:client模式启动快,编译快,内存占用少,针对桌面应用程序设计,优化客户端环境的启动时间。server模式启动慢,编译更完全,编译器是自适应编译器,效率高,针对服务端应用设计,优化服务器环境的最大化程序执行速度。注:一般来说系统应用选择有两种方式:吞吐量优先和停顿时间优先,对于吞吐量优先的采用server默认的并行GC(Parallel Scavenge),对于停顿时间优先的选择并发GC(CMS)。

十二、JVM进程有哪些线程启动

1、main,主线程

2、Reference Handler,处理引用的线程,用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题

3、Finalizer,调用对象的finalize()方法的线程,就是垃圾回收的线程

4、Singal Dispatcher,分发处理发送给JVM信号的线程

5、Attach Listener,负责接收外部的命令的线程

十三、Java8的Metaspace(元空间)

方法区是所有线程共享。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区是JVM的规范,永久代(PermGen space)是HotSpot对这种规范的实现。在jdk8中,HotSpot已经没有永久代,取而代之的是Metaspace(元空间)。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别是:元空间并不在虚拟机中,而是使用本地内存。

为什么要使用原空间替代永久代:

1、字符串存在永久代中,容易出现性能问题和内存溢出。

2、类及方法的信息等比较难确定其大小,因此对于永久代的大小难以指定,太小容易OOM,太大容易导致老年代OOM。

3、永久代会为GC带来不必要的复杂度,并且回收频率偏低。

十四、执行引擎

执行引擎的工作过程:

Java代码执行过程,绿色为解释器执行流程,蓝色为即时编译器(JIT)执行流程



解释器:当Java虚拟机启动时会根据预定义的规范对字节码进行逐行解释的方式执行,将每条字节码中的内容“翻译”为对应平台的机器指令执行。


JIT编译器:虚拟机直接将源代码编译成和本地机器平台相关的机器语言。


所以HotSpot是解释器和JIT共同工作

十五、String

如果字符串进行了拼接:



StringBuilder进行拼接的字符串最终是不会放到字符串常量池中的

public class Test2 {
    public static void main(String[] args) {
        String s = "a";
        String s1 = new String("b");
        String s2 = s + s1;
//        s2.intern();
        String s3 = "ab";
        System.out.println(s2 == s3);
    }
}


如这段代码,注释打开和没打开是两种结果,原因就是下面的对intern()方法是解释。


intern()方法:



版权声明:本文为qq_41841130原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。