JVM的概述

JVM的构成

  • 类加载子系统(负责将类读取到内存,校验类的合法性,对类进行初始化)

  • 运行时数据区(负责存储类信息,对象信息,以及执行计算的区域)

  • 执行引擎(负责从指定地址对应的内存中读取数据然后执行,同时还自带GC操作)

  • 本地库接口(负责Java语言与其它语言之间进行通讯)

类加载子系统

  • 主动加载:访问本类成员或方法时触发的类的加载。

  • 被动加载:访问本类对应的父类属性、方法时,本类属于被动加载,父类属于主动加载。

类加载器

  • BootStrapClassLoader(负责加载基础类库中的类,例如Object,String,....)

  • ExtClassLoader(负责加载扩展类库中的类ext/...)

  • AppClassLoader(负责加载classpath路径下我们自定义的类)

  • 自定义ClassLoader(可以指定自己要加载的路径或加载方式),所以即使是同一个类,但是它的类加载器不同,生成的字节码对象也可以不同。场景:

  • 指定加载源头?(例如从数据库中读取类)

  • 保证类的安全?(例如可以在类编译时加密,加载时解密)

  • 打破双亲委派模型?(对自己写的类不想使用双亲委派机制了)

class SimpleClassLoader extends ClassLoader{
 ...
}

双亲委派类加载模型

  • 双亲委派可以简单理解为依次向上询问类是否已经加载过,假如加载过则无需再次加载。假如没有加载过则从顶层(BootStrapClassLoader)向下依次尝试是否可以加载此类,假如可以则对类进行加载。

  • 通过这种机制可以更好保证一个类在内存中只被加载一次,例如java.lang.Object这个基础,类库中的类不需要反复加载。

  • 弊端是假如不同项目部署到了同一个web服务中,但是不同项目中有包名、类名相同的类(内容不同),这样可能会导致有一个类不会被加载。假如要想解决这个问题可以自己定义类加载规则,但是无论你怎么定义,建议基础类库还是要使用双亲委派方式进行加载。

类加载的基本步骤

  • 查找类(例如从指定路径找到包名+类名对应的文件)

  • 读取类(通过字节输入流对类进行读取)

  • 校验类(对内存中读取到的类信息进行校验、默认初始化等)

  • 创建字节码对象(java.lang.Class)

触发类加载的时机

  • 直接通过类加载器(ClassLoader)的loadClass去加载。

  • 基于Class.forName("包名.类名")方式去加载。

  • 直接访问类的属性,方法,构建类的对象。

运行时数据区

  • 方法区(Method Area): 存储类的字节码信息、常量池。

  • 堆区(Heap Area): 存储对象

  • Java方法栈(Stack Area): 所有方法运行时,会创建一个栈帧对象,然后进行入栈

  • 本地方法栈(Stack Area):用C语言写的,方法执行时候,会进入本地方法栈

  • 程序计数器(Pc Register): 用于记录当前线程要执行的下一条字节码指令的地址

