一、JVM内存结构

java 永久代对象 jvm 永久代存放了什么_CMS

1、方法区(Method Area)

别名Non-Heap(非堆)、永久代(Permanent Generation)、持久代(PermGen),各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、以及编译器编译后的代码等数据(jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中)。
:在Java 8里已被废除了,被元空间取代;

2、Java堆(Java Heap)

Java堆也是线程共享的一块内存区域,此内存区域的唯一目的就是存放对象实例(对象和数组),可以想象一个系统会产生很多实例,因此java堆的空间也是Java虚拟机所管理的内存中最大的一块。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )。

3、Java栈(Java Stacks)

虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应 Java 代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个 Java 方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。
如果java栈空间不足了,程序会抛出StackOverflowError异常,递归深度过深,会执行大量的方法,方法越多java栈的占用空间越大,常常会引起StackOverflowError异常。

4、本地方法栈(Native Method Stacks)

本地方法栈角色和java栈类似,也是线程私有的,只不过它是用来执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。

5、程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,他也是线程私有的,它的作用可以看做是当前线程所执行的字节码的行号指示器,控制程序指令的执行顺序。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

6、元空间

上面说到,jdk1.8 中,已经不存在永久代(方法区),替代它的一块空间叫做 “ 元空间 ”,和永久代类似,都是 JVM 规范对方法区的实现,但是元空间并不在虚拟机中,而是使用本地内存,元空间的大小仅受本地内存限制,但可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来指定元空间的大小。

二、类加载

java 永久代对象 jvm 永久代存放了什么_java 永久代对象_02


类装载流程可以概括为:加载–>连接–>初始化–>使用–>卸载。

我们主要介绍这一过程的主角:类加载器ClassLoader,它是一个抽象类,负责整个类装载流程中的“加载”阶段。ClassLoader的具体实例负责把java字节码读取到JVM当中,ClassLoader还可以自定义以满足不同字节码流的加载方式,比如从网络加载、从文件加载。

1、类加载器

java 永久代对象 jvm 永久代存放了什么_加载_03


注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

类加载器中启动类加载器属于 JVM 的一部分,其他类加载器都用 java 实现,并且最终都继承自 java.lang.ClassLoader。

(1)、启动类加载器

Bootstrap ClassLoader,是由 C/C++ 编译而来的,看不到源码,所以在 java.lang.ClassLoader 源码中看到的 Bootstrap ClassLoader 的定义是 native 的 private native Class findBootstrapClass(String name);负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。具体加载哪些类可以通过 System.getProperty(“sun.boot.class.path”) 来查看。

(2)、扩展类加载器

Extension ClassLoader,负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。可以用通过 System.getProperty(“java.ext.dirs”) 来查看具体都加载哪些类。

(3)、应用程序类加载器

Application ClassLoader,负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

(4)、自定义加载器

应用程序都是由以上三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此可以通过编写自己的ClassLoader,实现从特定的场所取得java class(例如数据库中和网络中)等功能。

2、类加载原则

全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

3、类加载的三种方式

(1)、命令行启动应用时候由JVM初始化加载
(2)、通过Class.forName()方法动态加载
(3)、通过ClassLoader.loadClass()方法动态加载

三、垃圾回收算法

垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言。有了C语言的前车之鉴,Java语言在设计之初就避免让程序员进行GC, jvm 中,程序计数器、Java栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中.

1、对象存活判断

(1)引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,但是无法解决对象相互循环引用的问题。
(2)可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
在Java语言中,GC Roots包括:

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性实体引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。

2、选择垃圾收集的时机

当程序运行时,各种数据、对象、线程、内存等都时刻在发生变化,当下达垃圾收集命令后就立刻进行收集吗?肯定不是。这里来了解两个概念:安全点(safepoint)和安全区(safe region)。
安全点
从线程角度看,安全点可以理解为是在代码执行过程中的一些特殊位置,当线程执行到安全点的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这里暂停用户线程。当垃圾收集时,如果需要暂停当前的用户线程,但用户线程当时没在安全点上,则应该等待这些线程执行到安全点再暂停。
举个例子,妈妈在扫地,儿子在吃瓜子(瓜子皮会扔到地上),妈妈扫到儿子跟前时,儿子说:“妈妈等一下,让我吃完这块再扫。”儿子吃完这个瓜子把瓜子皮扔到地上后就是一个安全点,妈妈可以继续扫地(垃圾收集器可以继续收集垃圾)。理论上,解释器的每条字节码的边界上都可以放一个安全点,实际上,安全点基本上以“是否具有让程序长时间执行的特征”为标准进行选定。
安全区
安全点是相对于运行中的线程来说的,对于如sleep或blocked等状态的线程,收集器不会等待这些线程被分配CPU时间,这时候只要线程处于安全区中,就可以算是安全的。安全区就是在一段代码片段中,引用关系不会发生变化,可以看作是被扩展、拉长了的安全点。
还以上面的例子说明,妈妈在扫地,儿子在吃瓜子(瓜子皮会扔到地上),妈妈扫到儿子跟前时,儿子说:“妈妈你继续扫地吧,我还得吃10分钟呢!”儿子吃瓜子的这段时间就是安全区,妈妈可以继续扫地(垃圾收集器可以继续收集垃圾)。

