JVM(36098字)
1. 说一下 JVM 的主要组成部分?
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
- 各组件的作用?
首先通过类加载器会把 Java 代码转换成字节码,运行时数据区再把字节码加载到内存中,执行引擎负责将字节码翻译成底层系统指令,交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口来实现整个程序的功能。
- Java中的类加载机制有了解吗?
Java中的类加载机制指虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。类加载机制的保持则包括前面五个阶段。
2. 谈谈对运行时数据区的理解?
VM中的内存主要划分为5个区域,即方法区,堆内存,虚拟机栈,本地方法栈以及程序计数器。
- 方法区:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码等(存储的数据一般持久性比较强)数据。
- 堆内存:在虚拟机启动时创建,存放对象实例。Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC”堆(Garbage Collected Heap)。从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代。
- 虚拟机栈:为虚拟机执行Java方法(也就是字节码)服务。
- 本地方法栈:为虚拟机使用到的 Native(调用非java代码的接口)方法服务。
- 程序计数器:是当前线程所执行的字节码的行号指示器。
- 哪些属于线程独占,哪些属于线程共享?
线程共享:方法区,堆内存
线程独占:程序计数器,虚拟机栈以及本地方法栈
- 解释下运行时常量池?
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一些信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 堆和栈的区别是什么?
从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。
3. 谈谈对内存泄漏的理解?
在 Java 中,内存泄漏就是存在一些不会再被使用确没有被回收的对象,这些对象有下面两个特点:
- 这些对象是可达的,即在有向图中,存在通路可以与其相连;
- 这些对象是无用的,即程序以后不会再使用这些对象。
如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。
哪些操作会造成内存泄漏?
内存泄漏,就是不再需要的对象仍然存在内存中,内存泄漏不断堆积的后果就是内存溢出,即内存不够用。
垃圾回收机制会定期扫描对象,如果一个对象没有被其他对象引用,或两个对象互相引用但没有被第三个对象引用,则它们的内存会被回收。
\1. setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄露
\2. 全局变量
\3. 闭包
\4. dom清空或删除时,事件未清除导致的内存泄漏
\5. 控制台日志
\6. 循环
4. JMM是什么?
JMM
:Java内存模型,是java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别(注意这个跟JVM完全不是一个东西,现在还有小伙伴搞错的
)。
其实早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度
,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)
来作为内存与处理器之间的缓冲。
将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence)
。
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rXqeM8Hr-1596595328720)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200518151220348.png)]
5. 为什么要学习Jvm?
- 为了更好的理解java语言
- 为了再内存泄漏和溢出时候进行补救
6. 什么是栈帧?
每个方法再虚拟机栈被执行的时候,都会同步创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
7. Stop-The-World
一.概述:
java对象内存申请过程:
1.JVM会试图为相关Java对象在Eden中初始化一块内存区域;当Eden空间足够时,内存申请结束。否则到下一步;
2.JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
3.Survivor区被用来作为Eden及old的中间交换区域,当old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
4.当old区空间不够时,JVM会在old区进行major collection;
5.垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Out of memory错误";
Stop-The-World:
在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
不同垃圾收集器的Stop-The-World情况,Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情况;而即便是最新的G1收集器也不例外。
- Java中一种全局暂停的现象,jvm挂起状态
- 全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
- 多半由于jvm的GC引起,如:
1.老年代空间不足。
2.永生代(jkd7)或者元数据空间(jkd8)不足。
3.System.gc()方法调用。
4.CMS GC时出现promotion failed和concurrent mode failure
5.YoungGC时晋升老年代的内存平均值大于老年代剩余空间
6.有连续的大对象需要分配 - 除了GC还有以下原因:
1.Dump线程–人为因素。
2.死锁检查。
3.堆Dump–人为因素。
Full GC 是清理整个堆空间—包括年轻代和老年代。
GC时为什么会有全局停顿?
类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。当gc线程在处理垃圾的时候,其它java线程要停止才能彻底清除干净,否则会影响gc线程的处理效率增加gc线程负担,特别是在垃圾标记的时候。
危害
- 长时间服务停止,没有响应
- 遇到HA系统,可能引起主备切换,严重危害生产环境。
- 新生代的gc时间比较短(),危害小。
- 老年代的gc有时候时间短,但是有时候比较长几秒甚至100秒–几十分钟都有。
- 堆越大花的时间越长。
链接:https://www.jianshu.com/p/d686e108d15f
8. 元空间(Metaspace)
java8中移除了永久代,新增了元空间的概念。原来的方法区是逻辑划分中的一个区域,对应hotspot jdk6中的永久代,可以说永久代是方法区在hotspot的一个具体实现,但是从jdk7以后方法区就“四分五裂了”,不再是在单一的一个去区域内进行存储。
java8中继承了一些jdk7中的改变:符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,这个影响了String的intern()方法的行为,这里不做intern的详述。
而在java8中移除了永久代,新增了元空间,其实在这两者之间存储的内容几乎没怎么变化,而是在内存限制、垃圾回收等机制上改变较大。元空间的出现就是为了解决突出的类和类加载器元数据过多导致的OOM问题,而从jdk7中开始永久代经过对方法区的分裂后已经几乎只存储类和类加载器的元数据信息了,到了jdk8,元空间中也是存储这些信息,而符号引用、字符串常量等存储位置与jdk7一致,还是“分裂”的方法区。
符号引用没有存在元空间中,而是存在native heap中,这是两个方式和位置,不过都可以算作是本地内存,在虚拟机之外进行划分,没有设置限制参数时只受物理内存大小限制,即只有占满了操作系统可用内存后才OOM。
链接:https://www.jianshu.com/p/474d98fc4776
【<类加载器专题>】
[1] 什么是类加载器?
类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。包括以下 7 个阶段(加点盐,准备解出时写):
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
加点盐,准备解析初始化
**[类加载过程:](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=类加载过程)**包含了加载、验证、准备、解析和初始化这 5 个阶段。
-
加载:加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,最后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
加载过程完成以下三件事:
- 通过类的完全限定名称获取定义该类的二进制字节流。
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
- 从网络中获取,最典型的应用是 Applet。
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
-
验证:验证的作用是确保被加载的类的正确性,包括文件格式验证,元数据验证,字节码验证以及符号引用验证。
-
准备:准备阶段为类的静态(static)变量分配内存,并将其初始化为默认值。
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
由于方法区是一个逻辑区域,所以在JDK8之后,类变量则会随着Class对象一起存放在Java堆中。假设一个类变量的定义为public static int val = 3;那么变量val在准备阶段过后的初始值不是3而是0。这是因为尚未执行任何java方法,而把value复制为12的putstatic指令是被编译后,存放再类构造器的()方法中,所以把value复制为123的操作需等到类的初始化阶段才会被执行。
-
解析:解析阶段时java虚拟机将常量池内的符号引用转换为直接引用的过程。解析包括:类或接口的解析,字段解析,方法解析,接口方法解析。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。
- 符号引用(Symbolic References):
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
- 直接引用
直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
-
初始化:初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 () 方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
方法 的内容: 所有的类变量初始化语句和类型的静态初始化器
[2] 类加载器的分类有?
启动类加载器(Bootstrap ClassLoader):启动类加载器负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的类。
扩展类加载器(ExtClassLoader):扩展类加载器负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)。
应用类加载器(AppClassLoader):应用类加载器负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器。
[3] 类加载器的职责有?
全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
父类委托(防止重复):类加载机制会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。父类委托机制是为了防止内存中出现多份同样的字节码,保证java程序安全稳定运行。
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
[4] 什么是双亲委派机制?
双亲委派模式的工作原理的是;如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的双亲委派模式.那么这种模式有什么作用?
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理会先检查自己是否已经加载过,如果没有再往上。注意这个过程,直到到达Bootstrap classLoader之前,都是没有哪个加载器自己选择加载的。如果父加载器无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
【JVM垃圾回收专题】
- 我们首先需要判断垃圾,其中中心思想为判断其是否还有引用。可以使用引用计数法和可达性分析;
- 我们对垃圾进行回收时,需要一些垃圾回收算法进行理论支持。包括:标记-清除,复制,标记-整理,分代收集算法。
- 如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。见下图,其中CMS和G1重点了解。
- 最后我们划分了堆内存(年轻代<一个生成区和两个幸存区>和老年代)和非堆内存(永久代),进行垃圾分类。还有两种针对不同区域实施垃圾回收策略,追求高效率的回收。
[1] 垃圾回收的场所及原因?
垃圾回收的主要场所是堆内存,主要对象是实例对象和变量。随着程序的运行它们占据的内存越来越多,如果不及时进行垃圾回收,必然会带来程序性能的下降,甚至会因为可用内存不足造成一些不必要的系统异常。
[2] 为什么学习GC和如何学习GC?
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集器成为系统达到更高并发量的瓶颈时,我们就需要对这些自动化的技术实施必要的监控和调节。
- 哪些内存需要回收?->
- 什么时候回收?
- 如何回收?
[3] JVM如何判定一个对象是否应该被回收?
判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及root根搜索方法。
-
引用计数法:是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理对象间循环引用的问题。
-
可达性分析:root搜索方法的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。
- 什么对象会被认为是root对象?
- jvm运行时方法区类静态变量(static)引用的对象
- jvm运行时方法区常量引用的对象
- jvm当前运行线程中的虚拟机栈变量表引用的对象
- 本地方法栈中(jni)引用的对象
- 被同步锁(synchronized)持有的对象
- 虚拟机内部的引用
总之一句话,GC Root 对象一定是影响程序运行的对象。
- 何为循环引用?
- “引用”是什么意思?
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表一个引用。JDK1.2以后将引用分为强引用,软引用,弱引用和虚引用四种。
- 非死不可?
真正宣告一个对象死亡,至少要经历两次被标记的过程,如果没有引用链,就会进行第一次标记,随后进行一次筛选,筛选的条件时对象是对象是否覆盖过finalize方法,或者finalize方法已经被虚拟机调用过,则执行finalize方法。
[4] 在java中为什么不推荐使用finalize
一般来说,finalze方法都是在Java虚拟机发现去除那些已经被执行了finalize的对象之外,没有任何活动的线程能够引用到该对象的时候调用。在finalze方法里,可以做任何事情,甚至让该对象重新可被其他线程引用;但是一般来说,finalize方法的通常目的是在该对象真正被回收之前做一些清理的工作。比如,一个代表输入/输出连接的对象,它的finalize方法可能会在自己被回收之前中断对应的I/O连接。
https://baijiahao.baidu.com/s?id=1655232869611610920&wfr=spider&for=pc
我们都知道一个对象如果没有了任何引用,java虚拟机就认为这个对象没什么用了,就会对其进行垃圾回收,但是如果这个对象包含了finalize函数,性质就不一样了。怎么不一样了呢?
java虚拟机在进行垃圾回收的时候,一看到这个对象类含有finalize函数,就把这个函数交给FinalizerThread处理,而包含了这个finalize的对象就会被添加到FinalizerThread的执行队列,并使用一个链表,把这些包含了finalize的对象串起来。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k4N2T2q9-1596595328727)(https://pics5.baidu.com/feed/1e30e924b899a9010d34c78331bb0d7d0208f532.jpeg?token=cc6b19d8b9ecf5e0697042b70da81e1d&s=0D40EC12E18768EA584DA0CE0200D0A1)]
他的影响在于只要finalize没有执行,那么这些对象就会一直存在堆区,不过这里只是4个包含了finalize的对象,影响不是那么大,如果有一万个或者是十万个呢?这就影响大了。
finalize的原理其实很简单,在这里简要的梳理一下:
(1)对象在初始化的过程中会判断是否重写了finalize,方法是判断两个字段标志has_finalizer_flag和RegisterFinalizersAtInit。
(2)如果重写了finalize,那就把当前对象注册到FinalizerThread的ReferenceQueue队列中。注册之后的对象就叫做Finalizer。方法是调用register_finalizer函数。此时java虚拟机一看当前有这个对象的引用,于是就不进行垃圾回收了。
(3)对象开始被调用,FinalizerThread线程负责从ReferenceQueue队列中获取Finalizer对象。开始执行finalize方法,在执行之前,这个对象一直在堆中。
(4)对象执行完毕之后,将这个Finalizer对象从队列中移除,java虚拟机一看对象没有引用了,就进行垃圾回收了。
这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。
为什么要舍弃finalize
第一:调用时机的不确定性
虽然finalize()方法早晚会被调用到,但这种调用时机的不可控性可能会导致资源迟迟不被释放而出现系统级异常。因为计算机的资源有是限的,当明确要释放某些资源时(比如上面例子中reader所掌控的一系列资源),应该使用其它的办法让这些资源立即释放(后面给出例子)!
第二:影响代码的可移植性
因为每种JVM内置的垃圾回收算法都是不同的,所以可能在你的JVM里,你辛辛苦苦编写的使用finalize方法的案例运行的很好,但移植到不同的JVM中时,很有可能会崩溃的一塌糊涂!
第三:成本较高
如果某个类重载了finalize方法且在方法内部实现了一些逻辑,那么JVM在构造或销毁这个类的对象之前,会做很多额外的工作。很明显,如果一个类没有重载finalize方法,那么销毁时只要将堆中的内存处理一下就可以了,而如果重载了finalize方法的话,就要执行finalize方法,万一执行过程中再出现点异常或错误,那消耗的成本就更高了。
第四:异常丢失
第三点中也说过了,万一fianlize方法中抛出了异常,那么finalize会终止运行,而抛出的这个异常也会被舍弃,最终会让对象实例处于一种半销毁半存活的僵尸状态,导致意想不到的后果!
[4] 详细说下四种引用?
强引用:普通存在, P p = new P(),只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。默认情况下,对象采用的均为强引用。
软引用:通过SoftReference类来实现软引用,在内存不足的时候会将这些软引用回收掉。软引用是Java中提供的一种比较适合于缓存场景的应用.
弱引用:通过WeakReference类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用。
虚引用:也称为幽灵引用或者幻影引用,通过PhantomReference类实现。设置虚引用只是为了对象被回收时候收到一个系统通知。
https://www.cnblogs.com/shoshana-kong/p/10575781.html
强引用-FinalReference
介绍:
强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:String str = new String("str");
这个str就是强引用。
可用场景:
地球人都知道,但是我讲不出来。软引用-SoftReference
介绍:
软引用在程序内存不足时,会被回收,使用方式:// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的, // 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T SoftReference<String> wrf = new SoftReference<String>(new String("str"));
可用场景:
创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。PS:图片编辑器,视频编辑器之类的软件可以使用这种思路。
软引用使用例子传送门:https://www.cnblogs.com/mjorcen/p/3968018.html弱引用-WeakReference
介绍:
弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:WeakReference<String> wrf = new WeakReference<String>(str);
可用场景:
Java源码中的java.util.WeakHashMap
中的key
就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
弱引用使用例子传送门:http://www.importnew.com/21206.html虚引用-PhantomReference
介绍:
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue
中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue
中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue
,使用例子:PhantomReference<String> prf = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());
可用场景:
对象销毁前的一些操作,比如说资源释放等。Object.finalize()
虽然也可以做这类动作,但是这个方式即不安全又低效(传送门:http://blog.csdn.net/aitangyong/article/details/39450341),so。强调
上诉所说的几类引用,都是指对象本身的引用,而不是指
Reference<T>
的四个子类的引用(SoftReference<T>
等)。
[5] 常用的垃圾收集算法有哪些?
参考网站:https://www.cnblogs.com/ghoster/p/7580729.html
-
标记-清除算法(Mark-Sweep):算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,之所以说它是最基本的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。它的主要不足有两个:一是效率问题,标记和清除效率都不高;二是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序在运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
-
复制算法(Copying):为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活这的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,代价未免太高了一点。
-
标记-整理算法(Mark-compact):复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是如果不想浪费50%的空间就要使用额外的空间进行分配担保(Handle Promotion当空间不够时,需要依赖其他内存),以应对被使用的内存中所有对象都100%存活的极端情况,对于“标记-整理”算法,标记过程仍与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有的存活对象都向一端移动,然后直接清理掉端边界以外的内存。
-
分代收集算法:当前的商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把堆划分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。
为什么在新生代使用使用复制算法,在老年代使用标记整理算法?
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
什么是内存碎片?如何解决?
由于不同 Java 对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。
[4] 分代收集的理论支撑?
弱分代假说:绝大数对象都是朝生夕灭的;
强分代假说:熬过多次垃圾收集过程的对象越难以消亡。
跨代引用假说:跨代引用相对于同代引用只占少数。
1. 弱分代假说和强分代假说证明了什么?
弱分代假说和强分代假说鉴定了垃圾收集器一致的原则,收集器应该讲java划分为不同区域,然后将回收对象依据年龄(年龄即对象熬过垃圾收集的过程)分配到不同的存储之中存储,使用不同垃圾回收策略。
2. 跨代引用假说解释了什么?
由于对象之间不是孤立的,对象之间会存在跨代引用。所以可以用跨代引用假说证明,存在互相引用关系的两个对象,应该是倾向于同时生存和消亡的。举个例子,如果某新生代的对象存在于老年代难以消亡,那么引用会使新生代的对象存活很久,进而晋升到老年代中,这种跨代引用随即被消除了。
3. 记忆集?
https://segmentfault.com/q/1010000023017473/
依据这条假说,我们不应该为了少量的跨代引用去扫描整个老年代。我们我们可以在新生代中建立一个全局数据结构–记忆集。这个结构把老年代划分为很多小块,并且标记出那一块存在跨代引用。当发生minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入GC roots进行扫描。
[6] 常用的垃圾收集器(内部使用垃圾收集算法)有哪些?
垃圾回收算法是垃圾回收的方法论,垃圾收集器,是内存回收的实践论。link,记忆方法:单线程:serial与serial old,多线程:ParNew与CMS,高吞吐:Parallel Scavenge与Parallel Old,G1
- Serial 收集器:新生代与老年代的单线程收集器,标记和清理都是单线程,在垃圾回收时会暂停其他工作线程,直到它收集完,即stop the world。优点是简单粗暴适合单核及少核处理器。
- Serial Old 收集器(标记-整理算法)老年代单线程收集器,Serial 收集器的老年代版本。服务端使用时:可以与Parallel Scavenge 收集器配合使用,也可以成为CMS收集器失败后的备选预案。
- ParNew 收集器(标记-复制算法)新生代收集器,可以认为是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现。它是激活CMS后默认的新生代收集器。
- Parallel Scavenge 收集器(标记-复制算法)并行多线程收集器,追求高吞吐量,高效利用 CPU。吞吐量一般为 99%, 适合后台应用等对交互相应要求不高的场景。吞吐量= 用户线程时间 / (用户线程时间+GC线程时间),吞吐量适合保证高效率的执行有效工作。
- Parallel Old 收集器(标记-整理算法)Parallel Old 收集器的老年代版本,并行收集器,吞吐量优先。
- **CMS(Concurrent Mark Sweep)**收集器(标记-清除算法)高并发、低停顿,追求最短 GC 回收停顿时间,cpu 占用比较高,响应时间快,停顿时间短,多核 cpu 追求高响应时间的选择。CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
- G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First的由来。
[7] 垃圾收集器如何互相配合使用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eoRDCOOC-1596595328729)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715164735100.png)]
[8] CMS说一下?
- CMS垃圾清除的步骤?
link:首先停止线程,标记root根遍历直接对象,然后根据其扫瞄,之后停止线程重新扫描,最后删除
- 初始标记(stop the world):使用可达性分析记录下直接与 root 相连的对象,暂停所有的其他线程,速度很快;
- 并发标记:同时开启 GC 和用户线程,用 CG root 直接关联对象一个去记录可达对象。但在这个阶段结束,这并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记(stop the world):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录【这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短】;
- 并发清除:开启用户线程,同时 GC 线程开始对为标记的区域做清扫。
- CMS 的优缺点?
优点:CMS(concurrent low pause collector)并发收集、低停顿;
缺点:对 CPU 资源敏感、无法处理浮动垃圾、它使用的回收算法“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
并发消耗CPU资源 其中的并发标记和并发清理是工作线程和垃圾回收线程并发工作,这样在需要STW的时间内不会让整个系统不可用。但是在并发标记阶段,需要根据GC Roots标记出大量的存活对象,而在并发清理阶段,则需要将垃圾对象从各种随机内存位置删掉,这两个阶段都非常消耗性能,所以垃圾回收线程会占用一部分的CPU资源,导致系统的执行效率降低。
CMS默认的回收线程数是 (CPU个数+3)/4,当在CPU核数较多的时候,对系统性能的影响并不是特别大。但是如果是CPU核数较少,例如双核的时候,就会占用一个CPU去处理垃圾回收,系统的CPU资源直接降低50%,这就严重影响了效率。
因为现在CPU的核数越来越多,所以这种场景基本不会对系统造成很大的影响,可以忽略不计。
Concurrent Mode Failure问题 并发清理阶段,工作线程和垃圾回收线程并发工作的时候,此时工作线程会不断产生新的垃圾,但是垃圾回收线程并不会去处理这些新生成的垃圾对象,需要等到下次垃圾回收的时候才会去处理,这些垃圾对象称之为:浮动垃圾 。因为有这些浮动垃圾的存在,所以老年代不能在100%使用的时候才去进行垃圾回收,否则就放不下这些浮动垃圾了。有一个参数是“-XX:CMSInitiatingOccupancyFraction”,这个参数在jdk1.6里面默认是92%,意思是老年代使用了92%的空间就会执行垃圾回收了。但是即使预留了8%的内存去存放浮动垃圾,但是还是有可能放不下,这样就会产生Concurrent Mode Failure问题。一旦产生了Concurrent Mode Failure问题,系统会直接使用Serial Old垃圾回收器取代CMS垃圾回收器,从头开始进行GC Roots追踪对象,并清理垃圾,这样会导致整个垃圾回收的时间变得更长。
解决办法就是根据系统的需求,合理设置“-XX:CMSInitiatingOccupancyFraction”的值,如果过大,则会产生Concurrent Mode Failure问题,如果设置的过小,则会导致老年代更加频繁的垃圾回收。
空间碎片问题 CMS的标记-清理算法会在并发清理的阶段产生大量的内存碎片,如果不整理的话,则会有大量不连续的内存空间存在,无法放入一些进入老年代的大对象,导致老年代频繁垃圾回收。所以CMS存在一个默认的参数 “-XX:+UseCMSCompactAtFullCollection”,意思是在Full GC之后再次STW,停止工作线程,整理内存空间,将存活的对象移到一边。还要一个参数是“-XX:+CMSFullGCsBeforeCompaction”,表示在进行多少次Full GC之后进行内存碎片整理,默认为0,即每次Full GC之后都进行内存碎片整理。
CMS虽然使用并发的方式降低了STW的时间,但是还需要配合一些CMS的参数才能完全发挥出CMS的优势,否则甚至会降低垃圾回收的效率。因此只有掌握了CMS的原理和参数的调试,才能让系统运行的更加流畅。
[9] G1 说一下?
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。G1(Garbage-First)开创了面向局部收集的设计思路和基于region的内存布局形式。JAVA9时取代Parallel Scavenge与Parallel Old组合。G1 在扫描了 region 以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的 region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为 Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记:仅仅标记一下GCroot可以关联到的对象,并且修改TAMS指针的值,让下一个阶段用户线程并发运行时,可以正确在可用的region中分配新对象。这个阶段需要停顿线程但是耗时极短。而且是借用minor GC时候同步完成,所以G1收集器在这个接丢单没有额外的停顿。
- 并发标记:从已经标记的堆中的对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个过程耗时比较长,
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收(暂停用户线程):首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。可以自由选择多个region构成回收集,然后把决定回收的那一部分region的存活对象复制到空的regions中,再清理掉整个旧的region的全部空间。
具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
- G1的另一个显著特点他能够让用户设置应用的暂停时间,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。 (阿里面试)
[10] G1和CMS?
总体来说,G1跟CMS一样,是一块低延时的收集器,同样牺牲了吞吐量,不过二者之间得到了很好的权衡。
G1与CMS对比有一下不同:
-
分代: CMS中,堆被分为PermGen,YoungGen,OldGen;而YoungGen又分了两个survivo区域。在G1中,堆被平均分成几个区域(region),在每个区域中,虽然也保留了新老代的概念,但是收集器是以整个区域为单位收集的。
-
算法: 相对于CMS的“标记—清理”算法,G1会使用压缩算法,保证不产生多余的碎片。收集阶段,G1会将某个区域存活的对象拷贝的其他区域,然后将整个区域整个回收。
-
停顿时间可控: 为了缩短停顿时间,G1建立可预存停顿模型,这样在用户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。
[10] 说下你对垃圾回收策略的理解/垃圾回收时机?
答:JVM的内存可以分为堆内存和非堆内存。堆内存分为年轻代(复制算法)和老年代(标记-整理算法)。年轻代又可以进一步划分为一个Eden(伊甸)区和两个Survivor(幸存)区组成。
- Minor / Scavenge GC:所有对象创建在新生代的 Eden 区,当 Eden 区满后触发新生代的 Minor GC,将 Eden 区和非空闲 Survivor 区存活的对象复制到另外一个空闲的 Survivor 区中,在这个过程中保证一个 Survivor 区是空的,新生代 Minor GC 就是在两个 Survivor 区之间相互复制存活对象,直到 Survivor 区满为止。我们创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代。
- Full GC:**Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。对整个堆进行整理,包括 Young、Tenured 和 Perm。**Full GC 因为需要对整个堆进行回收,而且会"**STOP THR WORLD"**所以比 Minor GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就是对于 Full GC 的调节。
如果生成区存活对象太多,导致幸存者区放不小怎么办?
可以使用逃生门,将对象转移到其他内存区域,如:老年代。
[11] 分区大小比值及其理论依据
生成区:s0:s1=8:1:1 老年代:新生代=2: 1
-
为什么年轻代新增servivor区域?
-
在年轻代新增Surviver区,有利于减轻老年代的负担,尽可能的让大部分对象在年轻代通过较高效的Yong GC回收掉,不至于老年代里存放的对象过多导致内存不足而进行频繁的Full GC操作。而且这种分区有利于减少内存碎片的产生。
-
为什么使用 8:1:1,而不使用5:5,或者8:2?
-
由于IBM的研究表示,新生代中有98%的对象活不过第一轮收集。使用5:5会造成空间的极大浪费。哪为什么使用8:2?因为这样会导致比例为2的空间向空间为8的区域内复制时,发生minor gc速度过快。
-
老年代:新生代=2: 1?
-
老年代不宜过小,如果老年代小,会导致转为老年代的时候,老年代撑不下,导致full gc,回收停顿时间过长。
[12] 内存分配策略
[1. 对象优先在 Eden 分配](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_1-对象优先在-eden-分配)
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
[2. 大对象直接进入老年代](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_2-大对象直接进入老年代)
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
[3. 长期存活的对象进入老年代](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_3-长期存活的对象进入老年代)
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
[4. 动态对象年龄判定](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_4-动态对象年龄判定)
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
[5. 空间分配担保](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_5-空间分配担保)
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
[13] Minor GC 和 Full GC触发条件
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
[Full GC 的触发条件](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=full-gc-的触发条件)
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
[1. 调用 System.gc()](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_1-调用-systemgc)
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
[2. 老年代空间不足](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_2-老年代空间不足)
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
[3. 空间分配担保失败](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_3-空间分配担保失败)
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。
[4. JDK 1.7 及以前的永久代空间不足](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_4-jdk-17-及以前的永久代空间不足)
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
[5. Concurrent Mode Failure](https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_5-concurrent-mode-failure)
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
【JVM调优】
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。
[1] 说下你用过的 JVM 监控工具?
-
jvisualvm:虚拟机监视和故障处理平台
-
jps :查看当前 Java 进程
-
jstat:显示虚拟机运行数据
-
jmap:内存监控
-
jhat:分析 heapdump 文件
-
jstack:线程快照
-
jinfo:虚拟机配置信息
[2] 如何利用监控工具调优?
- 1. 堆信息查看
-
可查看堆空间大小分配(年轻代、年老代、持久代分配)
-
提供即时的垃圾回收功能
-
垃圾监控(长时间监控回收情况)
-
查看堆内类、对象信息查看:数量、类型等
-
对象引用情况查看
- 有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
-
年老代年轻代大小划分是否合理
-
内存泄漏垃
-
圾回收算法设置是否合理
- 2. 线程监控
线程信息监控:系统线程数量
线程状态监控:各个线程都处在什么样的状态下
Dump 线程详细信息:查看线程内部运行情况
死锁检查
- 3. 热点分析
-
CPU 热点:检查系统哪些方法占用的大量 CPU 时间;
-
内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。
- 4. 快照
快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问题。
举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。
- 5. 内存泄露检查
内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问题则是具体问题具体分析了。
内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。内存泄漏对系统危害比较大,因为它可以直接导致系统的崩溃。
[3] JVM 的一些参数?
- 1. 堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1/4
-XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如:3,表示 Eden:Survivor=3:2,一个Survivor区占整个年轻代的 1/5
-XX:MaxPermSize=n:设置持久代大小
- 2. 收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
- 3. 垃圾回收统计信息
-XX:+PrintGC:开启打印 gc 信息
-XX:+PrintGCDetails:打印 gc 详细信息
-XX:+PrintGCTimeStamps
-Xloggc:filename
- 4. 并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的 CPU 数
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比
- 5. 并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单 CPU 情况
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的 CPU 数。并行收集线程数
[4] JVM在内存调优方面
JVM在内存调优方面,提供了几个常用的命令,分别为jps,jinfo,jstack,jmap以及jstat命令。分别介绍如下:
-
jps:主要用来输出JVM中运行的进程状态信息,一般使用jps命令来查看进程的状态信息,包括JVM启动参数等。
-
jinfo:主要用来观察进程运行环境参数等信息。
-
jstack:主要用来查看某个Java进程内的线程堆栈信息。jstack pid 可以看到当前进程中各个线程的状态信息,包括其持有的锁和等待的锁。
-
jmap:用来查看堆内存使用状况。jmap -heap pid可以看到当前进程的堆信息和使用的GC收集器,包括年轻代和老年代的大小分配等
-
jstat:进行实时命令行的监控,包括堆信息以及实时GC信息等。可以使用jstat -gcutil pid1000来每隔一秒来查看当前的GC信息。
[5] 怎么做JDK8的内存调优?
1. JDK8内存结构
JDK8的内存结构主要包括程序计数器(Program Counter Register)、虚拟机栈(Java Virtual Machine Stacks)、本地方法栈(Native Method Stacks)、堆(Java Heap)、元空间(Metaspace)。
其中堆又被划分为老年代(Old Generation)、年轻代(Young Generation),其中年轻代又被划分为一个Eden区和两个Survivor区。
一边说着,一边拿起笔在纸上画了起来:
画完以后,我又说:JDK8的内存调优主要针对的是堆和元空间。内存调优时常用到JVM参数有这些:
-server
JVM的server模式, 在多CPU服务器中性能可以得到更好地发挥。JDK的64位版本只支持server模式,因此在这种情况下,选项是隐式的。
-Xmx
指定堆所分配内存的最大值,等同于-XX:MaxHeapSize。不附加字母时,单位为byte,必须是1024的倍数,并且大于2MB;附加字母k或K时,表示单位为KB;附加字母m或M时,表示单位为MB;附加字母g或G时,表示单位为G。
下面的例子是使用不同的单位把堆所分配内存的最大值设置为1GB:
-Xmx1G
-Xmx1024M
-Xmx1048576K
-Xmx1073741824
1234
-Xms
指定堆所分配内存的初始值,不附加字母时,单位为byte,必须是1024的倍数,并且大于1MB;附加字母k或K时,表示单位为KB;附加字母m或M时,表示单位为MB;附加字母g或G时,表示单位为G。如果不设置这个初始值,那么初始值将被设置为老年代和年轻代分配内存的大小的总和。
下面的例子是使用不同的单位把堆所分配的初始值设置为4GB:
-Xms4G
-Xms4096M
-Xms4194304K
-Xms4294967296
1234
对于生产环境的部署,-Xms和-Xmx通常设置为相同的值。
-Xmn
指定堆的年轻代分配内存的初始值和最大值,不附加字母时,单位为byte;附加字母k或K时,表示单位为KB;附加字母m或M时,表示单位为MB;附加字母g或G时,表示单位为G。
堆的年轻代区域用于存放新生对象。与其他区域相比,在这个区域执行垃圾回收的频率更高。如果年轻代的内存太小,那么将执行许多次垃圾回收。如果年轻代的内存太大,那么执行完整的垃圾回收可能需要很长时间才能完成。一般建议把年轻代的大小保持在整个堆大小的1/2到1/4之间。
下面的例子是使用不同的单位把年轻代所分配内存的初始值和最大值设置为2GB:
-Xmn2G
-Xmn2048M
-Xmn2097152K
-Xmn2147483648
1234
除了使用-Xmn选项设置年轻代的初始值和最大值,还可以使用-XX:NewSize设置年轻代的初始值,使用-XX:MaxNewSize设置年轻代的最大值。
-XX:NewRatio
指定老年代和年轻代空间大小的比率。默认为2,即老年代和年轻代空间大小的比率为2:1,年轻代占整个堆内存空间大小的1/3。下面的例子是把老年代和年轻代空间大小的比率设置为1:
-XX:NewRatio=1
1
另外,年轻代分配内存设置的优先级如下:
- 高优先级: -XX:NewSize/-XX:MaxNewSize
- 中优先级: -Xmn
- 低优先级: -XX:NewRatio
-XX:SurvivorRatio
指定Eden区和一个Survivor区的空间大小的比率。默认为8,即Eden区和一个Survivor区的空间大小为8:1,因为一共有两个Survivor区,所以Eden区占年轻代内存大小的80%。下面的例子是把Eden区和一个Survivor区的空间大小的比率设置为4:
-XX:SurvivorRatio=4
1
-XX:MetaspaceSize
指定元空间第一次触发垃圾回收的内存大小的阈值。当元空间内存占用不断增大,直到达到这个阈值时,就会触发一次垃圾回收。所以,适当的增大这个阈值,会减少垃圾回收的次数。默认值根据平台而定,一般情况下大约20.8MB。下面的例子是把元空间第一次触发垃圾回收的内存大小设置为256MB:
-XX:MetaspaceSize=256M
1
有一些小伙伴对这个参数有误解,造成不必要的麻烦。重申一下:-XX:MetaspaceSize不是元空间内存大小的初始值,不是元空间内存大小的初始值,不是元空间内存大小的初始值,重要的事情说三遍。
-XX:MaxMetaspaceSize
指定元空间所分配内存的最大值,默认是没有限制,取决于系统的可用内存量,理论上可以占满整个系统的内存。为了避免这种惨剧,影响系统上的其他应用,需要适当设置它的大小。下面的例子是把元空间所分配内存的最大值设置为512MB:
-XX:MaxMetaspaceSize=512M
1
面试官微笑地说:这些常用的内存调优参数总结的不错,可以结合这些参数写一个内存调优实例吗?
被面试官夸奖一下,我按捺住心中的喜悦说:当然可以。
内存调优实例
**如果把堆内存的空间设置大一些,以减少垃圾回收的次数。但是由于堆内存过大,在垃圾回收时会导致长时间的停顿。假设服务器上的可用内存还有12GB,那么先指定堆所分配内存的最大值和初始值为8GB。一般情况下,年轻代内存大小需在整个堆大小的1/2到1/4之间,那么就指定年轻代内存大小为3GB,老年代内存大小为5GB。再把Eden区和一个Survivor区的空间大小的比率设置为4。**元空间第一次触发垃圾回收的内存大小的阈值设置为256MB,一般情况下足够用。元空间所分配内存的最大值设置为512MB,为了避免极端情况下占用大量内存。另外,还需要明确指定JVM以server模式启动。
内存调优的参数基本敲定,用它启动一个名为one-more-study-0.0.1-SNAPSHOT.jar的jar文件:
java -server -Xmx8G -Xms8G -Xmn3G -XX:SurvivorRatio=4 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=512M -jar one-more-study-0.0.1-SNAPSHOT.jar
1
如果执行jmap -heap
命令查看对应Java进程的内存配置和使用情况,应该是这样的:
Attaching to process ID 31828, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.251-b08
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration: #堆的内存配置
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
# 堆内存的最大值
MaxHeapSize = 8589934592 (8192.0MB)
# 年轻代内存的大小
NewSize = 3221225472 (3072.0MB)
# 年轻代内存的最大值
MaxNewSize = 3221225472 (3072.0MB)
# 老年代内存的大小
OldSize = 5368709120 (5120.0MB)
# 老年代和年轻代空间大小的比率
# 因为设置Xmn参数,该设置未生效
NewRatio = 2
#Eden区和一个Survivor区的空间大小的比率
SurvivorRatio = 4
# 元空间第一次触发垃圾回收的内存大小
MetaspaceSize = 268435456 (256.0MB)
# 元空间内存的最大值
MaxMetaspaceSize = 536870912 (512.0MB)
G1HeapRegionSize = 0 (0.0MB)
Heap Usage: # 堆的使用情况
PS Young Generation
Eden Space: # Eden区内存的使用情况
capacity = 2147483648 (2048.0MB)
used = 901945720 (860.16MB)
free = 1245537928 (1187.83MB)
42.000120505690575% used
From Space: # Survivor的From区内存的使用情况
capacity = 536870912 (512.0MB)
used = 0 (0.0MB)
free = 536870912 (512.0MB)
0.0% used
To Space: # Survivor的To区内存的使用情况
capacity = 536870912 (512.0MB)
used = 0 (0.0MB)
free = 536870912 (512.0MB)
0.0% used
PS Old Generation # 老年代内存的使用情况
capacity = 5368709120 (5120.0MB)
used = 0 (0.0MB)
free = 5368709120 (5120.0MB)
0.0% used
12047 interned Strings occupying 1045744 bytes.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
听了我的回答后,面试官对我会心一笑,我仿佛还在她的眼神中看到了一丝敬仰。正所谓:万两黄金容易得,知心一个也难求,欲知后事如何,且听下回分解。
[6] 调优实战
JVM调优工具
Jconsole,jProfile,VisualVM
Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。详细说明参考这里
JProfiler:商业软件,需要付费。功能强大。详细说明参考这里
VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。
如何调优
观察内存释放情况、集合类检查、对象树
上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功能
堆信息查看
可查看堆空间大小分配(年轻代、年老代、持久代分配)
提供即时的垃圾回收功能
垃圾监控(长时间监控回收情况)
查看堆内类、对象信息查看:数量、类型
对象引用情况查看
有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
–年老代年轻代大小划分是否合理
–内存泄漏
–垃圾回收算法设置是否合理
线程监控
线程信息监控:系统线程数量。
线程状态监控:各个线程都处在什么样的状态下
Dump线程详细信息:查看线程内部运行情况
死锁检查
热点分析
CPU热点:检查系统哪些方法占用的大量CPU时间
内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)
这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。
快照
快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问题
举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。
内存泄漏检查
内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问题则是具体问题具体分析了。
内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。
内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。
需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。
年老代堆空间被占满
异常: java.lang.OutOfMemoryError: Java heap space
说明:
这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。
如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)
解决:
这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
持久代被占满
**异常:**java.lang.OutOfMemoryError: PermGen space
说明:
Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。
更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。
解决:
\1. -XX:MaxPermSize=16m
\2. 换用JDK。比如JRocket。
堆栈溢出
**异常:**java.lang.StackOverflowError
**说明:**这个就不多说了,一般就是递归没返回,或者循环调用造成
线程堆栈满
异常:Fatal: Stack size too small
说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
系统内存被占满
异常:java.lang.OutOfMemoryError: unable to create new native thread
说明:
这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。
分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。
解决:
\1. 重新设计系统减少线程数量。
\2. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。
https://www.iteye.com/blog/pengjiaheng-552456