JVM知识点总结
总结javaguide面试题,想要了解详情请点击javaguide
JVM介绍
JVM( Java Virtual Machine)它是一个虚构出来的计算机,一种规范,直接与操作系统进行交互,与硬件不直接交互,操作系统可以帮我们完成和硬件进行交互的工作
Java代码是如何运行的
当我们编写好一个.java文件如果想让jvm执行这个文件。将.java文件编译成可读取的二进制.class文件。
类加载器
当JVM需要执行.class文件,我们需要将其装入到类加载器中,让它加载到JVM中进行执行。它们在文件开头会有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载,而是否能够运行则由 Execution Engine 来决定
类加载器的流程
从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接
加载
3件事情
1.通过全类名获取定义此类的二进制字节流
2.将字节流所代表的静态存储结构转换为方法区运行时的数据结构
3.在内存中生成一个代表该类的Class对象,作为方法区数据访问入口。
一个非数组类的加载阶段(加载获取二进制字节流的动作)是可控性最强的阶段,这一步还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的loadClass方法)。数组类型不通过类加载器创建,有Jvm直接创建。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7N22AFf3-1616416172233)(C:\Users\24953\AppData\Roaming\Typora\typora-user-images\image-20210321205717977.png)]
验证
验证该类是否符合jvm规范是否合法
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些内存都将在方法区中分配。
1.这时候进行内存分配的仅包括类变量(static)而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在java堆中
2.这里所设置的初始值通常情况下是数据类型默认的值(0,null,false等)比如定义了
public static int value=111
那么value变量在准备阶段的初始值就是0而不是111(初始化阶段赋值)如果要是加上final 那么久赋值为111了。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。
符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行初始化方法 ==<clinit> ()
==方法的过程。
<clinit>()
方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。
虚拟机严格规范了有且只有5种情况下,必须对类进行初始化。
1.当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时
比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
-
当jvm执行new指令时会初始化类。即当程序创建一个类的实例对象。
-
当jvm执行getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
-
当jvm执行putstatic指令时会初始化类。即程序给类的静态变量赋值。
-
当jvm执行invokestatic指令时会初始化类。即程序调用类的静态方法。
2.在使用反射时也就是java.lang.reflect包下的方法对类进行反射,Class.foName,newInstance等等
3.初始化一个类,如果它的父类没有初始化,应当先初始化它的父类
4.当虚拟机启动时,用户需要定义一个要执行的主类(包含main方法的那个类)虚拟机会先初始化这个类。
5.MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用,就必须先使用findStaticVarHandle来初始化要调用的类。
当一个接口中定义了JDK8新加入的默认方法(default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在之前被初始化。
卸载
卸载类即该类的Class对象被GC。
卸载类需要满足3个要求:
-
该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
-
该类没有在其他任何地方被引用
-
该类的类加载器的实例已被GC
在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
类加载器的加载顺序
Custom ClassLoader:自定义加载器->App ClassLoader :指定的classpath下的jar包也叫程序加载器->Extension ClassLoader 加载扩展的jar包->Bootstrap ClassLoader 根加载器rt.jar
双亲委派机制
当一个类收到加载请求时,它是不会先自己去加载的,而是委派给父类完成,只有当父类加载器都反馈自己无法加载这个请求,子类才会自己进行尝试加载
加载位于rt.jar包中的类时不管是哪个加载器加载,最终都会委托到BootStrap ClassLoader进行加载,保证了使用不同的类加载器得到的都是一个结果。可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改
方法区
是用来存放类似于元数据信息方面的数据,比如类信息,常量,静态变量,编译后的代码等,
类加载器将 .class 文件搬过来就是先丢到这一块上
运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有常量池(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池是方法区的一部分,自然受到了内存的限制,当常量池无法申请到内存时会报出OutofMemoryError
1.7之前常量池逻辑包含字符串常量池,存放在方法区
1.7字符串常量池被从方法区拿到了堆中
1.8移出了永久代,用元空间取而代之,这时候字符串常量池还是在堆中,运行时常量池在还在方法区,只不过方法区的实现由永久代变成了元空间。
判断一个无用的类
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的
ClassLoader
已经被回收。该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
堆
主要存放了一些存储的数据,比如对象实例,数组等。它和方法区都是线程共享的,所以线程不安全。
栈
栈是代码运行空间,我们编写的每个方法都会放到栈中运行,JVM调优都是围绕着堆和栈两大块进行
程序计数器
主要完成一个加载工作,类似于一个指针,指向下一行要执行的代码。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器它和栈一样都是线程独享的,所以每一个线程都会有自己对应的一块区域而不会存在并发和多线程问题。
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
本地方法栈和程序计数器
使用native关键字修饰的方法都是本地方法,是使用c来实现的,这些方法会存放到一个叫做本地方法栈区域。
程序计数器是内存区域中唯一一个不会出现OutOfMemoryError,而且占用内存空间小到基本可以忽略不计,这个内存仅代表当前线程所执行的字节码的行号指示器,字节码解析器通过改变这个计数器的值选取下一条需要执行的字节码指令。如果执行的是native方法,那这个指针就不工作了。
虚拟机栈
概念
它是java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。里面会对局部变量,动态链表,方法出口,栈的操作进行存储,线程独享,同时如果我们听到局部变量表,那就是虚拟机栈(实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用
Java 虚拟机栈会出现两种错误:StackOverFlowError
和 OutOfMemoryError
。
StackOverFlowError
: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。OutOfMemoryError
: 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。
存在的异常
如果线程请求的栈的深度大于虚拟机的最大深度,就会报StackOverflowError。java虚拟机也可以动态扩展,但随着扩展会不断申请内存,当无法申请足够的内存时就会报OutOfMemoryError
生命周期
对于栈来说,不存在垃圾回收,只要程序运行结束,栈的空间自然就释放了。栈的生命周期和所处线程是一致的。
8种基本类型的变量+对象的引入变量+实例方法都是在栈里面分配内存的
栈的执行
栈帧数据,就是在JVM中叫做栈帧,放到java中其实就是方法,它也是存放在栈的。栈中的数据都是以栈帧的格式存在,它是一个方法和运行期数据的数据集
局部变量的复用
局部变量表用于存放方法参数和方法内部所定义的局部变量,它的容量是以Slot为最小单位,一个slot可以存放32位以内的数据类型。虚拟机通过索引定位的方式来使用局部变量表,范围为==[0,局部变量表的Slot的数量]。方法中的参数就会按一定顺序排列在这个局部变量中==,为了节省栈帧空间,这些slot是可以复用的,当方法执行位置超过了某个变量,那么这个变量的slot可以被其它变量所复用,垃圾回收不会去动这些内存
虚拟机堆
概念
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
JVM内存会划分为堆内存和非堆内存,堆内存中也会划分为年轻代和老年代,非堆内存为永久代。年轻代又分为Eden和Survivor区。Survivor区又分为FromPlace和ToPlace,Toplace的survivor区域是空的,Eden,FromPlace,ToPlace默认占比为8:1:1
堆内存中存放的是对象,垃圾收集就是收集这些对象然后交给GC算法进行回收,非堆内存就是方法区。在1.8中已经移除了永久代,替代品为一个元空间(MetaSpace),最大区别是metaSpace是不存在与JVM中的,它使用的是本地内存,并且有两个参数
MetaspaceSize:初始化元空间大小,控制发生GC
MaxMetaspaceSize:限制元空间大小上限,防止占用过多物理内存。
移除的原因可以大致了解一下:融合HotSpot JVM和JRockit VM而做出的改变,因为JRockit是没有永久代的,不过这也间接性地解决了永久代的OOM问题。
Eden年轻代
当我们new一个对象后,会先放到Eden划分出来的一块作为存储空间的内存,因为堆内存是线程共享的所以线程不安全,有可能两个对象使用一个内存。JVM处理这个问题会采用,每个线程都会预先申请好一块连续的内存空间并且规定了对象存放的位置,而如果空间不足会申请多块内存空间,这个操作称为TLAB(线程本地分配缓存区)
当Eden空间满了之后,会触发Minor GC(发生在 年轻代的GC)操作,存活下来的对象移动到Survivor0区,当Survivor0区满了就会再次触发Minor GC,将存活的对象移动到survivor1区,此时还会把FromPlace和ToPlace两个指针进行交换,这样保证了一段时间内总有一个survivor区为空,且为to所指向的survivor区为空。经过多次Minor GC后仍然存活的对象(这个存活判断15次,因为Hotspot会在对象投中的标记字段里记录年龄,分配的空间仅有4位1,2,4,8所以最多为15次)就会移动到老年代,老年代是用来存储长期存活的对象,占满时就会触发Full GC,期间会停止所有线程等待的GC完成,所以对应响应要求高的应用应该尽量减少Full GC从而避免超时的问题。
当老年代执行了Full GC之后还是无法保存对象,就会产生OOM,这个时候就是虚拟机中堆内存不足,可能是堆内存设置的过小,使用-Xms、-Xmx来调整,也可能是代码中创建的对象大且多,而且他们一直在被引用从而长时间垃圾收集无法收集他们。
判断一个对象是否真正死亡
1.引用计数器计算:给对象添加一个计数器,引用一次时计数器+1,引用失效时技术器 -1 当技术器为0时说明不在使用。这个方法的弊端是出现对象的循环使用时,GC没法回收。
2.可达性分析计算:这是一种类似于二叉树的实现,将一系列的GC ROOTS作为起始的存活对象集,从这个节点往下搜索,搜索所走过的路径称为引用链,把能被该集合所引用到的对象加入到集合中,搜索当一个对象到GC ROOTs没有使用任何引用链时,则说明对象是不可用的。缺点是,要消耗大量的资源和时间
宣告一个对象的真正死亡
finalize()是Object类的一个方法,一个对象的此方法只会被系统自动调用一次,经过finalize方法逃脱的对象,第二次不会再调用。由于他的执行时间不确定,甚至是否被执行也不确定,而且运行代价高昂,无法保证各个对象的调用顺序,在java9中已经被标记为deprecated 且java.lang.ref.Cleaner中已经逐步替换掉了,比finalize更加轻量且可靠。
判断一个对象的死亡至少需要两次标记
- 如果对象进行可达性分析之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。判断的条件是决定这个对象是否有必要执行finalize()方法。如果对象有必要执行finalize()方法,则被放入F-Queue队列中。
- GC对F-Queue队列中的对象进行二次标记。如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么==二次标记时则会将它移出“即将回收”集合。==如果此时对象还没成功逃脱,那么只能被回收了。
引用
jdk1.2之前,如果reference类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存为一个引用
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
1.强引用
这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2.软引用
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存。软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出
3.弱引用
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
4.虚引用
虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
HotSpot虚拟机对象的创建
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LcBh9Ad3-1616416172249)(file://C:\Users\24953\Desktop\Folder\javaguide\JavaGuide\docs\java\jvm\pictures\java内存区域\Java创建对象的过程.png?lastModify=1616400374)]
1类加载检查
当虚拟机遇到一条new命令时,首先会去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被,加载过,解析过和初始化过。如果没有,那必须先执行以下类加载过程(7个过程,加载,验证,准备,解析,初始化,使用,卸载)。
2分配内存
当类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需的内存大小在类加载完成后便可以确定,为对象分配内存空间就好像是将一块已知大小的内存从堆中划分出来。分配的方式有两种,“指针碰撞”和“空闲列表”两种,选择哪种分配方式有java堆是否规整决定,而java堆是否规整由采用的垃圾收集器是否带有压缩整理功能决定,也就是取决于GC收集器的算法是“标记-清除”还是“标记-整理”,复制算法内存也是规整的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dCXn0UKg-1616416172255)(file://C:\Users\24953\Desktop\Folder\javaguide\JavaGuide\docs\java\jvm\pictures\java内存区域\内存分配的两种方式.png?lastModify=1616401038)]
内存分配的并发问题
在创建一个对象时一定要保证线程是安全的,在实际开发过程中,创建线程是很频繁的事情。虚拟机采用两种方式来保证线程安全:
1.CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
2.TLAB:为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
3初始化零值
内存分配完成后,虚拟机需要将分配的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在java代码中可以不赋初始值就可以使用,程序能访问到这些字段的数据类型所对应的零值。
4设置对象头
设置完零值后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄情况。这些信息存放在对象头中。会根据虚拟机当前运行状态的不同,如是否启用偏向锁,对象头会有不同的设置方式。
init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init>
方法。
对象的内存布局
在Hotspot虚拟机中,对象在内存中的布局可以分为3块区域==,对象头,实例数据,对齐填充==
对象头分为两部分信息,第一部分用于存储对象自身的运行时数据(哈希码,GC分代年龄,锁状态标志等),第二部分为类型指针,即指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容
对象的访问定位
我们的java程序通过栈上的reference(参考)数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,主流的两种方式使用句柄和直接指针
-
句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6oau4vfG-1616416172257)(C:\Users\24953\Desktop\Folder\javaguide\JavaGuide\docs\java\jvm\pictures\java内存区域\对象的访问定位-使用句柄.png)]
-
直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DNZyIRfW-1616416172259)(C:\Users\24953\Desktop\Folder\javaguide\JavaGuide\docs\java\jvm\pictures\java内存区域\对象的访问定位-直接指针.png)]
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
垃圾回收算法
标记清除算法
分为标记和清除两个阶段,标记所有需要被回收的对象,标记结束后统一回收。就是把已死亡的对象标记为空闲内存,然后记录在一个空闲列表中,当我们需要new一个对象时,内存管理模块会从空闲列表中寻找空闲的内存来分给新的对象。
缺点是效率低下,这种做法会让内存中的碎片非常多,这个导致了如果我们需要使用到较大内存时,无法分配到足够的连续内存。
此时可使用的内存块都是零零散散的,导致了刚刚提到的大内存对象问题
复制算法
将可用内存按容量划分为两等分,每次只使用其中的一块。和Formplace跟ToPlace是一个性质,交换指针。FromPlace满了,就把存活的对象copy到ToPlace上,交换指针,这样解决了碎片问题。
缺点:这个算法的代价就是把内存缩水,这样堆内存的使用效率就会变得十分低下了。在对象存活率高的情况下也有一定的效率问题。
不过它们分配的时候也不是按照1:1这样进行分配的,就类似于Eden和Survivor也不是等价分配是一个道理。
标记整理算法
标记还是将存活的对象进行标记,但后续不是直接对可回收对象进行处理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。具体问题具体分析
主要的垃圾回收器
1.8默认的是Parallel Scavenge和Parallel Old,都是并行,前者新生代,后者老年代,前者使用复制算法,后者标记整理算法,目标都为吞吐量优先,使用场景都为,在后台运算而不需要太多交互的任务。
1.9默认的是G1,G1回收器的停顿时间最短而且没有明显的缺点,非常适合web应用。
Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F49jHH6d-1616416172265)(C:\Users\24953\Desktop\Folder\javaguide\JavaGuide\docs\java\jvm\pictures\jvm垃圾回收\46873026.png)]
虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
新生代采用标记-复制算法,老年代采用标记-整理算法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y9oxT8wu-1616416172267)(C:\Users\24953\Desktop\Folder\javaguide\JavaGuide\docs\java\jvm\pictures\jvm垃圾回收\22018368.png)]
它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
并行和并发概念补充:
-
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
-
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
Parallel Scavenge 收集器
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?
-XX:+UseParallelGC
使用 Parallel 收集器+ 老年代串行
-XX:+UseParallelOldGC
使用 Parallel 收集器+ 老年代并行
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用标记-复制算法,老年代采用标记-整理算法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FMSx0dwf-1616416172270)(C:\Users\24953\Desktop\Folder\javaguide\JavaGuide\docs\java\jvm\pictures\jvm垃圾回收\parllel-scavenge收集器.png)]
这是 JDK1.8 默认收集器
使用 java -XX:+PrintCommandLineFlags -version 命令查看
-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能
Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS收集器
**CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。**CMS 收集器是一种 “标记-清除”算法实现的
整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EAJqTVll-1616416172271)(file://C:\Users\24953\Desktop\Folder\javaguide\JavaGuide\docs\java\jvm\pictures\jvm垃圾回收\CMS收集器.png?lastModify=1616407158)]
主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
G1收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
1.7中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
G1 收集器的运作大致分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
并行和并发概念补充:
-
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
-
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
JVM调优
-Xmx:指java堆最大值(默认为物理内存的1/4(<1GB))
-Xms:初始java堆最小值(默认为物理内存的1/8(<1GB))
两个值会配成相同的值,目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分割计算堆区的大小而浪费资源。最大堆内存和最小堆内存如果数值不同会导致多次的gc,需要注意。
(MinHeapFreeRato参数可以调节)默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx,(MaxHeapFreeRato参数可以调节)默认空余堆内存大于70%时,JVM会减少堆知道-Xms的最小限制。
java会尽可能的将total memory的值维持在最小堆内存大小。
调整新生代和老年代的大小
-XX:NewRatio 新生代和老年代的比值
例如:-XX:NewRatio=4,表示新生代:老年代=1:4,即新生代占整个堆的1/5。在Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
调整Survivor区和Eden区的比值
-XX:SurvivorRatio 设置两个Survivor区和eden的比值
例如:8,表示两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10
设置年轻代和老年代的大小
-XX:NewSize 设置年轻代大小
-XX:MaxNewSize 设置年轻代最大值
可以通过设置不同参数来测试不同的情况,反正最优解当然就是官方的Eden和Survivor的占比为8:1:1
根据实际事情调整新生代和幸存代的大小,官方推荐新生代占java堆的3/8,幸存代占新生代的1/10
设置永久区
-XX:PermSize
-XX:MaxPermSize
初始空间(默认为物理内存的1/64)和最大空间(默认为物理内存的1/4)
jvm启动时,永久区一开始就占用了PermSize大小的空间,如果空间还不够,可以继续扩展,但是不能超过MaxPermSize,否则会OOM。
如果堆空间没有用完也抛出了OOM,有可能是永久区导致的。堆空间实际占用非常少,但是永久区溢出 一样抛出OOM。
JVM栈的参数调优
可以通过-Xss:调整每个线程栈空间的大小。
JDK5.0以后每个线程堆栈大小为1M,在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成。
-XXThreadStackSize:设置线程栈的大小
String类和常量池
String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd");//堆中创建一个新的对象
String str3 = new String("abcd");//堆中创建一个新的对象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false
这两种不同的创建方法是有差别的。
- 第一种方式是在常量池中拿对象;
- 第二种方式是直接在堆内存空间创建一个新的对象。
直接使用双引号声明出来的 String 对象会直接存储在常量池中。
String.intern() 是一个Native方法。如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。
String s1=new String(“abc”);创建了几个字符串对象?
将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// 输出 true
8种基本类型的包装类和常量池
Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。