深入Java虚拟机

2018-11-01  本文已影响19人  阿闯学长

一、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设计的关键所在,那么在整个运行时数据区里面,就有若干个组成部分:
  1. 栈内存:是程序的运行单位,里面存储的信息都使与当前线程有关的内容,包括:局部变量、程序的运行状态、方法返回值。
  2. 堆内存:Java的引用传递的实现依靠的就是堆内存,同一块堆内存空间可以被不同的栈内存所指向。
  3. 程序计数器:是一个非常小的内存空间,这个空间主要是进行一个计数的操作,对象的晋升问题(依靠的就是计数器)。
  4. 方法栈内存:在进行递归调用的时候所保存的栈帧的内容;
    |-组成部分:局部变量表、操作数栈、当前方法所属于类的运行时产量的引用、返回地址。

在整个JVM运行时数据区之中,关键的部分在于需要进行堆的优化,既然要进行优化,那么就必须清除Java的对象访问模式。Java在进行对象引用的时候并没有使用到句柄的概念(步骤多一些,导致性能下降),它直接采用的HotSpot虚拟机标准的指针引用。

Java是一个开源的编程语言,实际上在世界的技术公司里面有三个所谓的虚拟机标准:

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、年轻代
2、老年代

哪些又臭又硬的对象,这些对象都已经经历了无数次的GC之后依然被保留下来的对象。于是这些对象很难被清除。但是有可能也会被清除。同时如果是一个很大的对象,那么默认的也会直接保存到老年代,如果现在老年代空间不足了,会出现MajorGC(FullGC),进行老年代的清理(这样的清理是非常耗费性能的),所以这也是为什么不去使用System.gc()方法。

3、于是现在最核心的问题在于:如何可以进行堆的结构优化。
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

老年代:
与年轻代比率:-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】

在年轻代进行MinorGC的时候实际上也由可能触发到老年代GC操作。

【老年代串行GC】
算法:标记-清除-压缩;
扫描老年代中的存活对象,并且进行对象的标记;
遍历整个老年代的内存空间,回收所有标记对象;
为了保证可以方便的计算出老年代的大小,还需要进行压缩(碎片整理,把空间集中在一起。)
【老年代并行GC】

以前使用的:-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"

上一篇 下一篇

猜你喜欢

热点阅读