一.Java 整体的运行结构以及与 JVM 的关系
1.类加载器在 JDK 1.8 以前和 JDK 1.9 以后
不管版本如何变化,双亲加载依然是使用的主体,不可能改变。
class="java">package com.bijian.study; public class TestClassLoaderDemo { public static void main(String[] args) { String str = ""; System.out.println(str.getClass().getClassLoader()); // Bootstrap加载器 } }
运行结果:null
package com.bijian.study; 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()); } }
JDK1.8及以下版本运行结果:
sun.misc.Launcher$AppClassLoader@73d16e93 sun.misc.Launcher$ExtClassLoader@15db9742 null
? JDK1.9及以上版本运行结果:
jdk.internal.loader.ClassLoaders$AppClassLoader@4b85612c jdk.internal.loader.ClassLoaders$PlatformClassLoader@66133adc(改变:ExtClassLoader) null
? 其中,null就是bootstrap 加载器,可以看到JDK1.9的类加载器已经发生了改变。
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 VM 18.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"
?
特别说明:此文章是《开课吧》公开课的笔记