一、JVM虚拟机的内部组成
假设我们创建了一个类叫Math.java,这个java文件会由javac编译成Math.class字节码文件,然后通过类加载,将类信息加载到运行时数据区中,然后再由执行引擎来执行运行时数据区中的代码。
调优主要是针对运行时数据区。
1.1 栈
JVM会为每个线程分配一个栈空间,栈空间中保存一个一个的栈帧,一个方法对应一块栈帧内存区域
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n9" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Math {
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
}</pre>
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n10" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Test {
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}</pre>
以上面的例子为例。JVM在栈中给main线程分配一块其专属的栈空间。main线程执行的main方法是一个栈帧,被压入栈空间中,然后math.compute()方法又是一个栈帧,被压入到栈空间中。
每个栈帧中保存了:
局部变量表:保存局部变量,如果是基本类型直接存值,如果是引用类型则保存其地址。除此之外还保存了当前对象本身的地址,即this
-
操作数栈
比如:int a = 1;
会先将1这个常量放入操作数栈,然后给变量a在局部变量表中分配一块内存空间,然后从操作数栈中把常量1弹出,然后把这个1放入到常量a对应的内存空间中。
int c = (a + b) * 10;
这行代码首先会先将局部变量a对应的内存空间中的值1放入操作数栈,然后再将局部变量b对应的内存空间中的值2放入操作数栈,然后从操作数栈中弹出1和2进行"+"操作,然后把计算出来的指3作为一个临时值压入操作数栈中,然后在将常量10压入操作数栈中,然后再从操作数栈中弹出3和10进行"*"操作,然后将得到的指30压入到操作数栈中,然后再将这个30从操作数栈中弹出,存入局部变量c对应的内存空间
动态链接:比如main方法调用了math.compute()方法,那么compute方法对应的地址(保存在方法区中),就保存在动态链接中
方法出口:记录这个栈帧对应的方法执行完之后,要执行的下一行代码的位置;记录栈帧对应的方法的返回值
这里的math是main线程的栈空间中的一个局部变量,因为是new实例化的,所以math保存在栈空间中的是一个指针,其指向具体math对象在堆中的地址
1.2 方法区
JDK1.6及之前使用永久代,常量池和静态变量存放在永久代中;JDK1.7开始,字符串常量池、静态变量从永久代移动到堆中;JDK1.8开始,使用元空间替代永久代。
永久代和元空间的区别:永久代是JVM虚拟机中的一块内存空间,而元空间并不在JVM虚拟机中,是物理机器本身的内存。
方法区中存放:常量、静态变量、加载的类信息、JIT代码缓存、运行时常量池。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n34" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class Math {
public static final int intData = 666; // 常量,存放在方法区
public static User user = new User(); // 静态变量,因为是一个引用类型,所以存放其指针在方法区,
// 指针指向其在堆中真正的地址
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
}</pre>
常量池:存在于class文件中,主要包含字面量和符号引用。字面量:比如类中的成员变量 int i = 5; 这里的5就是字面量;符号引用: 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。方法名,类名,字段名都是符号引用
运行时常量池:运行时常量池就是常量池被加载到内存之后的版本,它的字面量能够动态添加
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n37" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class TestConst {
public static Integer CONST_A = 1;
public Integer const_B;
public static void main(String[] args) {
TestConst t = new TestConst();
t.const_B = 127; // 动态添加字面量127,放入运行时常量池
Integer const_B_i = 127; // 动态添加字面量127,放入运行时常量池
Integer const_C = 128; // 超过127,不会放入运行时常量池
Integer const_D = 128; // 超过127,不会放入运行时常量池
Float const_C_f = 2.0f; // 浮点类型不会放入运行时常量池
Float const_D_f = 2.0f; // 浮点类型不会放入运行时常量池
System.out.println(t.const_B == const_B_i); // 结果为true
System.out.println(t.const_C == const_D); // 结果为false
System.out.println(t.const_C_f == const_D_f); // 结果为false
}
}</pre>
1.3 本地方法栈
是JVM底层使用C或C++编写的方法。
比如Thread的start方法
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n42" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">new Thread().start();</pre>
进入到start方法中可以看到Thread的start方法会调用一个由native修饰的start0方法。这个由native修饰的方法就是本地方法。本地方法栈就是给执行本地方法分配的内存空间
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n44" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">private native void start0();</pre>
1.4 堆
可达性分析算法:将 GC ROOT 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都被标记为非垃圾对象,其余未被标记的对象都是垃圾对象
GC ROOT :线程栈中的局部变量表中引用的对象、静态属性引用的对象;本地方法栈的变量;方法区中常量引用的对象
关于垃圾回收,例子:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n52" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class HeapTest {
public static void main(String[] args) throws InterruptedException {
ArrayList<HeapTest> heapTests = new ArrayList<>();
while(true) {
heapTests.add(new HeapTest());
Thread.sleep(10);
}
}
}</pre>
这里创建出来的一个个new HeapTest() 对象不会被垃圾回收,因为根据可达性分析算法,所有创建出来的new HeapTest() 对象都被heapTests对象引用,而heapTests对象是在main线程栈帧的局部变量表中引用的对象
1.5 程序计数器
每个线程都有单独的程序计数器,记录该线程将要执行的下一行代码的位置,这里的代码指的是javac编译过后的字节码文件代码
Code下的编号就是用来被保存在程序计数器中的。
因为JVM是支持多线程的,比如线程A执行完第1行代码后,线程B抢到了CPU时间碎片,等线程B释放CPU后,线程A再次获取到CPU时间碎片接着往下执行,那么线程A重新获取到CPU后怎么知道它接下来应该执行哪行代码,就需要去它专属的程序计数器中找。
二、JVM调优
2.1 GC算法
2.1.1 三大垃圾回收算法
标记清除算法:根据可达性分析算法,将不在GC Root引用链上的对象进行标记,然后清除。优点是回收效率高,不浪费内存;缺点是会产生内存碎片化。
复制算法:对内存进行分区,根据可达性分析算法,将存活的对象进行从内存中的一块区域复制到另一块区域,并排好序,然后将原来的区域中的对象整个回收掉。优点是不产生内存碎片化,效率适中;缺点是浪费内存。
标记整理算法:根据可达性分析算法,将存活的对象与要回收的对象进行重新排列,先排列存活的对象,再排列被标记的对象,然后再清除。优点是不产生内存碎片化,不浪费内存;缺点是回收效率下。
2.1.2 三色标记算法
2.1.2.1 原理
并发垃圾回收算法:三色标记算法:
首先我们从GC Roots开始枚举,它们所有的直接引用变为灰色,自己变为黑色。可以想象有一个队列用于存储灰色对象,会把这些灰色对象放到这个队列中
然后从队列中取出一个灰色对象进行分析:将这个对象所有的直接引用变为灰色,放入队列中,然后这个对象变为黑色;如果取出的这个灰色对象没有直接引用,那么直接变成黑色
继续从队列中取出一个灰色对象进行分析,分析步骤和第二步相同,一直重复直到灰色队列为空
分析完成后仍然是白色的对象就是不可达的对象,可以作为垃圾被清理
最后重置标记状态
颜色会记录在对象头的markwork中
前面的描述都比较抽象,这里以一个例子进行说明,假设现在有以下引用关系:
首先,所有GC Root的直接引用(A、B、E)变为灰色,放入队列中,GC Root变为黑色:
然后从队列中取出一个灰色对象进行分析,比如取出A对象,将它的直接引用C、D变为灰色,放入队列,A对象变为黑色:
然后从队列中取出一个灰色对象进行分析,比如取出A对象,将它的直接引用C、D变为灰色,放入队列,A对象变为黑色:
继续从队列中取出一个灰色对象E,但是E对象没有直接引用,变为黑色:
同理依次取出C、D、F对象,他们都没有直接引用,那么变成黑色(这里就不一个一个的画了):
到这里分析已经结束了,还剩一个G对象是白色,证明它是一个垃圾对象,不可访问,可以被清理掉。
2.1.2.2 存在的问题
非垃圾变成了垃圾:
比如我们回到上述流程中的这个状态:
此时E对象已经被标记为黑色,表示不是垃圾,不会被清除。此时某个用户线程将GC Root2和E对象之间的关联断开了(比如 xx.e=null;):
后面的图就不用画了,很显然,E对象变为了垃圾对象,但是由于已经被标记为黑色,就不会被当做垃圾删除,姑且也可以称之为浮动垃圾。
垃圾变成了非垃圾:
如果上面提到的浮动垃圾你觉得没啥所谓,即使本次不清理,下一次GC也会被清理,而且并发清理阶段也会产生所谓的浮动垃圾,影响不大。但是如果一个垃圾变为了非垃圾,那么后果就会比较严重。比如我们回到上述流程中的这个状态:
标记的下一步操作是从队列中取出B对象进行分析,但是这个时候GC线程的时间片用完了,操作系统调度用户线程来运行,而用户线程先执行了这个操作:A.f = F;那么引用关系变成了:
接着执行:B.f=null;那么引用关系变成了:
好了,用户线程的事儿干完了,GC线程重新开始运行,按照之前的标记流程继续走:从队列中取出B对象,发现B对象没有直接引用,那么将B对象变为黑色:
接着继续分别从队列中取出E、C、D三个灰色对象,它们都没有直接引用,那么变为黑色对象:
到现在所有灰色对象分析完毕,你肯定已经发现问题了,出现了黑色对象直接引用白色对象的情况,而且虽然F是白色对象,但是它是垃圾吗?显然不是垃圾,如果F被当做垃圾清理掉了,就会造成空指针异常。
所以在后来新版本的CMS工具中对三色标记算法进行了优化
写屏障:如果黑色对象有引用指向白色对象,则在这个引用建立好之后,黑色对象变为灰色
GC工具:
Serial:工作在年轻代,单线程。因为工作在年轻代,所以是复制算法
-
Serial Old:工作在老年代,单线程。因为工作在老年代,所以用的是标记整理算法
以上两个进行组合,只能用在小型程序中(几十M)
Parallel Scavenge:相当于多线程的Serial
-
Parallel Old:相当于多线程的Serial Old
以上两个是JDK1.8的默认组合,一般用在几个G的程序中
ParaNew:是基于Parallel Scavenge的优化,用于配合CMS
-
CMS:并发垃圾回收工具。基于三色标记算法。即业务线程和垃圾回收线程可以同时工作。因为多线程的Parallel Old并不是线程越多越好,线程多了会造成线程管理上的消耗,所以在几十G的大型程序中会考虑使用CMS
过程如下:
image初始标记:找到 GC Root 对象,这一步是STW的
并发标记:基于三色标记算法。标记的同时业务线程也在运行。CMS的优化:写屏障:如果黑色对象有引用指向白色对象,则在这个引用建立好之后,黑色对象变为灰色
重新标记:在经过了三色标记算法并发标记之后,从黑色的对象开始重新扫描一边,这一步是STW。因为只从黑色对象开始扫描,就会比从头开始STW扫描所有对象要快得多
并发清理:把所有重新标记过后的白色对象进行垃圾回收,同时业务线程也在运行
G1:是对CMS的一个优化:使用原始快照(STAB):就是当灰色对象指向白色对象的引用消失的时候,将这种情况下的白色对象的引用记录下来,直到重新标记的时候,对这些记录下来的对象进行最终判断,然后再进行清理。现在大型程序一般不用CMS了,通常使用G1
2.2 调优
2.2.1 调优步骤
调优第一步:指定堆内存大小,如果我们能确定程序大概需要的堆内存,就把最小堆内存和最大堆内存设置为一样的,减少内存抖动
调优第二步:换垃圾回收器。
查看当前Java程序使用的垃圾回收器。根据JVM内存大小更换垃圾回收器
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n156" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">java -XX:+PrintCommondLineFlags -version</pre>
2.2.2 调优工具
jps:定位到我们要找的Java程序的进程号
jinfo 进程号:打印进程号对应的Java程序信息,可以查看启动参数
jstat -gc 进程号 500:打印进程号对应的Java程序的GC信息,可以看到Full GC次数,Eden区 S0 S1区占用空间大小,最后跟着的500单位是毫秒,代表每500毫秒就打印一次
jstack 进程号:会把进程号对应的Java程序所有的线程都打印出来,包括线程名称、线程优先级,还会打印每个线程执行的方法链条(即线程堆栈),以及线程状态,是否在等待锁(WAITING),以及是垃圾回收线程(VM GC)还是业务线程
top:Linux系统级命令,类似Windows的任务管理器,可以查看到进程占用的CPU比例,占用的内存比例;top -Hp 进程号:可以查看到对应进程内部所有线程所占的CPU比例,占用的内存比例,根据这个打印结果可以查看占用CPU最高的线程,然后到jstack中去查找其对应执行的方法。如果是垃圾回收线程占用比较高,可以去看垃圾回收日志,看是否是频繁Full GC
-
jmap -histo 进程号:可以打印出进程号对应的Java程序当前哪些类都有多少个对象,占多少内存。当出现频繁的异常Full GC时(每次Full GC只回收掉一点点内存就是异常)就可以用来查看到底是哪些对象一直没有被释放。
注意:jmap命令不能在生产环境运行,因为jmap命令会让JVM暂时卡住,然后输出卡住的那个状态下的对象占用。使用场景一般是在测试环境压测后执行
jmap -dump:format=b,file=20220601.hprof 进程号:可以进行Java程序的堆转储,即可以把JVM中的堆信息以文件的形式导出,然后可以使用JDK自带的Java VisualVM工具来进行图形化界面的展示
-
在Java程序启动的时候可以设置这样一个启动参数,在OOM的时候自动存储堆转储文件
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n176" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">-XX:+HeapDumpOnOutOfMemoryError</pre>
2.2.3 调优案例
JVM调优主要就是减少Full GC的次数。因为Full GC的调用会有一个STW机制,即把正在运行的线程挂起,来执行我们的Full GC,等Full GC执行完毕后,线程才能接着工作。因为如果不STW,可能就会有一边回收,对象的引用状态一边改变的可能,会导致垃圾回收不彻底。
什么情况下会产生Full GC:
老年代空间满了
元空间满了
手动调用了System.gc()方法
实例:
对于一个每秒创建300个订单的机器,假设每秒产生的对象总量为60M(这些对象大多数都会在1秒之后变为垃圾对象,这是因为比如订单对象,是一个局部变量,等订单写入后台成功,方法执行结束后,其指针就会从栈中弹出,导致堆中的对象失去了引用,变为垃圾对象)
如果对于一个8G的内存的服务器来说。如果我们分配3个G给堆内存
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n193" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">java -Xms3G -Xmx3G -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar xxxxxxx-service.jar
-Xms堆内存最小,不指定是物理内存的64分之1
-Xmx堆内存最大,不指定是物理内存的4分之1
-Xss栈内存 -XX:MetaspaceSize元空间内存
最大堆和最小堆设置的值一样是为了防止内存抖动,
即刚开始按照最小堆来分配内存,内存不足时要进行扩容,等内存太充足又要释放内存</pre>
如果我们不指定堆内部的内存分配默认会按照老年代2:1新生代来分配内存。其中新生代的eden区和s0 s1是8:1:1。
如果假设我们机器运行14秒会把eden区占满,在进行minor gc前1秒产生的对象因为STW机制,线程被挂起,扫描对象的时候不会被认为是垃圾对象,从而这些最后一秒产生的60M的对象不会被minor gc回收,按照常理来说应该会被移动到s0区,但是有些情况下对象会直接进入到老年代:
-
大对象直接进入到老年代,这个阈值是可以设定的,默认值是0,表示任何大小的对象都会先尝试在eden区中分配
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n200" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">-XX:PretenureSizeThreshold=1024000(这个单位是字节) -XX:+UseSerialGC</pre>
-
长期存活的对象:即每经过一次minor gc,年龄增长一岁,达到阈值后进入到老年代,默认值是15
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n203" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">-XX:MaxTenuringThreshold=10</pre>
-
对象动态年龄判断:一批对象的总大小如果大于将要进入的S0区域的50%(默认50%,可以修改),那么这批对象都会直接进入老年代
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n206" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">-XX:TargetSurvivorRatio=80</pre>
上面的例子就是因为第3点,所以这一批60M的对象超过了100M的50%,直接进入了老年代,导致频繁产生Full GC。
解决方法:1. 提高对象动态年龄判断百分比;2.在内存分配的时候给新生代分配内存更大些
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="shell" cid="n209" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">java -Xms3G -Xmx3G -Xmn2G -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar xxxxxxx-service.jar
-Xmn分配堆中新生代的内存大小</pre>
三、类加载
3.1 类加载过程
loading:加载,把.class文件加载到内存
-
linking
2.1 verification:校验,验证.class文件是否符合规范
2.2 preparation:准备,静态成员变量赋默认值
2.3 resolution:解析,将类、方法、属性等符号引用解析为直接引用,常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
initializing:初始化,给静态成员变量赋初值
3.2 类加载器(ClassLoader)
一个class文件被加载到内存中分为两部分:一部分是class文件的二进制内容;还有一部分是生成了一个Class对象(在方法区中),指向了class文件的二进制内容。
比如 String.class 获取的就是String这个class文件的Class对象,小class可以理解为Class的实例,且是单例模式,因为同一个类只会有一个小class。所以 String.class 也可以写成String.getClass()
在反射中,我们都是通过这个class对象去找它对应的class文件的二进制内容。比如 String.class.getMethods() 通过反射去获取String这个类的所有方法,就会通过String的class对象去找它对应的String.class文件的二进制内容,来解析成Java指令运行,或者说String.class文件的二进制内容被解析成Java指令,存放到class对象中,通过class对象的方法来进行调用。
3.2.1 类加载器的层次(双亲委派模型)
Bootstrap:最顶层的核心类库中的类是由Bootstrap类加载器加载的,比如String类。是C++实现的一个类加载器
Extension:用来加载扩展jar包 jre/lib/ext/*.jar下的所有类
App:我们自己写的类,是由App类加载器加载
自定义的ClassLoader
一个类被类加载器加载的过程(双亲委派机制)是这样:
比如我们要通过自定义ClassLoader来加载一个类,如果从来没有被加载过,并不会直接由自定义ClassLoader进行加载,自定义ClassLoader会先去它已加载过的缓存中找看是否这个类已经被加载,如果已经被加载就直接返回结果,否则就去询问他的父类加载器App加载器,看App加载器中是否已经加载了这个类,如果App加载器中没有,就再去App的父类加载器Extension中询问,如果没有就去最顶层的Bootstrap加载器中询问,如果Bootstrap中还是没有加载这个类进来,Bootstrap又会向他的子类加载器Extension询问,如果Extension没有,就再去App询问,如果还是没有,最终才会由自定义ClassLoader进行类加载,如果最终都没有加载成功,就会报ClassNotFoundException异常。
A:为什么要使用双亲委派机制进行类加载?
Q:主要是为了安全。比如我们想使用一个自定义类加载器加载 java.lang.String,如果没有双亲委派机制,就会把我们自己写的一个 java.lang.String 覆盖掉JDK原来的 java.lang.String。次要问题是资源浪费的问题,如果一个类已经加载过直接返回结果就行,不需要重复加载。
注意:父加载器不是“类加载器的加载器”!也不是“类加载器的父类加载器”。父加载器只是这个当前这个加载器里的一个parent变量定义的加载器。
实例:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n248" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class ParentClassLoaderTest {
public static void main(String[] args) {
// 返回类型为AppClassLoader的一个对象
System.out.println(ParentClassLoaderTest.class.getClassLoader());
/*
返回null,因为这里的getClass得到的是AppClassLoader这个类的class对象,
这个类是由最顶层的Bootstrap加载器加载的,所以getClassLoader()理论上应该返回Bootstrap的一个对象,
但实际上Bootstrap的实现是由C++实现的,所以并没有一个Java类型的对象与之对应,所以在Java程序中get不到
*/
System.out.println(ParentClassLoaderTest.class.getClassLoader().getClass().getClassLoader());
/*
返回类型为ExtensionClassLoader的一个对象,因为是AppClassLoader的父加载器
/
System.out.println(ParentClassLoaderTest.class.getClassLoader().getParent());
/
返回null,原因是ExtensionClassLoader的父加载器是Bootstrap
*/
System.out.println(ParentClassLoaderTest.class.getClassLoader().getParent().getParent());
}
}</pre>
3.2.2 自定义类加载器
自定义类加载器需要继承ClassLoader类,并重写其findClass方法
实例:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n253" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 这里的name就是class的全类名
// 指定去哪个文件加载class文件,这个class文件就不一定是我们项目路径下的了
File f = new File("c:/test/" + name.replaceAll(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b = fis.read()) != 0) {
baos.write(b);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
// defineClass方法是把全类名跟加载进来的二进制class文件进行绑定,然后返回一个Class对象
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> clazz = myClassLoader.loadClass("com.atguigu.gulimall.classLoader.Hello");
System.out.println(clazz.getName());
}
}
</pre>
loadClass方法源码:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n255" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
/*
可以看到loadClass方法如果父加载器都没找到的话就会调用自己的findClass方法,
所以我们自定义的ClassLoader只要重写findClass方法就好
*/
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}</pre>
比如我们想加载一个不在我们项目目录下的class文件,就可以使用自定义的ClassLoader来写。首先创建一个类,继承ClassLoader,然后重写findClass方法,传入的参数是全类名,然后实例化一个自定义的类加载器对象,然后调用它的loadClass方法,loadClass方法的过程就是双亲委派的过程
四、对象
4.1 对象在内存中的布局
4.1.1 普通对象
分为四部分:
-
markword:8个字节。
1.1 记录了hashCode,即调用过一次对象的hashCode方法后,会将hashCode(32bit)保存在markword中,下次再调用hashCode方法就直接从这里拿,不再计算一次
1.2 记录了锁信息,即如果把这个对象当成资源进行加锁,会记录这个对象被加了锁
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n267" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit;">System.out.println(ClassLayout.parseInstance(t).toPrintable()); // 打印对象内存信息
synchronized(t) {
// ...
System.out.println(ClassLayout.parseInstance(t).toPrintable()); // 打印对象内存信息
}
System.out.println(ClassLayout.parseInstance(t).toPrintable()); // 打印对象内存信息/*
会发现,加了锁之后的markword和加锁前,锁释放后的markword不一样
*/</pre>1.3 记录了垃圾回收信息,即这个对象处于垃圾回收的哪个阶段(新生代Eden、S1、S2;老年代),还有垃圾回收时的年龄
-
class pointer:4个字节,保存一个指针,指向new的这个对象的class文件
(1和2算对象头)
实例数据:比如int a int b,这些成员变量。这里注意,如果成员变量不是基本类型,比如String,在当前对象里就只会保留指针,即4个字节
对齐:因为现在的机器大多是64位的,64位占8个字节,所以如果当前new出来的对象只有60个字节,需要补齐到8的整数倍,即64个字节,才是这个对象所占的内存大小
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n276" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class T {
int a;
int b;
boolean flag;
String str = "hello";
}
/*
这个对象所占内存大小分析:
1. makrword + class point = 12 字节
2. 成员变量: 两个int 8 字节 + 一个boolean 4 字节
(因为布尔类型,short类型,byte类型虽然都不足4个字节,但是有一个内部对齐的机制
所以需要补齐到4个字节)+ 一个引用类型的String 4 个字节
(只保存指针,真正的hello字符串在字符串常量池中)
= 16个字节
3. 12 + 16 = 28个字节,不能被8整除,需要再补4个字节补齐到32个字节
所以这个T对象占32个字节
*/</pre>
指针大小:即使是64位机器也默认是32bit的指针,即4个字节, 这是因为Java虚拟机启动时默认使用了压缩指针,把指针压缩为32bit
4.1.2 数组对象
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n280" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">int[] a = new int[4];
T[] t = new T[5];</pre>
数组对象和普通对象所占内存的四个部分是一样的,区别在于数组对象多了4个字节来记录数组长度
4.2 对象怎么分配
对象分配步骤:
刚创建的对象会先尝试看是否能在栈上分配。在栈上分配的好处是每个方法在栈中有一块自己管理的空间,当方法运行结束,这个方法所管理的空间就从栈里被弹出,那么这个空间里所有的对象都会被清理掉,也就不存在垃圾回收了。在栈上分配的条件是根据逃逸分析来的,即在方法中创建的这个对象只用于这个方法内部,如果脱离了这个方法所管理的空间,在这个方法所管理的空间中就只能保存这个对象的指针,真实的对象会放到堆内存中
对于不能在栈上分配的对象。先判断对象的大小,如果超过了新生代内存阈值,会直接把对象放入到老年代中
如果对象大小没超过阈值,会根据JVM的TLAB分配方法,将对象分配到新生代的Eden区中。TLAB是指,为了防止线程冲突(比如有两个内存都想抢占同一个地址的内存,这时候就会加锁,看谁先抢到锁,谁来占用地址,这会导致效率低下),JVM会为每一个线程分配一块内存区域,这个区域是线程私有的,所以线程在分配对象内存的时候优先在自己私有的内存区域中分配,这样就不会和其他内存产生冲突,提高运行效率,当这块区域满了之后,才会到公共区域中进行抢锁分配内存。
进入到Eden区域后就进行垃圾回收,如果第一次回收还存活的就年龄+1被分配到S1区,第二次回收还存活就年龄+1被分配到S2区,第三次回收还存活就年龄+1被分配到S1区,直到年龄到达阈值后被分配到老年代区,然后直到Full GC才被清除(当老年代空间不足就会执行Full GC)