一、Java整体的运行结构以及与JVM的关系。
1、类加载器在JDK 1.8以前和JDK 1.9以后。不管版本如何变化,双亲加载依然是使用的主体,不可能改变。
package cn.mldn.jvm;
public class TestClassLoaderDemo {
public static void main(String[] args) {
String str = "" ;
System.out.println(str.getClass().getClassLoader()); // Bootstrap加载器
}
}
新版本之后的类加载器已经发生了改变:
jdk.internal.loader.ClassLoaders$AppClassLoader@4b85612c
jdk.internal.loader.ClassLoaders$PlatformClassLoader@66133adc(改变:ExtClassLoader)
bootstrap加载器
package cn.mldn.jvm;
class Member {}
public class TestClassLoaderDemo {
public static void main(String[] args) {
Member member = new Member() ;
System.out.println(member.getClass().getClassLoader()); // Bootstrap加载器
System.out.println(member.getClass().getClassLoader().getParent()); // Bootstrap加载器
System.out.println(member.getClass().getClassLoader().getParent().getParent());
}
}
2、运行时数据区是整个JVM设计的关键所在,那么在整个运行时数据区里面,就有若干个组成部分:
- 栈内存:是程序的运行单位,里面存储的信息都使与当前线程有关的内容,包括:局部变量、程序的运行状态、方法返回值。
- 堆内存:Java的引用传递的实现依靠的就是堆内存,同一块堆内存空间可以被不同的栈内存所指向。
- 程序计数器:是一个非常小的内存空间,这个空间主要是进行一个计数的操作,对象的晋升问题(依靠的就是计数器)。
- 方法栈内存:在进行递归调用的时候所保存的栈帧的内容;
|-组成部分:局部变量表、操作数栈、当前方法所属于类的运行时产量的引用、返回地址。
在整个JVM运行时数据区之中,关键的部分在于需要进行堆的优化,既然要进行优化,那么就必须清除Java的对象访问模式。Java在进行对象引用的时候并没有使用到句柄的概念(步骤多一些,导致性能下降),它直接采用的HotSpot虚拟机标准的指针引用。
Java是一个开源的编程语言,实际上在世界的技术公司里面有三个所谓的虚拟机标准:
- SUN(被Oracle收购了):所推出的JVM是基于HotSpot标准的虚拟机;
- BEA(被Oracle收购了):JRockit;
- IBM(曾经打算收购SUN公司):JVM's、J9。
Oracle不可能花费额外费用去维护两个虚拟机标准,所以未来的发展趋势:HotSpot + JRockit,而现在所使用的JVM实际上也全部都是HotSpot标准,执行:java -version
java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM18.9 (build 11+28, mixed mode)
一般都不需要去改变所谓的JVM运行模式,但是有一点需要清楚,当前的Java行业里面,Java已经不再适合于进行桌面程序开发了,也就是说客户端程序不是Java的重点了,那么这样一来对于资源的启动分配就非常重要了。
默认的JDK的配置使用的全部是服务器的运行模式:
/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home/lib/jvm.cfg
-server KNOWN
-client IGNORE
二、堆内存组织结构以及与内存有关的调整参数。
JVM的组成只是做为一个概念存在,如果每一天都只是进行JVM结构研究对开发的作用很小,最关键的问题就是优化:堆内存空间。
package cn.mldn.jvm;
public class TestGCDemo {
public static void main(String[] args) {
String str = "" ;
for (int x = 0 ; x < Integer.MAX_VALUE ; x ++) {
str += x + str ; // 多么万恶
str.intern() ; // 万恶加三级
}
}
}
堆内存之中需要考虑关于GC的问题,真正导致程序变慢的原因就在于堆内存的控制策略上。控制回收策略(JDK 1.9之后的默认策略已经非常好了,因为其已经更换为了G1)
1、年轻代
- 伊甸园区:新生的小对象。每当使用关键字new的时候默认的时候都会在此空间内进行对象创建。
- 如果创建的对象过多,那么最终的结果也有可能造成伊甸园区的内存空间沾满,所以此时就会发生晋级的操作(若干次MinorGC执行还保留的对象,晋升到存活区)。
- 存活区:进行一些GC后保存的对象(程序计数器,会记录GC的执行次数),存活区准备两块空间:S0、S1,有一块空间永远都是空的,是向老年代晋升。
2、老年代
哪些又臭又硬的对象,这些对象都已经经历了无数次的GC之后依然被保留下来的对象。于是这些对象很难被清除。但是有可能也会被清除。同时如果是一个很大的对象,那么默认的也会直接保存到老年代,如果现在老年代空间不足了,会出现MajorGC(FullGC),进行老年代的清理(这样的清理是非常耗费性能的),所以这也是为什么不去使用System.gc()方法。
3、于是现在最核心的问题在于:如何可以进行堆的结构优化。
- 每一块的空间实际上都会提供有一个伸缩区;
- 伸缩区的考虑是在某个内存空间不足时:会自动打开伸缩区继续扩大可用的内存,当发现当前的区域的空间内存可以满足要求的时候,就可以进行收缩。
- 如果不进行收缩的优点:可以提升堆内存分配效率;
- 如果不进行收缩的缺点:空间太大了,那么如果没有选择好合适的GC算法,就会造成堆内存的性能下降。
package cn.mldn.jvm;
public class ShowSpaceDemo {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime() ; // 获取Runtime实例化对象
System.out.println("MAX_MEMBER:" + runtime.maxMemory()); // 最大可用
System.out.println("TOTAL_MEMBER:" + runtime.totalMemory()); // 默认的可用内存
}
}
maxMemory:默认大小为当前物理内存的“1 / 4”、4294967296
totalMemory:默认大小为当前物理内存的“1 / 64”、268435456
伸缩区有这么大的处理范围,所以在进行堆内存分配的过程里面当用户访问量增加的时候就一定会导致不断的判断空间是否充足,不断的进行内存空间的增长,不断的进行内存空间的收缩于释放。
至关重要的两个参数:可以使用的单位(k、m、g)
-Xms:设置初始化的内存分配大小;
-Xmx:设置最大的可用内存空间。
-Xms16g -Xmx16g
可以减少堆内存的收缩处理操作。
当堆内存空间很大得情况下就需要考虑到GC的执行效率问题。
年轻代:
所以在这个环节里面就需要考虑两个技术名词:BTP、TLAB
- BTP:在伊甸园区采用栈的形式将最晚创建的对象保存在栈顶。
- TLAB:分块保存,适合于多线程的处理操作上。
-Xmn:设置年轻代的空间大小,默认采用的时物理内存的“1 / 64”
-Xss:设置每一个线程所占用的栈的线程
-X:SurvivorRatio:设置伊甸园区与两个存活区之间的内存分配比,默认“8 : 1 : 1”。
老年代:
与年轻代比率:-XX:NewRatio
当对象很大的时候往往不在年轻代进行保存,而是直接晋级到老年代,利用“-XX:PretenureSizeThreshold”。
【分水岭】JDK1.8之后取消了所谓的永久代,而变为了元空间(不在堆内存里面保存,而是直接利用物理内存保存。)
三、GC算法(主流:G1、未来:ZGC)
GC算法的选择直接决定了你最终程序的执行性能。
传统意义上进行的回收处理操作,只是认为简单的有垃圾产生了,而后自动进行GC操作(MinorGC、MajorGC),或者手工利用“System.gc()”操作(MajorGC、FullGC)。
Java的GC机制是经历快了20年的发展,对于电脑硬件技术也已经产生了很大得变化,最初的时候是在一块CPU上进行多线程的分配,而现在手机都多核CPU,多线程支持了。
对于GC算法里面就需要考虑不同的内存分代(新的JDK开发版本之中,以及现在项目里面不建议再使用如下的GC算法):
- 年轻代GC策略:串行GC、并行GC;
- 老年代GC策略:串行GC、并行GC。
【年轻代串行GC】
- 扫描年轻代中得所有存活对象;
- 使用MinorGC进行垃圾回收,同时将还能够存活下来的对象保存在存活区(S0、S1)里面;
- 每一次进行MinorGC的时候都会引起S0和S1的交换;
- 经过若干次MinorGC还能够继续保存下来的就进入到老年代。
【年轻代并行GC】
- 算法:复制-清理算法,在扫描和复制的时候均采用多线程的处理模式来完成。
在年轻代进行MinorGC的时候实际上也由可能触发到老年代GC操作。
【老年代串行GC】
算法:标记-清除-压缩;
扫描老年代中的存活对象,并且进行对象的标记;
遍历整个老年代的内存空间,回收所有标记对象;
为了保证可以方便的计算出老年代的大小,还需要进行压缩(碎片整理,把空间集中在一起。)
【老年代并行GC】
- 在最早的时候主要使用了此种GC算法,但是这种算法会有一个严重性的问题:STW(产生中断,因为需要进行垃圾的标记)。
- 暂停当前的所有执行程序(挂起);
- 标记出垃圾,标记的时间越长,那么挂起的时间就越长,如果此时你的堆内存空间很大,那么时间一定会更长;
- 预清除处理;
- 重新标记过程:看看还有没有垃圾;
- 进行垃圾的处理;
- 程序恢复执行。
以前使用的:-Xms48m -Xmx48m -XX:+PrintGCDetails
替换后使用:-Xms48m -Xmx48m -Xlog:gc*
【砍掉】串行GC:-Xms48m -Xmx48m -Xlog:gc* -XX:+UseSerialGC
【砍掉】并行GC:-Xms48m -Xmx48m -Xlog:gc* -XX:+UseParallelGC
【砍掉】并行年轻代GC:-Xms48m -Xmx48m -Xlog:gc* -XX:+UseParallelNewGC
【砍掉】并行老年代GC:-Xms48m -Xmx48m -Xlog:gc* -XX:+UseParallelOldGC
最终的GC发展到了今天,已经不单单再以上的古老算法了,不管是并行还是串行算法,实际上都有可能引起大范围的程序暂停问题(程序的性能不高),现在最关键的问题就需要去解决大空间下的性能问题。
最初的电脑是没有这么高的硬件配置的,内存最早出现的时候售卖的单位是K,这样的背景下就产生了G1回收算法(现在JDK 1.9之后的标配算法),支持的最大内存为64G(每一个小得区域里面可以设置的范围“1 -32”)
G1收集:-Xms48m -Xmx48m -Xlog:gc* -XX:+UseG1GC
JVM核心优化问题:
· 减少伸缩区的使用;
· 提升GC的效率,G1是现在最好用的GC算法。
· 线程的栈的大小配置;
· 线程池的配置。
如果现在是在Tomcat下那么如何优化呢?
JAVA_OPTS="-Xms4096m -Xmx4096m -Xss1024K"