JVM概念
JVM是java的核心和基础在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。
JVM的生命周期:
JVM的诞生:当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
JVM实例的运行:main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。
JVM实例的消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出
JVM运行内存模型
概念:JVM把文件.class字节码加载到内存,对数据进行校验,转换和解析,并初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制
以下是JVM的概念图
类装载器
类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例
在虚拟机提供了4种类加载器
启动类加载器(Bootstrap ClassLoader)-----c++编写,加载java核心库 java.*
■这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。
■由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
扩展类加载器(Extension ClassLoader)-----java编写,加载扩展库
■这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接用。
应用程序类加载器(Application ClassLoader)---------java编写,加载程序所在的目录
■这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。
自定义类加载器(User ClassLoader)
■用户自己定义的类加载器。
⚪原理
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:
↓加载(Loading)
↓验证(Verification)
↓准备(Preparation)
↓解析(Resolution)
↓初始化(Initialization)
↓使用(Using)
↓卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。
⚪加载:(重点)
加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:
1.通过“类全名”来获取定义此类的二进制字节流
2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构----模板
3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口
解析:相对于类加载过程的其他阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。然后在java堆中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。
⚪验证:
验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
⚪准备:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:
public static int value = 12;
那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。
上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建设上面类变量value定义为:
public static final int value = 123;
编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。
⚪解析:
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。
1.类、接口的解析
2.字段解析
3.类方法解析
4.接口方法解析
⚪初始化:(了解)
类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
类装载器的内部机制----------------双亲委派机制(重点)
原理:当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
个人理解:
即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这就是传说中的双亲委派模式
双亲委派机制的作用
1.保证安全性
防止重复加载同一个class。通过委托去向上面问,加载过了就不用再加载一遍。保证数据安全。
2.保证唯一性
保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
个人解释:
试想,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证,因为Object都各不相同那么程序运行启动就会出错,也保证了JVM能够正常的安全运行。
运行时数据区
方法区——线程共享
堆——线程共享
Java栈(虚拟机栈)——非线程共享
程序计数器——非线程共享
本地方法栈——非线程共享
(1)JVM运行时会分配好方法区和堆,而JVM每遇到一个线程,就为其分配一个程序计数器、Java栈、本地方法栈,当线程终止时,三者(程序计数器、Java栈、本地方法栈)所占用的内存空间也会释放掉。
(2)程序计数器、Java栈、本地方法栈的生命周期与所属线程相同,而方法区和堆的生命周期与JAVA程序运行生命周期相同,所以gc只发生在线程共享的区域(大部分发生在Heap上)
⚪堆:(吃多了拉)——线程共享
♦Java中的堆是用来存储对象实例以及数组(当然,数组引用是存放在Java栈中的)。堆是被所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。在JVM中只有一个堆。堆是Java垃圾收集器管理的主要区域,Java的垃圾回收机制会自动进行处理。
♦堆空间分为老年代和年轻代。刚创建的对象存放在年轻代,而老年代中存放生命周期长久的实例对象。
♦年轻代中又被分为Eden区和两个Survivor区。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次GC仍然存活的,就会被转移到老年代。 当一个对象大于eden区而小于old区(老年代)的时候会直接扔到old区。 而当对象大于old区时,会直接抛出OutOfMemoryError(OOM)
⚪元空间=永久代----方法区:----存储在物理硬盘(非堆)——线程共享
♦有时候也称为永久代(Permanent Generation),在方法区中,存储了每个类的信息(包括类的名称、修饰符、方法信息、字段信息)、类中静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息以及编译器编译后的代码等
♦当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,在这里进行的GC主要是方法区里的常量池和类型的卸载。当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。—OOM
♦在方法区中有一个非常重要的部分就是运行时常量池,用于存放静态编译产生的字面量和符号引用。运行时生成的常量也会存在这个常量池中,比如String的intern方法。它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。
为什么元空间又称为非堆: 因为在内存模型中他存储的数据和堆有些许不同,所以习惯性把他从堆中单独区分出来。 方法区属于非堆内存。它存储每个类结构,如运行时常数池、字段和方法数据,以及方法和构造方法的代码。它是在 Java 虚拟机启动时创建的。
⚪Java栈:(喝多了吐)——非线程共享
♦Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈。JVM栈是线程私有的,每个线程创建的同时都会创建自己的JVM栈,互不干扰。 Java栈是Java方法执行的内存模型。
♦Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。
♦当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。 当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。
♦局部变量表: 用来存储方法中的局部变量,对于引用类型的变量,则存的是指向对象的引用,对于基本数据类型的变量,则直接存储它的值
♦操作数栈: 栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
♦指向运行时常量池的引用: 因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
♦方法返回地址: 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址
⚪程序计数器:——非线程共享
♦程序计数器(Program Counter Register),也有称作为PC寄存器。
由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
♦在JVM规范中规定,如果线程执行的是非native(本地)方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
♦由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
⚪本地方法栈:——非线程共享
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。
本地方法栈与Java栈的作用和原理非常相似。**区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。**在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
创建对象内存分析
个人理解:
首先会在方法区加载Class模板,每个模板都有该类的属性以及方法和常量池,以及静态变量随着类加载而加载到静态方法区,并在java堆中生成一个代表这个类的Class对象的内存区域,为类变量分配内存并设置类变量初始值,例如String类型为null, 而后在栈中存储该对象的引用地址,地址指向堆中类的内存地址,最后初始化,例如构造方法,赋值给堆中的对象内存数据中。每一个线程都会给他分配指定的 java栈,程序计数器,本地方法栈,所以说这三个是线程独享,是线程安全,而堆和方法区大家一起使用的,因而是线程不安全
♦ JVM垃圾回收机制------GC
GC的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
个人理解:
JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。
1.程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因为方法结束或者线程结束时,内存自然就跟随着回收了。
2.而Java堆区和方法区则不一样,这部分内存的分配和回收是动态,正是垃圾收集器所关注的部分。
以下介绍GC回收机制的几种方式!
♦ 引用计数法------过时
概念: 引用计数是垃圾收集器中的早期策略。堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1,引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
缺点: 这样很浪费资源,要对所有对象引用计数,并且计数器本身也浪费资源
♦ 标记-清除(Mark-Sweep)算法
标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。(需要两次扫描)
主要缺点:
1.一个是效率问题,标记和清除过程的效率都不高。
2.另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致:当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。
♦ *标记-整理(Mark-Compact)算法====标记-清除-整理
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。(需要三次扫描)
优点:减少了内存碎片
缺点:更加降低了效率
♦ 复制算法
幸存区分为了两块,一个是form,一个是to-------谁空谁to
理解: 当新的对象存在Eden区时,进行GC时会将存活下来的复制幸存区的to区,并把原本为from区的也复制to区,这样原本为from区变成to区,to变成了from区,然后Eden区和to区每次清除都必定为空的,减少了内存碎片,适用于对象存活时间较短的项目中,而后在幸存from区超过15次都没有清除的就复制到老年区
总结:
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
♦分代收集(Generational Collection)算法------堆分区使用不同算法(重点)
⚪概念:
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和年轻代,在堆区之外还有一个代就是永久代,它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
⚪使用:
老年代的特点是每次垃圾收集时只有少量对象需要被回收,-------- (生命周期长)标记-清除-整理
而年轻代的特点是每次垃圾回收时都有大量的对象需要被回收,--------(生命周期短) 复制算法
*那么就可以根据不同代的特点采取最适合的收集算法。
老年代(Old Generation)的回收算法:标记-整理和标记清除一起使用
老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理和标记清除一起使用。
a)在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
b)内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC或Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
原因:因为老年代的对象通常生命周期大,只回收少量对象,使用标记清除算法被标记的数量较少,产生的内存碎片不会很多,可以多次标记清除后再进行标记整理内存碎片。
■年轻代中jvm使用的是:Mark-copy(标记-复制)算法
a)所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
b)年轻代分三个区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当另外一个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。
c)当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
d)新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
原因:因为年轻代的对象通常生命周期小,回收大量对象,使用复制算法效率高,并且不会有大量内存碎片,将对象直接批量删除,存活下来的全部复制到幸存区,很适合用复制算法
■永久代(Permanent Generation)的回收算法:
永久代(permanent generation) 也称为“方法区(method area)”,他存储class对象和字符串常量。所以这块内存区域绝对不是永久的存放从老年代存活下来的对象的。在这块内存中有可能发生垃圾回收。发生在这里垃圾回收也被称为major GC。
♦Minor GC、Full GC触发条件
Minor GC触发条件
Eden区域满了,或者新创建的对象大小 > Eden所剩空间
CMS设置了CMSScavengeBeforeRemark参数,这样在CMS的Remark之前会先做一次Minor GC来清理新生代,加速之后的Remark的速度。这样整体的stop-the-world时间反而短
Full GC的时候会先触发Minor GC
Full GC触发条件
Minor GC后存活的对象晋升到老年代时由于悲观策略的原因,有两种情况会触发Full GC,
一种是之前每次晋升的对象的平均大小 > 老年代剩余空间;
一种是Minor GC后存活的对象超过了老年代剩余空间。
这两种情况都是因为老年代会为新生代对象的晋升提供担保,而每次晋升的对象的大小是无法预测的,所以只能基于统计,一个是基于历史平均水平,一个是基于下一次可能要晋升的最大水平。
这两种情况都是属于promotion failure
CMS失败,发生concurrent mode failure会引起Full GC,这种情况下会使用Serial Old收集器,是单线程的,对GC的影响很大。concurrent mode failure产生的原因是老年代剩余的空间不够,导致了和gc线程并发执行的用户线程创建的大对象(由PretenureSizeThreshold控制新生代直接晋升老年代的对象size阀值)不能进入到老年代,只要stop the world来暂停用户线程,执行GC清理。可以通过设置CMSInitiatingOccupancyFraction预留合适的CMS执行时剩余的空间
新生代直接晋升到老年代的大对象超过了老年代的剩余空间,引发Full GC。注意于promotion failure的区别,promotion failure指的是Minor GC后发生的担保失败
Perm永久代空间不足会触发Full GC, 可以让CMS清理永久代的空间。设置CMSClassUnloadingEnabled即可
System.gc()引起的Full GC, 可以设置DisableExplicitGC来禁止调用System.gc引发Full GC
♦OOM - Out of Memory,内存溢出
软件所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。此时软件或游戏就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件
♦JVM调优参数简介
-XX 参数被称为不稳定参数,之所以这么叫是因为此类参数的设置很容易引起JVM 性能上的差异,使JVM 存在极大的不稳定性。如果此类参数设置合理将大大提高JVM 的性能及稳定性。
不稳定参数语法规则:
1.布尔类型参数值
-XX:+<option> '+'表示启用该选项
-XX:-<option> '-'表示关闭该选项
2.数字类型参数值:
-XX:<option>=<number> 给选项设置一个数字类型值,可跟随单位,例如:'m'或'M'表示兆字节;'k'或'K'千字节;'g'或'G'千兆字节。32K与32768是相同大小的。
3.字符串类型参数值:
-XX:<option>=<string> 给选项设置一个字符串类型值,通常用于指定一个文件、路径或一系列命令列表。
例如:-XX:HeapDumpPath=./dump.core
1
2
3
4
5
6
7
8
9
♦JVM参数示例
配置: -Xmx4g –Xms4g –Xmn1200m –Xss512k -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:PermSize=100m
-XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15
解析:
-Xmx4g:堆内存最大值为4GB。
-Xms4g:初始化堆内存大小为4GB 。
-Xmn1200m:设置年轻代大小为1200MB。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss512k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
-XX:PermSize=100m:初始化永久代大小为100MB。
-XX:MaxPermSize=256m:设置持久代大小为256MB。
-XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。