3、垃圾收集算法

(1)标记 -清除算法
“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它是最基础的收集算法,后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
它的主要缺点有两个:一个是效率问题,因为涉及大量的内存遍历工作,所以执行性能较低,这也会导致“stop the world”(程序停顿)时间较长,java程序吞吐量降低;
另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,这会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存,就不得不提前触发另一次垃圾收集动作。

(2)标记-压缩算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,解决了标记-清除算法的空间问题,但是效率问题没有解决。Erqie在压缩过程中一些对象内存地址会发生改变。

(3)复制算法
“复制”(Copying)算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。在垃圾回收时,将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
复制算法相对标记压缩算法来说更简洁高效,但它的缺点也显而易见,它不适合用于存活对象多的情况,因为那样需要复制的对象很多,复制性能较差,所以复制算法往往用于内存空间中新生代的垃圾回收,因为新生代中存活对象较少,复制成本较低。它另外一个缺点是内存空间占用成本高,因为它基于两份内存空间做对象复制,在非垃圾回收的周期内只用到了一份内存空间,内存利用率较低。整体来说:解决了标记-清除算法的效率问题,但是空间问题没有解决

(4)分代收集算法
GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

四、垃圾回收器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现,不同的算法各有各的优缺点,在JVM中并不是单纯的使用某一种算法进行垃圾回收,而是将不同的垃圾回收算法包装在不同的垃圾回收器当中。

1、串行(Serial)收集器

串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)

参数控制:-XX:+UseSerialGC 串行收集器

适用场景:Client 模式(桌面应用);单核服务器。

java 永久代对象 jvm 永久代存放了什么_加载_04


3、并行回收器

PS:关于垃圾收集器的并行和并发

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态;

并发(Concurrent):指用户线程与垃圾收集线程同时执行,用户线程在继续执行而垃圾收集程序运行在另外一个CPU上;并行回收器是使用多线程并行回收,不过针对新生代和老年代,是否都使用并行,有不同的回收器选择:

(1)ParNew收集器

ParNew收集器其实就是串行收集器的多线程版本。新生代并行,老年代还是串行;新生代依然使用复制算法、老年代也依然使用标记-压缩算法

参数控制:

-XX:+UseParNewGC ParNew收集器

-XX:ParallelGCThreads 限制线程数量

适用场景:多核服务器;与 CMS 收集器搭配使用。当使用 -XX:+UserConcMarkSweepGC 来选择 CMS 作为老年代收集器时,新生代收集器默认就是 ParNew

java 永久代对象 jvm 永久代存放了什么_java 永久代对象_05


(2)Parallel收集器

依然是并行回收器,但这种回收器有两种配置,一种是Parallel Scavenge类似于ParNEW:新生代使用并行回收、老年代使用串行回收。ParNew 的目标是尽可能缩短垃圾收集时用户线程的停顿时间, Parallel Scavenge的目标是达到一个可控制的吞吐量。可以认为在相同的条件下它比ParNew更优。可以使用 -XX:+UseParallelGC 来选择 Parallel Scavenge 作为新生代收集器,jdk7、jdk8 默认使用 Parallel Scavenge 作为新生代收集器。

适用场景:Client 模式(桌面应用);单核服务器;与 Parallel Scavenge 收集器搭配;作为 CMS 收集器的后备预案。

java 永久代对象 jvm 永久代存放了什么_加载_06


吞吐量: CPU 执行用户线程的的时间与 CPU 执行总时间的比值【吞吐量 = 运行用户代代码时间/(运行用户代码时间+垃圾收集时间)】,比如虚拟机一共运行了 100 分钟,其中垃圾收集花费了 1 分钟,那吞吐量就是 99% 。比如下面两个场景,垃圾收集器每 90 秒收集一次,每次停顿 10 秒,和垃圾收集器每 50 秒收集一次,每次停顿时间 7秒,虽然后者每次停顿时间变短了,但是总体吞吐量变低了(0.90>0.88),CPU 总体利用率变低了。Parallel回收器另外一种配置Parallel Old则不同于ParNew,对于新生代和老年代均适应并行回收,这个收集器是在JDK 1.6中才开始提供