方法区

  • 方法区是逻辑上一种定义,是一种规范,可被所有线程所共享,不同JVM对方法区的落地实现可能不同,例如在JDK7中方法区称之为持久代,在JDK8中方区叫元空间(Metaspace),并且这个元空间,可以是JVM堆外的一块内存,不占用操作系统为JVM分配的内存。

  • 包含了.class字节码文件(包括静态变量、所有方法)

  • Java中的堆是用于存储对象的一块区域,可被所有线程所共享。这块区域又可以分为年轻代和老年代,年轻代又分伊甸园区和2个幸存区(有一块区域始终是空的)。

  • 包含了new出来的对象(包括实例变量、数组的元素)

  • 对象的内存分配过程

  • 编译器通过逃逸分析(JDK8已默认开启)判定对象是在堆上分配还是在栈上分配。逃逸的在堆上,未逃逸的栈上

  • 假如确定是在堆上分析,则可首先选择TLAB区,检测这块区域是否可以存储这个对象,可以则存储。

  • 假如TLAB区无法存储新创建的对象,则可以考虑在TLAB之外的Eden区加锁分配。

  • 如果Eden区无法存储对象,则执行Yong GC (Minor)-年轻代的垃圾回收

  • 假如Eden区执行Yong GC之后,仍然不足以存储对象,则直接分配老年代。

  • 假如老年代也不可以存储这个对象,则执行Full Gc,这个过后还不能存储则抛出异常。

  • 设置年轻代和老年代主要是为了减少GC的空间范围,提高GC效率。

  • 当伊甸园区设置的比例比较小时候,因为我们创建的对象,大部分是要存储在伊甸园区。假如伊甸园区比较小,可能会增加GC的频率。进而影响系统的执行性能。

  • 当年轻代中幸存区设置的比例比较小时候,年轻代的伊甸园区对象越来越多时,会启动Yong GC,此时伊甸园区的对象就要拷贝到幸存区。假如这个幸存区比较小,无法存储从伊甸园区拷贝过来的幸存对象,此时这些对象就可能直接分配到老年代,就会导致老年代对象越来越多,触发老年代GC的频率就变高,老年代的GC一般为FullGC,这个GC的时间会比较长,会影响系统的吞吐量和执行效率。伊甸园区与两个幸存区的比例通常是8:1:1

  • 调优中推荐初始堆大小和最大堆大小要设置为一样,这是为了避免程序在运行过程中,因对象的多少或GC后内存的变化,进而导致的内存大小的调整,这个内存大小调整的过程可能会带来更大的系统开销。

  • 不经过年轻代直接存储到老年代的场景

  • 创建的对象比较大,年轻代没有空间存储这个对象。

  • 经过多次GC,没有被回收的对象,随着年龄的增加可能会移动到老年代。

  • 随着技术的进步,对象都是分配在堆上这个说法不准确了,对象可以分配在栈上了(小对象、未逃逸可以分配在栈上)。

  • 逃逸分析:逃逸分析本质上是一种数据分析算法,基于这个算法判定对象是否发生了逃逸,未逃逸的对象可以直接分配在栈上、也可以执行标量替换。这样可以有效减少对象在堆上的分配,进而减少阻塞、GC频率,提高执行效率。

  • 标量替换是一种将对象打散(将对象中成员以局部变量方式进行设计)分配在栈上的技术,减少对象在堆中创建次数,进而降低GC频率,提高其性能。

Java方法栈

  • Java中每个线程都有一个虚拟机栈(Java方法栈),每个方法的执行都会对应着一次入栈(Push)和出栈(Pop)操作,栈中的元素为一个一个的栈帧(Stack Frame)对象,这个栈帧的构成主要有如下几部分:

  • 操作数栈(用于执行运算)

  • 局部变量表(用于存储方法内部的局部变量)

  • 方法的返回值(存储方法的返回值)

  • 动态链接(方法中要访问的一些常量池数据,要调用的方法,都会对应一个链接)

  • 其它信息

方法运行示例图

程序计数器

  • 程序计数器用于记录当前线程要执行的下一条指令的偏移量地址,每个线程都有一个程序计数器,这个计数器也是所有内存中唯一一个不会出现内存溢出的区域。

内存溢出

  • 内存中剩余的内存空间不足以分配给新的内存请求,此时就会出现内存溢出。内存溢出可能会导致系统崩溃,具体可能导致内存溢出的原因有:

  • 创建的对象太大。(堆内存溢出)

  • 创建的对象太多了,又有大量的内存泄漏。(堆内存溢出)

  • 方法区的类太多了,没有足够的空间存储一些新的类型了。(方法区内存溢出)

  • 方法出现了无限递归调用,可能会导致栈内存溢出。(栈内存溢出)

内存泄漏以

  • 程序中的对象在使用完毕后,对象占有的内存空间,没有得到及时的释放,一直占用着内存空间,这个现象就称之为内存泄漏。常见内存泄漏可能有:

  • 缓存使用不当(例如缓存中对对象的引用都是强引用)

  • 内部类的使用不当(例如实例内部类会默认保存外部类引用)

  • 大量的IO,链接操作没有得到及时关闭。

  • 大量的使用static变量(这样的变量的生命周期不依赖于变量所在类对象)

四大引用

  • Java中为了更好的控制对象的生命周期,提高对象对内存的敏感度,设计了四种引用类型,按其在内存中的生命力强弱,可分为强引用、软引用、弱引用、虚引用。其中,强引用引用的对象生命力最强,其它引用引用的对象生命力依次递减。JVM的GC系统被触发时,会因为对象的引用不同,执行不同的回收逻辑。

GC系统

  • GC(Garbage Collection)称之为垃圾回收,在JVM的执行引擎中自带这样的一个GC系统,此系统会按照一定的算法对内存进行监控和垃圾回收。

