一、 内存模型以及分区,需要详细到每个区放什么?
(1)栈区:栈分为java虚拟机栈和本地方法栈
虚拟机栈:线程私有的,生命周期与线程相同。每个方法执行都会创建一个栈帧,用于存放局部变量表,操作栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。通常说的栈就是指局部变量表部分,存放编译期间可知的8种基本数据类型,及对象引用和指令地址。局部变量表是在编译期间完成分配,当进入一个方法时,这个栈中的局部变量分配内存大小是确定的。
注:会有两种异常StackOverFlowError和 OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。
本地方法栈:本地方法栈为虚拟机使用到本地方法服务(native)
(2)堆区:所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例;
堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区主要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1;
注:会有异常OutOfMemoneyError;
注:基于Hotspot虚拟机划分,把方法区算着为永久代;
(3)方法区:
被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代;
垃圾回收很少光顾这个区域,不过也是需要回收的,主要针对常量池回收,类型卸载;
常量池用于存放编译期生成的各种字节码和符号引用,常量池具有一定的动态性,里面可以存放编译期生成的常量;运行期间的常量也可以添加进入常量池中,比如string的intern()方法。
(4)程序计数器:
当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。
Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。
为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。
注:唯一一块Java虚拟机没有规定任何OutofMemoryError的区块。
二、 堆里面的分区:Eden,survivalfrom、tosurvival;老年代,各自的特点。
1.JVM中堆空间可以分成三个大区,新生代、老年代、永久代。
2.新生代可以划分为三个区,Eden区,两个幸存区。
在JVM运行时,可以通过配置以下参数改变整个JVM堆的配置比例
1.JVM运行时堆的大小
-Xms堆的最小值
-Xmx堆空间的最大值
2.新生代堆空间大小调整
-XX:NewSize新生代的最小值
-XX:MaxNewSize新生代的最大值
-XX:NewRatio设置新生代与老年代在堆空间的大小
-XX:SurvivorRatio新生代中Eden所占区域的大小
3.永久代大小调整
-XX:MaxPermSize
三、垃圾收集算法
1、复制算法:
对复制算法进一步优化:使用Eden/S0/S1三个分区
平均分成A/B块太浪费内存,采用Eden/S0/S1三个区更合理,空间比例为Eden:S0:S1==8:1:1,有效内存(即可分配新生对象的内存)是总内存的9/10。
算法过程:
1. Eden+S0可分配新生对象;
2. 对Eden+S0进行垃圾收集,存活对象复制到S1。清理Eden+S0。一次新生代GC结束。
3. Eden+S1可分配新生对象;
4. 对Eden+S1进行垃圾收集,存活对象复制到S0。清理Eden+S1。二次新生代GC结束。
5. goto 1。
默认Eden:S0:S1=8:1:1,因此,新生代中可以使用的内存空间大小占用新生代的9/10,那么有人就会问,为什么不直接分成两个区,一个区占9/10,另一个区占1/10,
这样做的原因大概有以下几种:
1.S0与S1的区间明显较小,有效新生代空间为Eden+S0/S1,因此有效空间就大,增加了内存使用率
2.有利于对象代的计算,当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后,会将其分到老年代中;
设想一下,如果没有S0/S1,直接分成两个区,该如何计算对象经过了多少次GC还没被释放?
你可能会说,在对象里加一个计数器记录经过的GC次数,或者存在一张映射表记录对象和GC次数的关系。
是的,可以,但是这样的话,会扫描整个新生代中的对象, 有了S0/S1我们就可以只扫描S0/S1区了。
复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象
比较少时,极为高效,但是带来的成本是需要一块内存交换空间进行对象的移动。也就是s0,s1等空间。
2、标记-清除算法(Mark-Sweep):
从根节点开始标记所有可达对象,其余没有标记的即为垃圾对象,执行清除。但回收后的空间是不连续的。
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,
但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
3、标记-整理法:
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时,在回收不存活的对象占用的空间后,
会将所有的存活对象网左端空闲空间移动,并更新相应的指针。标记-整理算法是在标记-清除算法的基础上,
又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
四、GC的两种判定方法:引用计数与引用链。
(1)引用计数(存在相互引用的问题):
当这个类被加载到内存之后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用时,引用计数器继续+1,而当其中一个引用销毁时,引用计数器-1,当引用计数器减为0的时候,标志着这个对象已经没有引用了,可以回收了!
(2)引用链:
从一个节点GC Root开始,寻找对应的引用节点,找到这个节点之后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被饮用到的节点,即无用的节点。
目前Java中可作为GC Root的对象有:
1.虚拟机栈中引用的对象(本地变量表);
2.本地方法栈中引用的对象(Native对象);
3.方法区中静态属性引用的对象;
4.方法区中常量引用的对象;
五、java中存在的四种引用
1、强引用:
只要引用存在,垃圾回收器永远不会回收;对象的实例没有其他对象引用,GC时才会被回收。
2、软引用:
非必须引用,内存溢出之前进行回收;
软引用主要用于用户实现类似缓存的功能,在内存不足的情况下直接通过软引用取值;
3、弱引用:
第二次垃圾回收时回收;
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,
将返回null。弱引用主要用于监控对象是否已经被标记为即将回收的垃圾,可以通过弱引用的isEnQueues方法
返回对象是否被垃圾回收器标记。在GC时一定会被GC回收。
4、虚引用:
虚引用是每次垃圾回收的时候都会被回收,用来得知对象是否被GC。
六、GC收集器有哪些?CMS收集器与G1收集器的特点。
1、串行垃圾回收器(Serial Garbage Collector)
2、并行垃圾回收器(Parallel Garbage Collector)
3、并发标记扫描垃圾回收器(CMS Garbage Collector)
特点:
CMS是一款并发、基于标记-清除算法的gc;
以获取最小停顿时间为目的(在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景);
缺点:
1、对CPU资源非常敏感:并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
2、产生大量内存碎片:CMS基于"标记-清除"算法,清除后不进行压缩操作产生大量不连续的内存碎片,这样会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
适合CMS的应用场景:
1、希望JAVA垃圾回收器回收垃圾的时间尽可能短;
2、应用运行在多CPU的机器上,有足够的CPU资源;
3、有比较多生命周期长的对象;
4、希望应用的响应时间短。
4、G1垃圾回收器(G1 GarbageCollector)
为解决CMS算法产生空间碎片和其它一系列的问题缺陷,HotSpot提供了另外一种垃圾回收策略,G1(Garbage First)算法;
特点:
1、G1在压缩空间方面有优势;
2、G1通过将内存空间分成区域(Region)的方式避免内存碎片问题;
3、Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活;
4、G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象;
5、G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做;
6、G1会在Young GC中使用、而CMS只能在O区使用
适合G1的场景:
1、服务端多核CPU、JVM内存占用较大的应用(至少大于4G);
2、应用在运行过程中会产生大量内存碎片、需要经常压缩空间;
3、想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象;
七、 Minor GC与Full GC分别在什么时候发生?
1、 Minor GC:从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC;
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。
2、Major GC :是清理永久代;
3、Full GC:清理整个堆空间—包括年轻代和永久代;
(1)、调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)、老年代空间不足
(2)、方法区(永久代)空间不足
(4)、通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
(5)、由Eden区、From Space区向To Space区复制(幸存区的复制)时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小;
八、几种常用的JVM内存调试工具?
jconsole:JDK自带的调试工具;
jvisualvm:监控内存泄露,跟踪垃圾回收、执行时内存、cpu分析、线程分析。
九、类加载的过程
1、加载:
1.通过类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构(加载的类信息)。
3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。
2、验证:
验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。验证过程分为四个阶段:
1.文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
2.元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。
3.字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
4.符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。
3、准备:
准备阶段为静态变量分配内存并设置静态变量和常量的初始化。
4、解析:
将常量池内的符号引用替换成直接引用。
主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。
5、初始化:
根据程序员通过程序制定的计划去初始化类的变量和其他资源(static{}块,构造函数,父类的初始化等);
6、使用过程就是根据程序定义的行为执行,卸载由GC完成。
十、双亲委派模型:Bootstrap ClassLoader、ExtensionClassLoader、ApplicationClassLoader;
1、类加载器按照层次,从顶层到底层,分为以下三种:
(1)启动类加载器(BootstrapClassLoader)
这个类加载器负责将存放在JAVA_HOME/lib下的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
(2)扩展类加载器(ExtensionClassLoader)
这个加载器负责加载JAVA_HOME/lib/ext目录中的类库,开发者可以直接使用扩展类加载器。
(3)应用程序类加载器(ApplicationClassLoader)
一般称为系统类加载器。它负责加载用户类路径(Classpath)上所指定的类库,可直接使用这个加载器,它是程序中默认的类加载器。
2、 类加载的双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承关系来实现,而是都使用组合关系来复用父加载器的代码;
工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,
而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,
因此所有的加载请求最终都应该传递到顶层的启动类加载器中,
只有当父类加载器反馈自己无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。 ```
好处:
Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如类Object,它放在rt.jar中,无论哪一个类加载器要加载这个类,
最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
判断两个类是否相同是通过classloader.class这种方式进行的,所以哪怕是同一个class文件如果被两个classloader加载,那么他们也是不同的类。
实现自己的加载器:
只需要继承ClassLoader,并覆盖findClass方法。
在调用loadClass方法时,会先根据双亲委派模型在父加载器中加载,如果加载失败,则会调用自己的findClass方法来完成加载。
十一、Java类加载器包括几种?它们之间的关系是怎么样的?双亲委派机制是什么意思?有什么好处?
1、三种:启动Bootstrap类加载、扩展Extension类加载、系统System类加载。
父子关系如下:
启动类加载器 ,由C++ 实现,没有父类;
扩展类加载器,由Java语言实现,父类加载器为null;
系统类加载器,由Java语言实现,父类加载器为扩展类加载器;
自定义类加载器,父类加载器肯定为AppClassLoader。
2、双亲委派机制:类加载器收到类加载请求,自己不加载,向上委托给父类加载,父类加载不了,再自己加载。
3、好处:避免Java核心API篡改;
十二、如何定义一个类加载器?你使用过哪些或者你在什么场景下需要一个自定义的类加载器吗?
1、只需要继承ClassLoader,并覆盖findClass方法。
2、自定义类加载的意义:
加载特定路径的class文件;
加载一个加密的网络class文件;
热部署加载class文件;
十三、做GC时,一个对象在内存各个Space中被移动的顺序是什么?
垃圾回收算法:标记清除法,复制算法,标记整理,分代收集算法;
1、新生代一般采用复制算法 GC,老年代使用标记整理算法。
2、垃圾收集器:串行收集器、并行收集器、G1收集器、CMS(Current Mark Sweep)收集器。
十四、jstack 是什么的? jstat 呢?如果线上程序周期性地出现卡顿,你怀疑可 能是 GC 导致的,你会怎么来排查这个问题?线程日志一般你会看其中的什么 部分?
1、jstack 用来查询 Java 进程的堆栈信息。
2、jstat 虚拟机统计信息监视工具
3、jhat 虚拟机转存储快照分析工具;先分析GC发生的场景,然后查看dump文件(通过jvisualvm )
十五、StackOverflow异常有没有遇到过,一般你猜测会在什么情况下被触发?如何指定一个线程的堆栈大小?一般你们写多少?
1、栈内存溢出,一般由栈内存的局部变量过爆了,导致内存溢出。出现在递归方法,参数个数过多,递归过深,递归没有出口。
2、-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,之前每个线程堆栈大小为256K。
根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。
但是操作系统对一 个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
十六、类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,他们的执行顺序?
先静态、先父后子。
一个类的实例化过程:
1,父类中的static代码块,当前类的static
2,顺序执行父类的普通代码块
3,父类的构造函数
4,子类普通代码块
5,子类(当前类)的构造函数,按顺序执行。
6,子类方法的执行,
十七、jvm中一次完整的GC流程(从ygc到fgc)是怎样的,重点讲讲对象如何晋升到老年代等?
对象优先在新生代区中分配,若没有足够空间,Minor GC;
大对象(需要大量连续内存空间)直接进入老年态;长期存活的对象进入老年态。如果对象在新生代出生并经过第一次MGC后仍然存活,年龄+1,若年龄超过一定限制(默认:15),则被晋升到老年态。
十八、你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms,g1?
1、串行收集器:暂停所用的应用的线程来工作,单线程;
2、并行收集器(默认的垃圾收集器):暂停所用的应用的线程来工作,多线程;
3、G1收集器:用于大堆区域;堆内存分隔,并发回收;
4、CMS收集器:多线程扫描,标记需要回收的实例,清除;
十九、深入分析了Classloader,双亲委派机制 ?
1、ClassLoader:类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。
2、双亲委派机制:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
二十、指令重排序是什么?
指令重排序:编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
二十一、volatile的语义,它修饰的变量一定线程安全吗?
1、一个变量被定义为volatile之后,具备两重语义:
①保证此变量对所有线程的可见性,即当一条线程修改了这个值,新值对于其他所有线程来说是立即得知的,普通变量需要通过主内存传递。
②禁止指令重排序优化。
2、Volatile修饰的变量不一定是线程安全的,eg非原子操作a++等
二十二、说一下强引用、软引用、弱引用、虚引用以及他们之间和gc的关系?
1、强引用:new出的对象之类的引用, 只要强引用还在,永远不会回收
2、软引用:引用但非必须的对象,内存溢出异常之前,回收
3、弱引用:非必须的对象,对象能生存到下一次垃圾收集发生之前。
4、虚引用:对生存时间无影响,在垃圾回收时得到通知。
二十三、 tomcat结构,类加载器流程 ?
Tomcat目录结构:
• /bin:存放windows或Linux平台上启动和关闭Tomcat的脚本文件
• /conf:存放Tomcat服务器的各种全局配置文件,其中最重要的是server.xml和web.xml
• /doc:存放Tomcat文档
• /server:包含三个子目录:classes、lib和webapps
• /server/lib:存放Tomcat服务器所需的各种JAR文件
• /server/webapps:存放Tomcat自带的两个WEB应用admin应用和 manager应用
• /common/lib:存放Tomcat服务器以及所有web应用都可以访问的jar文件
• /shared/lib:存放所有web应用都可以访问的jar文件(但是不能被Tomcat服务器访问)
• /logs:存放Tomcat执行时的日志文件
• /src:存放Tomcat的源代码
• /webapps:Tomcat的主要Web发布目录,默认情况下把Web应用文件放于此目录
• /work:存放JSP编译后产生的class文件
类加载器模式,双亲委派模式:
二十四、什么是JVM?
Java虚拟机包括一套字节码指令集、一组寄存器、内存空间(栈、堆、方法区、程序计数器)等;
JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
二十五、JRE/JDK/JVM是什么关系?
1、JRE
Java运行环境;,也就是Java平台。所有的Java 程序都要在JRE下才能运行。
2、JDK
程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
3、JVM
JVM是JRE的一部分。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
二十六、JVM的体系结构?
类装载器(ClassLoader)(用来装载.class文件)
执行引擎(执行字节码,或者执行本地方法)
运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)
二十七、对象“已死”的判定算法?
1、引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。
2、可达性分析算法(主流商用程序都用):通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。
Java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中Native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象。
二十七、垃圾收集算法
1、标记-清除算法
最基础的算法,分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。
特点:标记和清除过程都效率不高、会产生大量不连续的内存碎片。
2、复制算法
可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。
特点:解决了内存碎片问题,但是可以用内存就缩小为原来的一半;复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。
3、标记-整理算法
标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉端边界以外的内存。
特点:解决了内存碎片问题,又不以使用内存为代价;
4、分代收集算法
当前商业虚拟机的GC都是采用分代收集算法,这种算法并没有什么新的思想,而是根据对象存活周期的不同将堆分为:新生代和老年代,方法区称为永久代;
这样就可以根据各个年代的特点采用不同的收集算法;
新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少量存活,使用复制算法。
新生代又分为Eden区和Survivor区(Survivor from、Survivor to),大小比例默认为8:1:1。
老年代中的对象因为对象存活率高、没有额外空间进行分配担保,
就使用标记-清除或标记-整理算法。
新产生的对象优先进去Eden区,当Eden区满了之后再使用Survivor from,
当Survivor from 也满了之后就进行Minor GC(新生代GC),
将Eden和Survivor from中存活的对象copy进入Survivor to,
然后清空Eden和Survivor from,
这个时候原来的Survivor from成了新的Survivor to,
原来的Survivor to成了新的Survivor from。
复制的时候,如果Survivor to 无法容纳全部存活的对象,
则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,
如果老年代也无法容纳,则进行Full GC(老年代GC)。
大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,
令大于这个设置值的对象直接进入老年代,
目的是为了避免在Eden和Survivor区之间发生大量的内存复制。
长期存活的对象进入老年代:
JVM给每个对象定义一个对象年龄计数器,
如果对象在Eden出生并经过第一次Minor GC后仍然存活,
并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。
每熬过一次Minor GC,年龄就加1,
当他的年龄到一定程度(默认为15岁,可以通过XX:MaxTenuringThreshold来设定),
就会移入老年代。
但是JVM并不是永远要求年龄必须达到最大年龄才会晋升老年代,
如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,
年龄大于等于x的所有对象直接进入老年代,
无需等到最大年龄要求。