java 永久代对象 jvm 永久代存放了什么_CMS_07


参数控制: -XX:+UseParallelOldGC

可以通过 -XX:MaxGCPauseMillis 来设置收集器尽可能在多长时间内完成内存回收,可以通过 -XX:GCTimeRatio 来精确控制吞吐量。

适用场景:注重吞吐量,高效利用 CPU,需要高效运算且不需要太多交互。

4、CMS收集器

以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。需要注意的是CMS回收器是一种针对老年代的回收器,不对新生代产生作用。

java 永久代对象 jvm 永久代存放了什么_CMS_08


从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

① 初始标记(CMS initial mark):标记一下 GC Roots 能直接关联到的对象,速度较快。

② 并发标记(CMS concurrent mark):进行 GC Roots Tracing,标记出全部的垃圾对象,耗时较长。

③ 重新标记(CMS remark):修正并发标记阶段引用户程序继续运行而导致变化的对象的标记记录,耗时较短。

④ 并发清除(CMS concurrent sweep):用标记-清除算法清除垃圾对象,耗时较长。

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

优点: 并发收集、低停顿

缺点: CMS对 CPU 资源敏感(占用部分线程及CPU资源,影响总吞吐量):它的回收并不彻底。这也导致了CMS回收的频率相较其他回收器要高,频繁的回收将影响应用程序的吞吐量,默认分配的垃圾收集线程数为(CPU 数+3)/4,随着 CPU 数量下降,占用 CPU 资源越多,吞吐量越小;

无法处理浮动垃圾:在并发清理阶段,由于用户线程还在运行,还会不断产生新的垃圾,CMS 收集器无法在当次收集中清除这部分垃圾。同时由于在垃圾收集阶段用户线程也在并发执行,CMS 收集器不能像其他收集器那样等老年代被填满时再进行收集,需要预留一部分空间提供用户线程运行使用。当 CMS 运行时,预留的内存空间无法满足用户线程的需要,就会出现 “ Concurrent Mode Failure ” 的错误,这时将会启动后备预案,临时用 Serial Old 来重新进行老年代的垃圾收集。

因为 CMS 是基于标记-清除算法,所以垃圾回收后会产生空间碎片,可以通过 -XX:UserCMSCompactAtFullCollection 开启碎片整理(默认开启),在 CMS 进行 Full GC 之前,会进行内存碎片的整理。还可以用 -XX:CMSFullGCsBeforeCompaction 设置执行多少次不压缩(不进行碎片整理)的 Full GC 之后,跟着来一次带压缩(碎片整理)的 Full GC。

适用场景:重视服务器响应速度,要求系统停顿时间最短。
参数控制:
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

4、G1收集器

G1 收集器是 jdk1.7 才正式引用的商用收集器,现在已经成为 jdk9 默认的收集器,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

1.空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

2.可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

java 永久代对象 jvm 永久代存放了什么_CMS_09


和CMS的过程比较类似:

① 初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行。

② 并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。

③ 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录。

④ 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First 的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。

适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器。

特点

  • 并行与并发(充分利用多核多CPU缩短STW时间)
  • 分代收集(独立管理整个Java堆,但针对不同年龄的对象采取不同的策略)
  • 空间整合(局部看是基于复制算法,从整体来看是基于标记-整理算法,都不会产生内存碎片)
  • 可预测的停顿(可以明确指定在一个长度为M毫秒的时间片内垃圾收集不会超过N毫秒)
  • 将堆分为大小相等的独立区域,避免全区域的垃圾收集;新生代和老年代不再物理隔离,只是部分Region的集合;
  • G1跟踪各个Region垃圾堆积的价值大小,在后台维护一个优先列表,根据允许的收集时间优先回收价值最大的Region;
  • Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,采用Remembered Set来避免全堆扫描;

六、常见面试题

1、双亲委派模型内容及意义

java 永久代对象 jvm 永久代存放了什么_Java_10


双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制:

1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader“`去完成。

3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

双亲委派模型意义:

系统类防止内存中出现多份同样的字节码

保证Java程序安全稳定运行

1、总结
Jvm知识点很多,一篇文章很难包含所有,我以重点概括为中心思想,进行整理,意在让读者有整体认识,以后有机会再行展开