java---JVM
Posted ᕱᕱ*
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java---JVM相关的知识,希望对你有一定的参考价值。
目录
- 1、JVM简介
- 2、内存溢出(OOM)
- 3、内存泄漏
- 4、垃圾回收(GC)
- 5、 深拷贝和浅拷贝
1、JVM简介
1.1JVM概念
- JVM:(Java Virtual Machine)java虚拟机,是虚拟机的一个简化版本,裁剪了很多内容,完成了Java字节码指令集的执行(其实是完成了翻译工作:将字节码指令翻译成了机器码)
- 虚拟机
指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统 - Java程序运行机制
- 总结
类的加载是指:将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区域的方法区中,然后在堆中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
注意:
1)对于单行的字节码指令的翻译,速度很快,但是大量的字节码指令翻译积少成多,时间消耗也会很大,因此引出了JIT编译器:热点代码在运行时,直接编译为机器码,之后再执行就不需要再翻译(提高效率)
2)java进程运行以后,是启动了一个虚拟机
3)编译期的编译(java文件编译为class字节码文件),运行期同时存在解释、(将字节码翻译为机器码)、执行(执行机器码)和编译(JIT编译器对热点代码的编译)
4) java虚拟机存在编译器(编译期的编译工作)、解释器(运行时的解释(即翻译))、JIT(即时)编译器
5)有很多第三方java虚拟机,可以把其他语言(如kotlin)编译为字节码,通过java虚拟机加载到内存执行
6)java虚拟机和程序是如何绑定的?
1.2JVM的主要组成部分及其作用
1.2.1主要组成(4部分=2子系统+2组件)
- 组成:两个子系统(类加载、执行引擎)+两个组件(运行时数据区域、本地接口)
1.1.2作用
首先通过编译器将java代码转换为字节码,类加载器再将字节码加载到内存中,将其放在运行时区域的方法区内,但是由于字节码文件只是JVM的一套指令集规范,并不能直接交给底层的操作系统去执行,所以需要特定的命令解释器执行引擎,将字节码翻译成底层系统指令,,再交由CPU执行,这个过程中需要调用其他语言的本地接口来实现整个程序的功能
1.1.3类加载相关子类父类的执行顺序问题
执行顺序:
1)父类类加载(如果没有加载则先加载)
2)子类类加载(如果没有加载则先加载
3)父类的构造方法:指字节码层面上对象的构造方法(收集成员变量,实例代码块,java重的构造方法)
4)父类的构造方法:指字节码层面上对象的构造方法(同上)
1.3Java运行时数据区域(内存划分 )
线程私有
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存
-
程序计数器(PC)
当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成; -
Java 虚拟机栈(Java Virtual Machine Stacks)
虚拟机栈描述的是Java方法执行的内存模型 : 每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息;每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程
注意:该区域在使用时可能会抛出两种异常:
1)StackOverflowError:方法调用链太深,递归调用太深 (线程请求的栈的深度大于虚拟机所允许的深度)
2)OOM:虚拟机在动态扩展时申请不到足够的内存 -
本地方法栈(Native Method Stack)
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;(native本地方法:基于JNI技术,调用其他语言的函数接口) -
Java 堆(Java Heap)
Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;与进程同存亡,在JVM启动时创建,所有的对象实例以及数组都要在堆上分配
注:如果在堆中没有足够的内存完成实例分配并且堆也无法在拓展时,将会抛出OOM -
方法区(Methed Area)
用于存储已被虚拟机加载的类信息、常量、静态变量、所有的方法、即时编译后的代码等数据
注:当方法区无法满足内存分配需求时,将抛出OOM异常。 -
运行时常量池(方法区的一部分)
存放字面量与符号引用
1)字面量 : 字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
2)符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符
补充:其他常量池
1)class文件常量池:java文件编译为class字节码文件,存在的常量池,存放编译期的字面量和符号引用
Person pi=new Person("p1"); //"p1"字符串编译期就存在在class常量池中
//而不是运行起来之后才将该值放到内存中去
2)java进程运行起来之后,即使没有执行到某行代码,也已经将class文件常量池中的内容放在运行时常量池中
public static void swap(int m,int n)
int tmp=m;
m=n;
n=tmp;
Person p1=new Person("p1");//即使该行代码没有被执行到,在java进程运行起来之后
//也已经将"p1"从class文件常量池放到了运行时常量池中
3)字符串常量池:1.7之前该池是在运行时常量池中,1.7之后,是在堆里面
补充知识
- 字面量比较
int a=10;
char b=10; //字符会转成数字
double c=10.0;
//以下四个都是字面量的比较
System.out.println(a==b); //true
System.out.println(a==c); //true
System.out.println(b==c); //true
System.out.println(a==d1); //true
- Integer和Integer比较
//Integer和Integer比较
// 1、如果都是常量赋值的,则在-128到127范围内的比较,就是常量池对象的比较,
// 2、如果在范围外,就是堆里面的对象的比较
// 3、如果有一个是new Integer()的方式创建的,不管大小,肯定不相等
Integer d1=10;
Integer d2=10;
System.out.println(d1==d2); //true
//此处是本来常量池中没有10,经过 Integer d1=10后
//常量池中有了10,所以d2是从常量池中拿的10
Integer d3=128;
Integer d4=128;
System.out.println(d3==d4); //false
//超过范围,就是堆里面对象的比较,此时测试不通过
Integer d5=10;
Integer d6=new Integer(10);
System.out.println(d5==d6); //false
- String比较
String s1="hello";
String s2="hello";
Assert.assertTure(s1==s2); //true
// s2的"hello"是从常量池中取出来的
String s3="hello";
String s4="he"+"llo";
//JVM在编译期会对常量进行优化:s4为字面量拼接的字符串"hello",存在于常量池中
Assert.assertTure(s3==s4); //true
String s5="hello";
String s6=new String("hello");
Assert.assertTure(s5==s6); //false
//调用intern方法时,如果池中已包含与equals(Object)方法确定的
//相当于此String对象的字符串,则返回来自常量池中
//否则,此String对象将被添加到池中,并返回对此String对象的引用
//s.intern()方法在字符串常量池中生成一个引用,引用指向的是s,返回的是字符串常量池重的内容
//(即一看到s.intern()就要想到返回的是字符串常量池重的内容)
Assert.assertTure(s5==s6.intern()); //true
String s7="hello";
String s8="hel";
String s9="lo";
String s10=s8+s9;
//JVM在编译期不会对变量进行优化
Assert.assertTure(s7==s10); //false
//字符串常量池:由StringBuilder创建的"计算机软件"
//堆:toString()方法生成的对象,其字面量为"计算机软件"
String s=new StringBuilder("计算机软件").toString();
Assert.assertTure(s.intern()=="计算机软件"); //true:常量池和和常量池对象之间的比较
Assert.assertTure(s.intern()==s); //false:常量池和堆中对象的比较
2、内存溢出(OOM)
2.1发生的场景(在除程序计数器之外的其他运行时数据区域中会发生)
存放类信息数据/变量/对象时,都要先分配内存空间,如果该区域空间不足,就会发生内存溢出,严重时整个进程会挂掉
- 具体发生场景:
java虚拟机栈,本地方法栈:创建栈帧时,若空间不足,OOM
方法区、堆:在类加载,创建对象时空间不足,先GC,如果还不足,OOM
2.2分配内存—GC—内存溢出之间的关系(简略流程)
2.3解决方案
1)优化代码,节省空间
如提高空间复杂度,复用一些已有变量、对象…
2)java进程启动时设置内存区域大小,如堆、栈、方法区
3)如果本机硬件配置不足,加内存,通过2来加大进程的内存
3、内存泄漏
3.1 定义
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中,导致无用对象越来越多,进程的可用空间越来越小
3.2 发生的场景
java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收
- 举例:自己实现一个服务端Session
Map<sessionid,Map<String ,Object>>------>用户只登录,不点击注销,就不会调用注销代码来删除,数据依然会保存在内存中
3.3 解决方案
1)优化程序:将不用的数据定时清理(结合一些工具,jamp,内存堆转储快照导出为本地文件,再借助一些分析工具,分析内存中无用的对象)
2)重启进程:临时解决方案,适用于一些老旧系统并且很难优化的情况
4、垃圾回收(GC)
注意:对于程序计数器(没有GC)、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了,因此有关内存分配和回收关注的为Java堆与方法区这两个区域
- 垃圾
Java进程运行后,如果某个类型(方法区中的类信息、堆中的类对象),常量(常量池),对象(堆里的)如果不了用,就成为垃圾 - 什么是垃圾回收?
Java进程启动后,gc垃圾回收的守护线程会回收以上的垃圾对象 - 垃圾回收的时机?
1)System.gc() ------>显示调用该方法,建议jvm进行垃圾回收,但不一定会执行,即有很大几率被回收
2)创建对象时,需要在对应的内存区域(堆、方法区)分配内存空间,如果该区域不足,就触发该区域的GC
注意:在垃圾回收层面中,方法区被称为永久代(不意味着对象进入后,真的是永久存在);堆被称为GC堆
3)jvm规定的其他回收条件
4.1 jvm层面,如何判定一个对象是垃圾(即对象已死)?------>判断垃圾算法
4.1.1 引用计数法
- 给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"
- 注意:在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的 循环引用问题
4.1.2 可达性分析算法
-
通过一系列称为"GC Roots(GC根:当前时间点,线程运行某个方法,在方法战阵中持有的引用的对象)“的对象作为起始点,从这些节点开始向下搜索(查看),搜索,走过的路径称之为"引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。
-
以下图为例:
-
对象是生存还是死亡?
即使在可达性分析算法中不可打的对象,也并非"非死不可",此时它们正处于"缓刑"阶段。
对象真正死亡的条件:进行可达性分析之后发现没有与GC Roots相连接的引用链并且对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过(finalize()方法是对象逃脱死亡的最后一次机会)
注意:
1)finalize()方法效率低
2)手动调用finalize()方法,不会造成对象死亡
补充:
- Java引用的4种类型(了解)
1)强引用:普通的new对象,都是强引用,只要是可达的,任何情况下都不能被回收
2)软引用:即使是可达的,如果要发生该区域的OOM,就会被回收
3)弱引用:即使是可达的, 如果该区域发生GC,就会被回收
4)虚引用:任何情况下,都无法通过虚引用获取到对象。其存在的目的是:在该对象被回收的时候,通知JVM
4.2 堆的GC
堆在GC时进一步划分为以下内存:
-
新生代(Young Generation)
1)新生代的GC又被称为Young GC(YGC)、Minor GC(次要的GC,指的是对用户线程的影响来说是次要的)
2)java对象的多数都具备朝生夕灭的特性(大面积在方法中创建局部对象,方法返回后,栈帧清除,堆里面new出来的对象就没有任何引用再指向它了,此时变为可回收对象),因此Minor GC非常频繁,一般回收速度也比较快 -
老年代(Old Generation)
1)老年代的GC又被称为Old GC,Major GC(主要的GC,指的是对用户线程的影响来说是主要的)
2)Major GC的速度一般会比Minor GC慢10倍以上
3)Full GC:不同场景下,可能指老年代的GC,也可能是全堆GC,也可能是用户线程暂停的GC
4.3垃圾回收算法(老年代算法)
JVM是使用垃圾回收线程执行垃圾回收工作,基于垃圾回收器(每一种垃圾回收器使用的回收算法可能不同,还可能有多种),根据垃圾回收算法,执行垃圾回收工作,回收垃圾(不可达对象)
4.3.1 标记-清除算法
- 过程分为两个阶段:
1)标记:标记不可达对象
2)清除:清理垃圾 - 两个缺陷:
1)标记,清除两个阶段效率都不高
类似于本地删除文件,遍历某个文件夹,遍历10万个文件,标记1000个垃圾,遍历标记的1000个垃圾并删除
2)回收后还存在内存碎片(其实空间还够用,只是连续空间不够用)
4.3.2 复制算法(新生代回收算法)
- 过程
将可用内存划分为大小完全相同的两块,每次只是使用一块,当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配
时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针(把内存中的地址移动过去),按顺序分配即可。此算法实现简单,运行高效------>类似在某个文件夹的10万个文件中,将可用的50个文件直接复制到另一个文件夹,再将之前的文件夹整个清空 - 优点
1)效率高(对象朝生夕死,存活的对象也就不多,复制起来也很快,清理起来也很快)
2)没有内存碎片问题 - 缺点:
空间利用率不高,只有50% - 使用场景
对象存活率较低
4.3.3 标记-整理算法(老年代回收算法)
- 过程
标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存 - 优点
存活对象较多的时候,效率比较高,没有内存碎片问题
4.3.4 分代收集算法
-将以上GC堆划分为:
1)新生代(Eden区、From Survivor区(S0)、To Survivor区(S1))
2)老年代
- 算法
1)新生代:复制算法(此时内存划分比例不再为1:1,而是8(Eden):1(S0):1(S1))
具体流程
将内存(新生代内存)分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
2)老年代:标记-整理算法、标记-清除算法
4.4垃圾回收过程
-
Eden空间不足,触发Minor GC:用户线程创建的对象优先分配在Eden区,当Eden区空间不够时,会触发整个新生代的(Eden+S0+S1)Minor GC:将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间
-
垃圾回收结束后,用户线程又开始新创建对象并分配在Eden区,当Eden区空间不足时,重复上次的步骤进行Minor GC,但是此次GC时,使用的空间变为Eden+S1,使用S0来保留存活对象 (即S0和S1交替)
-
以上空间利用率:90%
-
年老对象晋升到老年代
由于虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应该放在 新生代,哪些对象应该放在老年代中
为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过一次Minor GC后仍然存活(即每回收一次,存活下来的对象年龄+1),当它的年龄增加到一定程度(默认为15岁),就将晋升到老年代中。 -
Survivor空间不足,存活对象通过分配担保机制进入老年代(4.5.4)
-
老年代空间不足,触发Major GC
由4.5.2和4.5.3可知,当老年代空间不足时,也需要对老年代进行垃圾回收,即出发Major GC
4.5 对象的内存分配与回收策略
4.5.1对象优先分配在Eden区
- 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发生一次Minor GC
4.5.2 长期存活的对象进入老年代
新生代的GC,如果对象在新生代GC之后还可以存活,年龄+1,超过年龄阈值(15)就进入老年代
4.5.3 大对象直接进入老年代
- 大对象存在的问题:
1)需要大量连续的空间(很很长的字符串/很大的数组)
2)存放的对象的特性都是’朝生夕死",经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来放置大对象。
因此有设置JVM大对象的一个参数(单位:bit),超过大对象的阈值,直接进入老年代
- 目的
新生代空间相对老年代小很多,新生代使用复制算法,如果查创建的大对象放在新生代,复制起来效率较低
4.5.4 空间分配担保机制
新生代GC后(相当于把贷的款剩下的还了之后),如果存活对象>10%(即花的贷款的钱>1000),空置的一个S区无法存放(即已有银行存款金额无法偿还)
-
解决方案
-
担保异常情况:担保人资产可能不够偿还
-
避免异常的做法:
-
动态对象年龄判定
新生代GC时,存活对象中,,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
4.6垃圾收集器
重点掌握:
- ParNew+CMS:用户体验优先
- Parallel Scanvenge + Parallel Old :性能优先(吞吐量)
- G1:用户体验优先
前置知识
-
用户线程暂停:用户线程和GC线程并发地执行,用户线程可能会让一些垃圾对象重新加入引用链,此时区清理垃圾就不行了,因此在某些时间,需要暂停用户线程,只运行GC线程
-
垃圾收集器考虑的两个指标
1)用户体验优先:用户线程单次停顿时间短,即使总的停顿时间长一点也可以接受。
2)吞吐量优先:用户线程总的停顿时间短,即使单次停顿时间长一点也可以接受。 -
三个应了解的概念
1)并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态
2)并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序在另外一个CPU上
3)吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值
4.6.1 Serial收集器(新生代收集器,串行GC)
- 特性
1)单线程
2)复制算法
4.6.2 ParNew收集器(新生代收集器,并行GC)
- 特性
1)多线程
2)复制算法
4.6.3 Parallel Scavenge收集器(新生代收集器,并行GC)
- 特性
1)多线程
2)复制算法 - 应用场景
“吞吐量优先”收集器,适用吞吐量需求高的任务型程序
4.6.4 Serial Old收集器(老年代收集器,串行GC)
- 特性
1)单线程
2)"标记-整理"算法
4.6.5 Parallel Old收集器(老年代收集器,并行GC)
- 特性
1)多线程
2)“标记-整理”算 - 应用场景
“吞吐量优先”:在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加
Parallel Old收集器。
4.6.6 CMS收集器(面试)(老年代收集器,并发GC)
设计目标:以获取最短回收停顿时间为目标
.1特性
1)并发收集,低停顿
2)用户体验优先
3)"标记-清除"算法
整个过程分为4个步骤:
- 初始标记(CMS initial mark)
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop TheWorld”------>(找根节点直接关联的对象) - 并发标记(CMS concurrent mark)
并发标记阶段就是进行GC Roots Tracing的过程------>(根据GC Roots找引用链上对象) - 重新标记(CMS remark)
重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”------>(修正第二个阶段用户线程并发修改过的标记) - 并发清除(CMS concurrent sweep)
并发清除阶段会清除对象
注意
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。所以其特性为并发收集、低停顿、用户体验优先
.2应用场景
很大一部分的Java应用集中在互联网站或者B/S系统的服务端
.3缺陷
1)用户体验优先,就存在性能下降(CPU要花费更多时间GC),单次停顿时间越短、总停顿时间就变成,有效代码执行占比下降
2)浮动垃圾问题 - 浮动垃圾(老年代的垃圾)
CMS第四个阶段,用户线程并发执行产生过的新的对象,其中包含垃圾对象(浮动垃圾),由于没有了标记,所以不能被清除。CMS只能在下一次收集中处理它们。这也导致了CMS不能在老年代几乎完全被填满了再去进行收集,必须预留一部分空间提供给并发收集时程序运作使用。 - 产生的问题
○需要预留空间
基于CMS收集器的老年代GC,不能等老年年代满了才执行GC,需要预留空间给CMS第四个阶段,用户线程并发地执行创建的对象
○并发模式失败的原因
预留空间还不够
解决方案:
当次GC还在CMS的第四个阶段执行,但又触发另一次的老年代GC(使用Serial Old)
但是这种情况也存在不足,两个GC串行执行,频繁出现并发模式失败,会使停顿时间急剧上升
3)CMS的"标记-清除"算法,会导致大量空间碎片的产生
4.6.7 G1收集器(唯一一款全区域的垃圾回收器)
【某个区域空间不够时,就将空白区域临时指定为某个其他区域】
- 用在堆空间很大的情况下,把堆划分为很多很多的region块,然后并行的对其进行垃圾回收。
- G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部即两个region之间基于"复制"算法)的策略来对region进行垃圾回收的。
- 用户体验优先
- G1收集器采用的算法都意味着 一个region有可能属于Eden,Survivor或者Tenured
内存区域。 - G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩
.1年轻代的垃圾收集
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域
.2老年代垃收集
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段:
- 初始标记(Initial Mark)阶段
同CMS垃圾收集器的Initial Mark阶段一样,但是G1的垃圾收集器的Initial Mark阶段是跟minor gc一同发生的。即在G1中,不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minor gc的时候一并将年老代上的Initial Mark给做了 - 并发标记(Concurrent Mark)阶段
在这个阶段G1做的事情跟CMS一样,Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面clean up阶段,同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。 - 最终标记(CMS中的Remark阶段)
跟CMS一样, 但是采用的算法不同 - 筛选回收(Clean up/Copy)阶段
和CMS不同,此处的是minor gc一同发生的
5、 深拷贝和浅拷贝
- 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
- 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
区分: - 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
- 深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
以上是关于java---JVM的主要内容,如果未能解决你的问题,请参考以下文章