1.java虚拟机HotSpot
原文章地址:https://blog.csdn.net/weixin_40449300/article/details/85529219
1.1前言
在自己电脑上输入java -version时出来:Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
,不知道是啥意思。查阅资料发现HotSpot是java的虚拟机。把前因后果写在下边。
1.2 HotSpot历史
SUN的JDK版本从1.3.1开始运用HotSpot虚拟机, 2006年底开源,主要使用C++实现,JNI(Java Native Interface
)接口部分用C实现。HotSpot是较新的Java虚拟机,用来代替JIT(Just in Time),可以大大提高Java运行的性能。 Java原先是把源代码编译为字节码在虚拟机执行,这样执行速度较慢。
而HotSpot将常用的部分代码编译为本地(原生,native)代码
,这样显着提高了性能。
HotSpot JVM 参数可以分为规则参数(standard options)
和非规则参数(non-standard options)
。
- 规则参数相对稳定,在JDK未来的版本里不会有太大的改动。
- 非规则参数则有因升级JDK而改动的可能。
1.3 HotSpot基础知识
HotSpot包括一个解释器
和两个编译器
(client 和 server,二选一的),解释与编译混合执行模式,默认启动解释执行。
- 编译器:java源代码被编译器编译成class文件(字节码),java字节码在运行时可以被动态编译(JIT)成本地代码(前提是解释与编译混合执行模式且虚拟机不是刚启动时)。
- 解释器: 解释器用来解释class文件(字节码),java是解释语言(书上这么说的)。
- server启动慢,占用内存多,执行效率高,适用于服务器端应用;
- client启动快,占用内存小,执行效率没有server快,默认情况下不进行动态编译,适用于桌面应用程序。
由-XX:+RewriteFrequentPairs参数控制client模式默认关闭,server模式默认开启
,在jre安装目录下的lib/i386/jvm.cfg 文件下。
- java -version
Java HotSpot™ Client VM (build 14.3-b01, mixed mode, sharing)
mixed mode 解释与编译 混合的执行模式 默认使用这种模式
- java -Xint -version
Java HotSpot™ Client VM (build 14.3-b01, interpreted mode, sharing)
interpreted 纯解释模式 禁用JIT编译
- java -Xcomp -version
Java HotSpot™ Client VM (build 14.3-b01, compiled mode, sharing)
compiled 纯编译模式(如果方法无法编译,则回退到解释模式执行无法编译的方法)
1.4 动态编译
动态编译(compile during run-time)
,英文称Dynamic compilation;Just In Time也是这个意思。
HotSpot对bytecode(字节码)
的编译不是在程序运行前编译的,而是在程序运行过程中编译
的。HotSpot里运行着一个监视器(Profile Monitor)
,用来监视程序的运行状况。
java字节码(class文件)是以解释
的方式被加载到虚拟机中(默认启动时解释执行)。 程序运行过程中,那一部分运用频率大,那些对程序的性能影响重要。对程序运行效率影响大的代码,称为热点(hotspot)
,HotSpot会把这些热点动态地编译成机器码(native code),同时对机器码进行优化,从而提高运行效率
。对那些较少运行的代码,HotSpot就不会把他们编译。
HotSpot对字节码有三层处理:
- 不编译(字节码加载到虚拟机中时的状态。也就是当虚拟机执行的时候再编译)
- 编译(把字节码编译成本地代码。虚拟机执行的时候已经编译好了,不要再编译了)
- 编译并优化(不但把字节码编译成本地代码,而且还进行了优化)。
至于那些程序那些不编译,那些编译,那些优化,则是由监视器(Profile Monitor)决定。
1.5 为什么不动态编译
为什么字节码在装载到虚拟机之前就编译成本地代码那?
- 动态编译器也在许多方面比静态编译器优越。静态编译器通常很难准确预知程序运行过程中究竟什么部分最需要优化。
- 函数调用都是很浪费系统时间的,因为有许多进栈出栈操作。因此有一种优化办法,就是把原来的函数调用,通过编译器的编译,改成非函数调用,把函数代码直接嵌到调用出,变成顺序执行。
- 面向对象的语言支持多态,静态编译无效确定程序调用哪个方法,因为多态是在程序运行中确定调用哪个方法。
2.HotSpot虚拟机在java 1.8中的新实现
有关jvm虚拟机运行内存详细信息:
https://blog.csdn.net/Zz110753/article/details/70170339
https://blog.csdn.net/u011080472/article/details/51320300
原文章地址:https://www.jianshu.com/p/a7e984a858ca
Java HotSpot 虚拟机是 Java SE 平台的一个核心组件。它实现 Java 虚拟机规范,并作为 Java 运行时环境中的一个共享库
来提供。作为 Java 字节码执行引擎,它在多种操作系统和架构上提供 Java 运行时设施,如线程和对象同步。它包括自适应将 Java 字节码编译成优化机器指令的动态编译器
,并使用为降低暂停时间
和吞吐量
而优化的垃圾收集器来高效管理 Java 堆
。它为分析、监视和调试工具及应用程序提供数据和信息。
1.8中的新特性:JAVA 从永久区(PermGen)到元空间(Metaspace)
2.1 JAVA 从永久区(PermGen)到元空间(Metaspace)
在Java虚拟机(JVM)内部,class文件中包括类的版本、字段、方法、接口
等描述信息,还有运行时常量池
,用于存放编译器生成的各种字面量和符号引用
。
在过去类大多是”static”的,很少被卸载或收集,因此被称为“永久的(Permanent)”。
同时,由于类class是JVM实现的一部分,并不是由应用创建的
,所以又被认为是“非堆(non-heap)”内存
。
在JDK8之前的HotSpot JVM,存放这些”永久的”的区域叫做“永久代(permanent generation)”。
永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数-XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M(64位JVM由于指针膨胀,默认是85M)。
永久代的垃圾收集是和老年代(old generation)捆绑在一起的
,因此无论谁满了,都会触发永久代和老年代的垃圾收集。
不过,一个明显的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OOM的错误
2.2 Metaspace(元空间)
jdk1.8中则把永久代给完全删除了,取而代之的是 MetaSpace
2.2.1 metaspace的组成
metaspace其实由两大部分组成
- Klass Metaspace
- NoKlass Metaspace
Klass Metaspace就是用来存klass的,klass是我们熟知的class文件在jvm里的运行时数据结构
,不过有点要提的是我们看到的类似A.class其实是存在heap里的,是java.lang.Class的一个对象实例
。
这块内存是紧接着Heap的,和我们之前的perm一样
,这块内存大小可通过-XX:CompressedClassSpaceSize参数来控制,这个参数前面提到了默认是1G,但是这块内存也可以没有,假如没有开启压缩指针就不会有这块内存,这种情况下klass都会存在NoKlass Metaspace里,另外如果我们把-Xmx设置大于32G的话,其实也是没有这块内存的,因为这么大内存会关闭压缩指针开关。还有就是这块内存最多只会存在一块
。
NoKlass Metaspace专门来存klass相关的其他的内容,比如method,constantPool(常量池)等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。
这块内存是必须的,虽然叫做NoKlass Metaspace,但是也其实可以存klass的内容,上面已经提到了对应场景。
Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的
,所以类加载器们要分配内存,但是每个类加载器都有一个SpaceManager,来管理属于这个类加载的内存小块
。
如果Klass Metaspace用完了,那就会OOM了,不过一般情况下不会,NoKlass Mestaspace是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。
2.2.2 Metaspace的内存分配与管理
Metaspace VM利用内存管理技术
来管理Metaspace。
这使得由不同的垃圾收集器来处理类元数据的工作,现在仅仅由Metaspace VM在Metaspace中通过C++来进行管理。
Metaspace背后的一个思想是,类和它的元数据的生命周期是和它的类加载器的生命周期一致的。
(元数据:描述数据的数据。类的元数据可以理解为描述类信息的数据。)也就是说,只要类的类加载器是存活的,在Metaspace中的类元数据也是存活的,不能被释放。每个类加载器存储区叫做“a metaspace”。这些metaspaces一起总体称为”the Metaspace”。
仅仅当类加载器不再存活,被垃圾收集器声明死亡后,该类加载器对应的metaspace空间才可以回收。Metaspace空间没有迁移和压缩。但是元数据会被扫描是否存在Java引用。Metaspace VM使用一个块分配器(chunking allocator)来管理Metaspace空间的内存分配。
块的大小依赖于类加载器的类型。其中有一个全局的可使用的块列表(a global free list of chunks)。当类加载器需要一个块的时候,类加载器从全局块列表中取出一个块,添加到它自己维护的块列表中。当类加载器死亡,它的块将会被释放,归还给全局的块列表。块(chunk)会进一步被划分成blocks,每个block存储一个元数据单元(a unit of metadata)。Chunk中Blocks的分配线性的(pointer bump)。这些chunks被分配在内存映射空间(memory mapped(mmapped) spaces)之外
。在一个全局的虚拟内存映射空间(global virtual mmapped spaces)的链表,当任何虚拟空间变为空时,就将该虚拟空间归还回操作系统
。
2.2.3 Metaspace VM内存碎片问题
先前提到的,Metaspace VM使用块分配器(chunking allocator)。chunk的大小取决于类加载器的类型。由于类class并没有一个固定的尺寸,这就存在这样一种可能:
可分配的chunk的尺寸和需要的chunk的尺寸不相等,这就会导致内存碎片。
Metaspace VM还没有使用压缩技术,所以内存碎片是现在的一个主要关注的问题。
2.2.4 Metaspace 总结
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:
元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小。
2.2.5 MetaSpace应该掌握的知识
- 存放类相关信息的地方也不在heap(堆)中。在元空间里。
- 在jdk1.8中没有永久代的概念
- metaspace其实由两大部分组成
- 有关于常量池的知识
https://blog.csdn.net/q5706503/article/details/84640762
https://blog.csdn.net/ychenfeng/article/details/77413206
在JDK1.7
之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
在JDK1.7
字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
在JDK1.8
hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
Klass Metaspace
存放klass的,klass是我们熟知的class文件在jvm里的运行时数据结构,这个空间的默认大小是1G
NoKlass Metaspace
专门来存klass相关的其他的内容,比如method,constantPool(常量池)等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。这块内存是必须的
Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以类加载器们要分配内存,但是每个类加载器都有一个SpaceManager,来管理属于这个类加载的内存小块。
如果Klass Metaspace用完了,那就会OOM了,不过一般情况下不会,NoKlass Mestaspace是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。
- metaspace主要相关参数
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整: 如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 - 为什么要将永久代替换成Metaspace?
字符串存在永久代中,容易出现性能问题和内存溢出。
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
Oracle 可能会将HotSpot 与 JRockit 合二为一。
3.引用计数法和根搜索算法
原文章地址:https://blog.csdn.net/qq_36866808/article/details/78663287
3.1 如何判断一个对象是否存活
3.1.1 引用计数法
引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。
首先需要声明,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存。
引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。
public static void main(String[] args) {
GcObject obj1 = new GcObject(); //Step 1
GcObject obj2 = new GcObject(); //Step 2
obj1.instance = obj2; //Step 3
obj2.instance = obj1; //Step 4
obj1 = null; //Step 5
obj2 = null; //Step 6
}
}
class GcObject{
public Object instance = null;
}
当程序进行到Step 1 时 obj1 的计数器=1;
当程序进行到Step 2 时 obj2 的计数器=1;
程序进行到Step 3 时 obj1 的计数器=2;
程序进行到Step 4 时 obj2 的计数器=2;
当程序进行到Step 5 时 obj1 的计数器-1 结果为1;
当程序进行到Step 6 时 obj2 的计数器-1 结果为1
那么如果采用的引用计数算法
的话 GcObject实例1和实例2的计数引用都不为0,两个实例所占内存不到释放,于是就产生了内存泄露
3.1.2 根搜索算法
根搜索算法的基本思路就是通过一系列名为**”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)**,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java语言中,可以作为GCRoots的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
如上图中,GC Root对object1,object2,object3,object4都是仍存活的对象,object5,object6,object7都是GC Root 触及不到的,因此是都要被回收的内存。
对于垃圾收集器力来说,堆中每个对象都有三种状态,可触及的
,可复活的
,不可触及的
。不可触及的也就是我们刚提的 GC Root引用链连接不到的对象。对于另外的两种,可触及的,是指每个对象从他生命的开始起,只要程序至少保留一个可触及的引用,那么它就是可触及的,一旦程序释放了所有对该对象的引用,那么这个对象就成了可复活状态
。
若满足以下条件则为可复活状态: - 从根节点连接不到,但是可能在垃圾收集器执行某些终结方法时触及;不仅仅是声明了finalize()方法的对象,而是所有的对象都要经过可复活状态
- 先来说finalize()方法,如果类中声明了此方法,垃圾回收器(GC)会在释放这个实例占据的内存空间之前,执行这个方法如下
class Final(){
protected void finalize(){
System.out.println("Final obj die!"); //go die
}
}
根搜索算法中不可达对象在回收之前,要进行二次标记
。
- 第一次标记时会进行一次筛选:筛选的条件是是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法,或者finalize()被虚拟机调用过,则虚拟机认为没有必要执行finalize()方法。 - 如果这个对象有必要执行finalize(),则会放在一个队列里,以一个低优先级的线程进行执行finalize()方法进行二次标记,如果在finalize()方法中,对象重新回到引用链上(比如this赋值给其他引用链上的对象),则该对象不被回收,而移出该队列。
注意:finalize()方法只被调用一次,如果这个对象在GC时被调用过一次finalize()方法,则第二次GC的时候,就会被判断为没有必要执行finalize()而被直接回收。
另外finalize()能做的所有工作,都可以通过try-finally更好、更及时的解决。所以请忘掉finalize().
public class TestGc {
public static TestGc HOOK;
@Override
protected void finalize() throws Throwable {
super.finalize();
TestGc.HOOK = this;
System.out.println("finalize");
}
/*
输出结果
finalize
HOOK is alvie
HOOK is dead
*/
public static void main(String[] args) throws Exception {
HOOK = new TestGc();
HOOK = null;
System.gc();//第一次GC,符合有必要要执行finalize的条件
Thread.sleep(500);//Finalizer线程优先级较低,所以要等一会
if (HOOK != null) {
System.out.println("HOOK is alvie");
} else {
System.out.println("HOOK is dead");
}
HOOK = null;
System.gc();//第二次GC,因为已经执行过一次finalize,所以没有必要进行二次标记
Thread.sleep(1000);//Finalizer线程优先级较低,所以要等一会
if (HOOK != null) {
System.out.println("HOOK is alvie");
} else {
System.out.println("HOOK is dead");
}
}
}
3.1.3 引用的类型
无论是引用计数法,还是跟搜索法都是引用操作,那么就有可能产生垃圾问题,但是对于引用也需要有一些合理化的设计。在很多的时候并不是所有的对象都需要被我们一直使用,那么就需要对引用的问题做进一步的思考。从JDK1.2之后关于引用提出了四种类型的引用:
原文地址: http://blog.csdn.net/qq_34280276/article/details/52863626;
- 强引用:当内存不足的时候,JVM宁可出现OutOfMemory错误停止,也需要进行保存,并且不会将此空间回收;
- 软引用:当内存不足的时候,进行对象的回收处理,往往用于高速缓存中;
- 弱引用:不管内存是否紧张,只要由垃圾产生了,那么立即回收;
- 幽灵引用:和没有引用是一样的。
- 强引用是JVM默认的支持模式,即:在引用的期间内,如果该堆内存被指定的栈内存有联系,那么该对象就无法被GC所回收,而一旦出现了内存空间不足,就会出现“OutOfMemoryError”错误信息。
package cn.test.demo;
public class TestDemo{
public static void main(String[]args){
Object obj=new Object();//强引用,默认的支持
Object ref=obj;//引用传递
obj=null;//断开了一个连接
System.gc();
System.out.println(ref);
}
}
如果此时堆内存有一个栈内存指向,那么该对象将无法被GC回收。强引用是我们一直在使用的模式,并且也是以后开发之中主要的使用模式,正因为强引用具备这样的内存分配异常问题,所以,尽量少实例化对象
- 在许多的开源组件之中,往往会使用软引用作为
缓存组件
出现,其最大的特点在于:不足时回收,充足时不回收
。想实现软引用,则需要有一个单独的类来实现控制:java.lang.ref.SoftReference
。这个类的方法如下:
● 构造:public SoftReference(T referent)
● 取出数据:public T get()
public class TestDemo{
public static void main(String[]args){
Object obj=new Object();
SoftReference<Object> ref=new SoftReference<Object><obj>;//软引用
obj=null;//断开连接
System.gc();
System.out.println(ref.get());
}
}
如果此时内存空间充足,那么对象将不会回收,如果空间不充足,则会进行回收。
public class TestDemo{
public static void main(String[]args){
Object obj=new Object();
String str="hello";
obj=null;//断开连接
SoftReference<Object> ref=new SoftReference<Object><obj>;//软引用
try{
for(int x=0;x<Inter.MAX_VALUE;x++){
str+=str+x;
str.intern();
}
}catch(Throwable e){
}
System.out.println(ref.get()+"##############################");
}
}
- 弱引用本质的含义指的是说只要一进行GC处理,那么所引用的对象将会被立刻回收。弱引用需要使用的是Map接口的子类:
java.util.WeakHashMap
。
public class TestDemo{
public static void main(String[]args){
String key=new String(“hi”);
String value=new String(“hello”);
Map<String,String> map=new WeakHashMap<String,String>();
map.put(key,value);
System.out.println(map.get(key));
key=null;
System.out.println(map);
System.gc();
System.out.println(map);
}
}
一旦出现GC,则必须进行回收处理,而且一回收一个准。
HashMap与WeakHashMap区别? HashMap是强引用,而WeakHashMap是弱引用。
package cn.test.demo;
import java.lang.ref.SoftReference;
public class TestDemo{
public static void main(String[]args){
String key=new String(“hi”);
WeakReference<String> map=new WeakHashMap<String>(key);
Key=null;
System.out.println(ref.get());
System.gc();
System.out.println(ref.get());
}
}
弱引用之所以不敢轻易使用的原因,就是因为其本身一旦有了GC之后就会立刻清空,这个对于程序的开发不利。
- 引用队列
所谓的引用队列就是保存那些准备被回收的对象
。很多的时候所有的对象的回收扫描都是从根对象开始的。
那么对于整个GC而言,如果要想确定那些对象可以被回收,就必须确定好引用的强度,这个也就是所谓的引用路径的设置。
如果现在要找到对象5,那么很明显1找到5属于“强”+“软”,而2找到5属于“强”+“弱”。软引用要比弱引用保存的强,所以这个时候实际上对于对象的引用而言,如果要进行引用的关联判断,那么就必须找到强关联,那么为了避免非强引用对象所带来的内存引用问题,所以提供有一个引用队列的概念,如果在创建软引用或者弱引用类型的时候使用了引用队列的方式,则这个对象被回收之后会自动保存在引用队列之中
。这种引用队列主要是做一些被回收对象的控制,意义不大,了解即可。
package cn.test.demo;
import java.lang.ref.SoftReference;
public class TestDemo{
public static void main(String[]args)throws Exception{
Object obj=new Object();
ReferenceQueue<Object> queue=new ReferenceQueue<>();
WeakReference<Obeject> ref=new WeakReference<Object>(obj,queue);
System.out.println(queue.poll());
obj=null;
System.gc();
Tread.sleep(200); //延迟200毫秒
System.out.println(queue.poll());
}
}
- 幽灵引用(虚引用)
永远取得不了的数据就叫做幽灵引用。所有保存在幽灵引用类型中的数据都不会真正的保留。
package cn.test.demo;
import java.lang.ref.SoftReference;
public class TestDemo{
public static void main(String[]args) throws Exception{
Object obj=new Object();
ReferenceQueue<Object> queue=new ReferenceQueue<Object>();
PhantomReference<Object> ref=new PhantomReference<Object>(obj,queue);
System.gc();
System.out.println(ref.get());
System.out.println(queue.poll());
}
}
4.垃圾收集算法 标记-清除、复制、标记整理、分代收集
原文地址:https://www.jianshu.com/p/5d612f36eb0b
4.1 标记-清除算法(Mark-Sweep)
分为标记
和清除
两阶段:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象
缺点:
- 标记阶段和清除阶段的
效率都不高
。 - 显而易见的,清除后产生了
大量不连续的内存碎片
,导致在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。
4.2 复制算法(Copying)
将可用内存按容量划分为大小相等的两块,每次只用其中一块。当这块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
特点:
- 效率高于标记-清除,但效率也不是很高。
- 不会产生过多碎片,每次都对半块内存操作,浪费内存。
4.3 标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
。
特点:
自带整理功能,这样不会产生大量不连续的内存空间,适合老年代的大对象存储。
4.3 分代收集算法(Generational Collection)
当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述三种算法整合了一下。具体如下:
根据各个年代的特点采取最适当的收集算法
- 老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
4.4 JVM垃圾回收机制优化
新生代被分为三个区:Eden、From Survivor、To Survivor
新生代中98%的对象都是“朝生夕死”的,所以不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
Eden和Survivor比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”
。我们没有办法保证每次回收都只有不多于10%的对象存货,所以当Survivor空间不够用时,需要依赖其他内存(指老年代)进行分配担保(Handle Promotion)
。
JVM动态对象年龄问题:
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC
)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。
好,那我们来想想在没有Survivor的情况下,有没有什么解决办法,可以避免上述情况:
方案 | 优点 | 缺点 |
---|---|---|
增加老年代空间 | 更多存活对象才能填满老年代。 | 降低Full GC频率 随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长 |
减少老年代空间 | Full GC所需时间减少 | 老年代很快被存活对象填满,Full GC频率增加 |
显而易见,没有Survivor的话,上述两种解决方案都不能从根本上解决问题。
我们可以得到第一条结论:Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生
)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中
其他文章:
JVM内存中各个区域的作用:
https://blog.csdn.net/u011080472/article/details/51320300
对象的创建:
https://www.jianshu.com/p/3fecd4286f78
GC收集器:
https://blog.51cto.com/12445535/2372976
JVM类加载机制
http://www.importnew.com/25295.html