前言
先给大家看几道面试题?
1、请你谈谈你对JVM的理解?Java8的虚拟机有什么更新?
2、什么是OOM?什么是StackOverFlowError?有哪些方法分析?
3、JVM的常用参数调优你知道哪些?
4、内存快照抓取和MAT分析DUMP文件知道吗?
5、堆里面的分区:Eden,Survival from to,老年代,各自的特点?
6、GC的三种收集方法:标记清除,标记整理,复制算法的原理与特点,分别用在什么地方?
唠叨几句
每一个学习JVM的人,都渴望成功。每一个Java开发人员的终极目标都是在日常生活中深入理解JVM的运
行原理。JVM和平时的应用框架明显的区别,应用框架学习之后,可以直接拿来写项目了,就可以运行
起来看到helloworld。然而对于JVM,是一个特别枯燥的事情,还看不到直接的效果,必须要写笔记,
因为一扭头就会忘记。
JVM是一个令人望而却步的领域,因为它博大精深,涉及到的内容与知识点非常之多。虽然Java开发者
每天都在使用JVM,但对其有所研究并且研究深入的人却少之又少。然而,JVM的重要性却又是不言而喻
的。基于JVM的各种动态与静态语言生态圈已经异常繁荣了,对JVM的运行机制有一定的了解不但可以提
升我们的竞争力,还可以让我们在面对问题时能够沉着应对,加速问题的解决速度;同时还能够增强我
们的自信心,让我们更加游刃有余。
而且,如果我们想要进阶到技术专家或者更高等级,就必须要学习 JVM;
JVM的位置
- JVM是
运行在操作系统之上
的,它与硬件没有直接的交互; - JVM就是一个软件,同其他软件一样,需要安装并运行在操作系统上,java程序需要在JVM这个软件上运行。
JVM体系结构图
-
编译和运行过程:
-
本地方法栈
要调用本地方法接口
,本地方法接口
要和本地库
相连
- 下面区域一定不会有垃圾回收,因为栈用完一个就弹出去了。
- 所谓JVM的调优,其实就是在调堆区域(方法区是个特殊的堆),而且99%情况下都在调堆区域里的堆。
类加载器ClassLoader
我们先来看看一个类加载到 JVM 的一个基本结构
- 先理解Class类
类加载的作用:
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
ClassLoader分类
有两种类型的类加载器
- Java虚拟机自带的加载器
1、根(启动)类加载器
Bootstrap ClassLoader :
最顶层的加载类,主要加载核心类库,也就是我们环境变量下面jre/lib下的rt.jar、resources.jar、charsets.jar和class等。
该加载器无法直接获取,因为其是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
2、扩展类加载器
Extention ClassLoader :
加载jre/lib/ext目录下的jar包和class文件。
3、系统(应用)类加载器
AppClassLoader:也称为SystemClassLoader。
加载当前应用的classpath的所有类。(加载你编写的类,编译后的类) - 用户自定义的类加载器
CustomClassLoader(用户自定义类加载器):
Java.lang.ClassLoader的子类(继承),可加载指定路径的class文件
双亲委派机制
-
类加载器采用的机制是双亲委派机制
-
概念
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时(使用findClass()方法找不到要加载的类),才会尝试执行加载任务。 -
双亲委派机制的好处
采用双亲委派的一个好处
是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:
1、可以避免重复加载
,父类已经加载了,子类就不需要再次加载
2、更加安全
,保证Java核心类库所提供的类不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
类的加载机制
加载
类加载的时机
关于在什么情况下需要开始类加载
过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束
,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段
,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”
:
- 使用new实例化对象、读写类的静态字段、调用类的静态方法时。
- 使用java.lang.reflect包的方法对类型进行反射调用时。
- 当初始化类时,若发现其父类还没有进行过初始化,则先初始化这个父类。
- 虚拟机启动时,需要指定一个要执行的主类,虚拟机会先初始化这个主类。
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为
REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个
方法句柄对应的类没有进行过初始化,则需要先触发其初始化。 - 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口
的实现类发生了初始化,那该接口要在其之前被初始化。
类加载的过程
链接
验证
准备
静态常量
在准备阶段,直接初始化为最终值。
解析
初始化
- 准备阶段给类变量初始化了零值(除“静态常量”),
初始化阶段是给类变量赋值
。
static int a = 1;
static{
int b = 2;
}
static int c = 3;
1.的意思是,()的收集顺序,是按标有static的代码 的出现顺序收集的,即上述代码收集后为:
int a = 1;
int b = 2;
int c = 3;
又因为是按标有static的代码 的出现顺序收集的,所以,静态语句块中只能访问到定义在静态语句块之
前的变量,因为若访问静态语句块之后的变量,之后的变量是在静态语句块之后初始化的,不能还没初始化的东西;
Native方法
- 一个例子:
编写一个多线程类启动
public static void main(String[] args) {
new Thread(()->{
},"your thread name").start();
}
点进去看start方法的源码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0(); //调用了一个start0方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {}
}
}
//这个Thread是一个类,这个方法定义在这里是不是很诡异!看这个关键字native;
private native void start0();
- 凡是带了
native关键字
的,说明 java的作用范围达不到,去调用底层C语言的库! - 具体为:(以这里的
start0()
为例)
1、JNI:Java Native Interface (Java本地方法接口)
2、Native Method Stack 本地方法栈
3、ExecutionEngine 执行引擎
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是:
凡是带了native关键字的方法就会进入本地方法栈
登记(即,start0()
进入Ntaive Method Stack);然后在 ( ExecutionEngine ) 执行引擎执行的时候加载Native Libraies,即,本地方法栈
调用本地方法接口
,本地方法接口
要和本地库
相连。
PC寄存器(程序计数器)
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的。
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是一个非常小的内存空间,几乎可以忽略不计
栈
- 栈:后进先出 / 先进后出
- 栈存储8大基本类型、对象的引用、实例的方法等。
栈也叫栈内存
,主管Java程序的运行,是在线程创建时创建
,它的生命期是跟随线程的生命期,线程结束栈内存也就释放
。
2、对于栈来说不存在垃圾回收问题,只要线程一旦结束,该栈就Over,生命周期和线程一致,是线程
私有的。
了解三种JVM:
- Sun公司的 HotSpot
【常用】
- BEA公司的 JRockit
- IBM公司的 J9VM
方法区
-
Method Area方法区
是 Java虚拟机规范中定义的运行时数据区域之一,它与堆(heap)一样在线程之间共享
。 -
方法区实际是堆的一部分:
Java 虚拟机规范把方法区描述为堆的一个逻辑部分
,但是它却有一个别名叫做 Non-Heap(非堆),目的是与 Java 堆区分开来。 -
JDK7 之前(永久代)用于存储已被虚拟机加载的类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。每当一个类初次被加载的时候,它的元数据都会被放到永久代中。永久代大小有限制,如果加载的类太多,很可能导致永久代内存溢出,即 java.lang.OutOfMemoryError:PermGen。
-
JDK8 彻底
将永久代移除出 HotSpot JVM
,将其原有的数据迁移至 Java Heap 或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)。元空间(Metaspace)
:元空间是方法区的在 HotSpot JVM 中的实现
, -
元空间的本质和永久代类似,都是
对 JVM 规范中方法区的实现
。不过元空间与永久代之间最大的区别
在于:元空间并不在虚拟机中,而是使用本地内存
。
可以通过-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
配置内存大小。
如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。
堆(Heap)
一个JVM只存在一个堆内存,堆内存的大小是可以调节的
,类加载器读取了类文件后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息(引用则放到栈中)
,以方便执行器执行,堆内存分为三部分:
- 新生区 / 伊甸园区 Young Generation Space Young/New
- 养老区 Tenure generation space Old/Tenure
- 永久区 / 元空间 Permanent Space Perm / Metaspace(永久区 : JDK8 以前名称 :;JDK8 以后名称)
新生区(New/Young Space)、养老区【发生GC垃圾回收】
- 新生区是类诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
- 新生区又分为两部分:伊甸区(Eden Space)和幸存者区(Survivor Space),
所有的类都是在伊甸区被new出来的
(new的是运行时类,即通过类加载器加载进内存的.class,运行时类放在元空间,即方法区里,new的结果放在伊甸园区); - 幸存区有两个:0区 和 1区,当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行
轻垃圾回收
(Minor GC
)。将伊甸园中的剩余对象移动到幸存0区,若幸存0区也
满了,再对该区进行垃圾回收,然后移动到1区,如果1区也满了,再移动到0区,这里幸存0区和1区是一个互相交替的过程,直到幸存0区和1区都满了,再移动到养老区,若养老区也满了,那么这个时候将产生`重垃圾回收MajorGC(Full GC),进行养老区的内存清理,若养老区执行了Full GC后发现依然无法进行对象的保存,就会产生OOM异常 “OutOfMemoryError ”。
如果出现 java.lang.OutOfMemoryError:java heap space异常,说明Java虚拟机的堆内存不够,原因
如下:
1、Java虚拟机的堆内存设置不够,可以通过参数 -Xms(初始值大小),-Xmx(最大大小)来调整。
2、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环
永久区(Perm)【没有GC】
- 首先JDK8之前有永久区,JDK8之后无永久区,换成元空间,元空间并不在虚拟机中,而是使用本地内存。
- 永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是
Java运行环境必须的类信息
,被装载进此区域的数据是不会被垃圾回收器回收掉的
,关闭JVM才会释放此区域所占用的内存。 - 如果出现 java.lang.OutOfMemoryError:PermGen space,说明是 Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包,例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
注意:
Jdk1.6之前: 有永久代,常量池在方法区
Jdk1.7: 有永久代,但是已经逐步 “去永久代”,常量池在堆中
Jdk1.8及之后:无永久代,常量池在元空间
- 对于HotSpot虚拟机,很多开发者习惯将方法区称之为 “永久代(Parmanent Gen)”,但严格本质上说两者不同,或者说是
永久代实现方法区而已
,即永久代是方法区的一个实现(相当于方法区是一个接口interface,永久代是方法区这个接口的实现类)
,Jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。 常量池(Constant Pool)是方法区的一部分
:Class文件除了有类的版本,字段,方法,接口描述信息
外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放!
GC垃圾回收
准备
- 垃圾回收频率:
次数频繁Young区,次数较少Old区,基本不动Perm(永久区)区。
JVM 在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生区,因此GC按照回收的区域又分了两种类型,一种是普通的GC / 轻GC(minor GC)
,一种是全局GC / 重GC(major GC or Full GC)
普通GC:只针对新生代区域的GC
全局GC:针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC
GC四大算法
标记清除算法(Mark-Sweep)【养老区使用的GC算法,属于重GC】【属于垃圾收集算法】
- 说明:养老区的GC一般是由
标记清除
或者是标记清除与标记整理的混合
实现
该算法分为“标记”和“清除”
两个阶段:⾸先标记
出所有不需要回收的对象,然后
在标记完成后统⼀清除
掉所有没有被标记的对象。它是最基础的收集算法
,后续的算法都是对其不⾜进⾏改进得到。
- 原理:
当堆中(具体是堆中养老区?)
的有效内存空间被耗尽
的时候,就会停止整个程序
(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
标记
:从引用根节点开始标记所有被引用的对象,标记的过程其实就是遍历所有的GC Roots ,然后将所有GC Roots 可达的对象,标记为存活的对象。
清除
: 遍历整个堆,把未标记的对象清除。
用通俗的话解释一下
标记/清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停
,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。
- 劣势:
- 效率问题
需要两次遍历堆(标记一次,清除一次)效率比较低,而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲; - 空间问题(标记清除后会产⽣⼤量不连续的碎⽚)
这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象
都是随机的出现在内存的各个角落,现在把他们清除之后,内存的布局自然乱七八糟,而为了应付
这一点,JVM就不得不维持一个内存空间的空闲列表,这又是一种开销。而且在分配数组对象的时
候,寻找连续的内存空间会不太好找。
复制算法(Copying)【新生区使用的GC算法,属于轻GC】
-
为了解决标记清除算法的效率问题,“复制”收集算法出现了。它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收。
-
两个幸存者区动态交替,一个为from区,一个为to区,
空的幸存者区为to区
,对象的复制方向
是:在GC后,Eden和from区
中存活的对象复制到to区
,然后清除Eden和from区,即,轻GC发生在Eden和from区。
-
原理:
HotSpot JVM 把年轻代分为了三部分:一个 Eden 区 和 2 个Survivor区(from区 和 to区)。默认比例为 8:1:1。
一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次
Minor GC后,如果仍然存活,将会被移到to区,然后清理
Eden区;对象在Survivor区中
每熬过一次Minor GC ,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到养老区中。【默认是15岁,通过-XX:MaxTenuringThreshold
设定参数)】
从第二次
Minor GC开始:在GC开始的时候,对象只会在Eden区和名为 “From” 的Survivor区,Survivor区“TO” 是空的,紧接着进行GC,Eden区中所有存活的对象都会被复制到 “To” , 而在 “From” 区中
,仍存活的对象会根据他们的年龄值来决定去向
。年龄达到一定值的对象会被移动到老年代中,没有达到阈值的对象会被复制到 “To区域”,经过这次GC后,Eden区和From区已经被清空,这个时候, “From” 和 “To” 会交换他们的角色,
也就是新的 “To“ 就是GC前的”From“ , 新的 ”From“ 就是上次GC前的 ”To“。不管怎样,都会保证名为To 的Survicor区域是空的
。 Minor GC会一直重复这样的过程。直到 To 区 被填满
, ”To “ 区被填满之后,会将所有的对象移动到老年代中。
- 好处:没有内存碎片,
- 坏处:
复制算法它的缺点也是相当明显的。
1、浪费内存空间:一半to空间(空的幸存区)永远是空的,他浪费了一半的内存,这太要命了;
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。
所以从以上描述不难看出。复制算法的最佳使用场景是对象的存活率较低的时候------>即,新生区
。
注:GC在新生去发生在Eden和from区
标记整理(Mark-Compact)
-
为了解决标记清除算法的空间问题。
标记过程仍然与“标记-清除”算法的标记⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉,如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。 -
注:
标记、整理算法不仅可以弥补 标记、清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。
但,时间效率较标记清除算法更差,因为标记整理有3次遍历,除了标记清除两次遍历,在标记清除中间还有整理,花费一次遍历。
分代收集算法
小总结
- 内存效率:复制算法 > 标记清除算法 > 标记整理算法 (时间复杂度)
- 内存整齐度:复制算法 = 标记整理算法 > 标记清除算法
- 内存利用率:标记整理算法 = 标记清除算法 > 复制算法
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所
提到的三个指标,标记整理算法相对来说更平滑一些 , 但是效率上依然不尽如人意,它比复制算法多了
一个标记的阶段,又比标记清除多了一个整理内存的过程。
难道就没有一种最优算法吗?猜猜看,下面还有
答案 : 无,没有最好的算法,只有最合适的算法 。 -----------------> 分代收集算法
分代收集算法
当前虚拟机的垃圾收集都采⽤分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为⼏块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法
。
- 年轻代:(Young Gen)
年轻代特点是区域相对老年代较小,对象存活低。这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。 - 老年代:(Tenure Gen)
老年代的特点是区域较大,对象存活率高!这种情况,存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。Mark阶段的开销与存活对象的数量成正比,这点来说,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核多线程利用,对并发,并行的形式提标记效率。Sweep阶段的开销与所管理里区域的大小相关,但Sweep “就地处决” 的 特点,回收的过程没有对象的移动。使其相对其他有对象移动步骤的回收算法,仍然是是效率最好的,但是需要解决内存碎片的问题。
垃圾回收器
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
经典垃圾收集器
HotSpot虚拟机的垃圾收集器:
- 垃圾回收肯定是既要考虑新生代,又要考虑老年代;所以两个代的垃圾收集器要搭配使用,上图中的连线,就是常搭配使用的组合,其中G1单独可回收新生代和老年代;
- JDK9图标:表示JDK9中该组合不推荐使用;
性能压测
包含两部门:
- Jmeter
压测
观察接口运行情况; - 同时在压测期间,观察JVM的一些性能指标(
性能监控
),比如CPU的使用率、内存的占用率、gc次数等,分析这些指标,进行合理的优化。
压测
- 内存泄漏:
比如一个接口没写好,在调用这个接口时,在一个循环内创建大量对象,并没有复用对象。
在百万的并发量时,对象不断的创建,又因对象出生在堆内存,会导致内存撑爆,即内存泄漏。
Jmeter
性能指标
性能监控
- 在进行压测的时候,同时也应进行性能监控,来看优化方向;
- 在优化前,首先考虑应用是CPU密集型,还是IO密集型
- CPU密集型:升级服务器,增加CPU;或做分布式(一台服务器不够,多个服务器分摊处理)
- IO密集型:加固态硬盘、内存条(读写速度更快的硬件);使用缓存技术等
两个工具:
jconsole
- 启动
jvisualvm
jvisualvm是 jconsole的升级版,一般用jvisualvm;
-
启动
-
界面
- 安装插件
通过这个插件可以看到整个垃圾回收的过程。
gc界面
线程监视页面
除了关注gc,还要监视各线程运行情况、CPU占用情况等
优化
中间件优化
由上图的前三项的单个中间件的压测数据
和 后三项的中间件组合的压测数据
得出:中间件越多,性能损失越大,大多都损失在网络交互了
;
Nginx动静分离【这里优化的是首页全量数据获取】
Nginx动静分离:
动态资源和静态资源目前都处于服务端时:请求静待资源,服务端也需要给该请求分配线程,静态资源占用部门线程,会导致吞吐量降低;又因动态资源需要服务器处理,必须放在服务端,静态资源不需要处理,而是直接返回,所以将静态资源全都放在Nginx中(因为Nginx也可作静态服务器),这样服务端的线程只处理动态资源,吞吐量就上升了。
由于动态资源和静态资源目前都处于服务端,所以为了减轻服务器压力,我们将js、css、img等静态资源放置在Nginx端,以减轻服务器压力。
- 具体步骤:
-
静态文件上传到 mydata/nginx/html/static/index/css,这种格式
-
修改index.html的静态资源路径,加上static前缀
src="/static/index/img/img_09.png"
-
修改
/mydata/nginx/conf/conf.d/gulimall.conf
如果遇到有
/static
为前缀的请求,转发至html文件夹
location /static {
root /usr/share/nginx/html;
}
location / {
proxy_pass http://gulimall;
proxy_set_header Host $host;
}
JVM调优(针对首页全量数据获取)
在“Nginx动静分离”优化后,“首页全量数据获取”的吞吐量增加到11,这是再用JvisualVM查看gc过程,如下。可以看到,伊甸园区经常满,老年代也快满了,并且老年代的full gc多,消耗时间多,所以要增加堆内存大小
-
将堆内存大小从100M增加到1024M,其中新生代(-Xmn,n代表新生代)设为512M,由上图看出,新生代频繁gc,即临时对象较多,所以将新生代设大一些,不让它进行那么多gc【gc花费时间】。
-
JVM调优后
JVM调优后,吞吐量达到了14
优化业务【即项目中的,优化三级分类】
优化业务:“遍历查询数据库”到“只查询一次数据库”:将所有数据一次性都查出,并保存下来,然后后面的查询直接从中获取
- 优化前
对二级菜单的每次遍历都需要查询数据库,浪费大量资源
- 优化后
仅查询一次数据库,剩下的数据通过遍历得到并封装
面试题
HotSpot 为什么要分为新⽣代和⽼年代?
主要是为了提升 GC 效率。上⾯提到的分代收集算法已经很好的解释了这个问题,根据上⾯的对分代收集算法的介绍回答。
【根据对象存活周期的不同将内存分为⼏块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法】