判定垃圾对象

  • 引用计数法:每个对象都一个引用计数器,只要有引用引用着这个对象,这个计数器就会加1,没有引用引用计数器的值就是0,缺陷是当出现循环引用的时候无法回收

  • 可达性分析:从一些GC Root对象可以无法找到这个对象,此对象就是垃圾.GC Root对象包括:

  • 类变量、常量直接引用的对象

  • 实例变量直接引用的对象

  • 局部变量、参数变量直接引用的对象

GC算法

  • 标记清除:扫描内存,对活着的对象进行标记,再次扫描内存,对未标记对象进行清除

  • 标记复制:扫描内存,将活着对象标记同时拷贝到一块空闲区,然后将原有内存全部清空

  • 标记整理:扫描内存,将活着对象向一侧移动,然后将边界外的内存进行清空

GC时的线程策略

  • 串行(整个GC过程只有1个线程执行)

  • 并行(允许多线程利用多核CPU优势执行并行GC)

  • 并发(允许GC线程和业务线程并发执行)

垃圾回收器

  • -XX:+PrintCommandLineFlags//查看垃圾回收器

Serial收集器
  • 内部只使用一个线程进行垃圾回收,不能执行并行化(不能充分利用多核CPU优势)

  • GC时所有正在执行的业务的线程都要暂停(Stop The World - STW)

  • 新生代使用标记复制算法,老年代使用标记整理算法。

  • -XX:+UseSerialGC//时候用Serial收集器

Parallel收集器
  • 内部有多个线程进行垃圾回收,可以利用多核CPU优势进行并行GC操作,可以减少业务暂停时间。

  • GC时所有正在执行的业务线程都要暂停(Stop The World - STW)

  • 新生代使用标记复制算法,老年代使用标记整理算法。

  • -XX:+UseParallelGC//使用Parallel收集器

CMS收集器
  • 内部有多个线程进行垃圾回收,可以利用多核CPU优势并行GC操作,可以减少业务暂停时间。

  • 用户业务线程和GC线程可以并发执行。

  • 新生代使用标记复制算法,老年代使用标记清除算法(不整理内存,响应速度会更快)。

  • -XX:+UseConcMarkSweepGC//使用CMS收集器

G1收集器
  • 工作于服务器模式,主要面向多核、大内存的服务器应用。

  • 整个堆不再分成连续的年轻代和老年代,而是划分为了多个小堆区。

  • GC时不会每次收集整个堆,而是以增量方法进行GC操作,每次只是对一个小堆区进行GC。

  • 可以在吞吐量和响应时间上达到一种相对的平衡。

  • 年轻代使用标记复制算法,老年代使用标记整理算法。

  • -XX:+UseG1GC//使用G1收集器

JVM调优策略
  • 更换CPU,内存; 调内存大小、比例参数、调整GC收集器

常用参数

  • 检查类加载

  • -XX:+TraceClassLoading

  • 方法区参数配置

  • -XX:MetaspaceSize

  • -XX:MaxMetaspaceSize

  • 常用堆参数配置

  • -Xms2048m(设置初始堆大小为2048m)

  • -Xmx2048m(设置最大堆大小为2048m)

  • -Xmn1g(设置年轻代大小为1g)

  • -XX:NewRatio=4(设置年轻代与老年的比例大小)

  • -XX:SurvivorRatio=4(设置年轻代中的Eden区与Survivor区比值,这里的4表示4:1:1)

  • -XX:MaxTenuringThreshold=15(年轻代对象转换为老年代对象最大年龄值,默认值15)

  • 常用栈参数配置

  • -Xss128k(设置每个线程的栈大小)

  • GC日志参数配置

  • -XX:+PrintGC (GC简单日志)

  • -XX:+PrintGCDetail (GC详细日志)

  • 垃圾回收器参数配置

  • Serial垃圾收集器(新生代)

  • 开启:-XX:+UseSerialGC

  • 关闭:-XX:-UseSerialGC

  • Parallel垃圾收集器(老年代)

  • 开启 -XX:+UseParallelGC

  • 关闭 -XX:-UseParallelGC

  • CMS垃圾收集器(老年代)

  • 开启 -XX:+UseConcMarkSweepGC

  • 关闭 -XX:-UseConcMarkSweepGC

  • G1垃圾收集器

  • 开启 -XX:+UseG1GC

  • 关闭 -XX:-UseG1GC

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值