JVM
编辑类加载器分类
启动类加载器BootstrapClassLoad
默认加载jre/lib下的类文件
位于jvm环境中,无法在代码中获取,因为是由虚拟机底层提供的,使用C++编写的
通过启动类加载器去加载用户定义jar包:
放入jre/lib目录下进行扩展,不推荐
使用参数进行扩展,-Xbootclasspath/a:/jar(路径)
默认类加载器
位于sun.misc.Launcher中,是一个静态内部类,继承自URLClassloader
扩展类加载器ExtensionClassLoader
/jre/lib/ext
使用java编写的类加载器
通过扩展类加载器去加载用户jar包:
放入/jre/lib/ext下进行扩展
使用参数进行扩展,-Djava.ext.dirs=jar,会覆盖原始目录,用;/:分割
应用程序类加载器AppClassLoader
加载classpath下
类加载器的双亲委派机制
如何主动加载一个类:
使用Class.forName,使用当前类的类的类加载器
获取类加载器,通过loadClass方法
解决的问题:
打破双亲委派机制
自定义类加载器
自定义类加载器并重写loadClass方法
tomcat使用自定义类加载器实现web应用之间的隔离,每一个应用会有一个独立的类加载器
双亲委派机制的核心代码位于loadClass方法中:
自定义类加载器不指定父类会默认指向应用程序类加载器
只有相同类加载器+相同的类限定名才会被认为是一个类
正确的实现一个自定义类加载器的方法是重写findClass方法,这样不会破坏双亲委派机制
线程上下文类加载器
jdbc的spi机制:jdk内置的服务提供发现机制
spi中使用了线程上下文中保存的类加载器进行类的加载,一般是应用程序类加载器
jdbc只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制
OSGi框架的类加载器
OSGi使用了类加载器实现热部署
案例:使用arthas不停机解决线上问题
jdk9之后的类加载器
jdk8之前的类加载器
位于rt.jar中的sun.misc.Launcher.java
jdk9之后的类加载器
引入了module的概念
启动类加载器使用java编写,位于jdk.internal.loader.ClassLoaders类中。继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件
扩展类加载器替换为平台类加载器(PlatformClassLoader),继承关系从URLClassLoader变成了BuiltinClassLoader
运行时数据区
jvm运行java程序过程中管理的内存区域
程序计数器
又叫PC寄存器,每个线程会通过程序计数器记录当前要执行的字节码指令的地址
加载阶段:
程序计数器保存的是字节码指令的地址,可以控制指令的进行,实现分支跳转异常等
多线程情况下,可以记录cpu切换前解释执行到哪一句指令
程序计数器只会存储定长的内存地址,不会发生内存溢出
Java虚拟机栈
FILO先进后出
随着线程的创建而创建,随线程的销毁而回收
栈帧(Frame)的组成
局部变量表
存放方法执行中所有的局部变量
栈帧中的局部变量表是一个数组,每一个位置是一个槽(slot),long和double类型占用两个槽,其他类型一个槽
实例方法中的局部变量表的0号位存放的是this,存放该实例对象的地址
局部变量表保存的内容:
实例方法的this对象
方法的参数
方法体中声明的局部变量
局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以被再次使用
操作数栈
存放执行过程中的中间数据
在编译期就可以确定最大深度,从而在执行时分配内存大小
帧数据
动态链接
动态链接保存了符号引用(编号)到运行时常量池的内存地址的映射关系,保存了其他类的属性和方法
方法出口
在当前栈帧中存储的下一个栈帧的下一条指令的地址
方法结束时,当前栈帧会被弹出,程序计数器应该指向上一个栈帧中下一条指令的地址,当前栈帧中要存储此方法出口的地址
异常表
代码中异常的处理信息
try,catch跳转到的位置
栈内存溢出
栈帧过多,占用内存超过栈内存可以分配的最大大小就会溢出
StackOverflowError
默认大小
Linux | BSD | Solaris | Windows |
---|---|---|---|
x86:1024kb;ppc:2048kb | x86:1024kb | 64位:1024kb | 基于操作系统默认值 |
设置大小
-Xss栈大小(单位)
单位:默认字节、k、m、g
也可以用-XX:ThreadStackSize=1024
HotSpot JVM对栈大小的最大值和最小值有要求,win64下jdk8测试最小值为180k,最大值为1024m
局部变量过多,操作数栈深度过大也会影响栈大小
一般情况下,栈深度最多也只是几百,所以可以指定为-Xss256k节省内存
本地方法栈
存储native方法的栈帧
在HotSpot中,和虚拟机栈使用同一个栈空间
Java堆
属于java程序中内存最大的一块区域,创建出来的对象都位于堆
栈上的局部变量表,可以存放堆内存对象的引用。静态变量也可以存放堆内存的引用,所以可以实现对象在线程间共享
used
total
max
默认max是系统内存的1/4,total是1/64
-Xmx(max,必须大于2m) -Xms(total,必须大于1m)
一般将两个参数值设为一样
方法区(Method Area)
存放基础信息的位置,线程共享
类的元信息
一般称为InstanceKlass对象,在类的加载阶段完成
运行时常量池
字节码文件中通过编号查表找到常量,称为静态常量池。常量池加载到内存中,通过内存地址快速定位,叫做运行时常量池
Jdk7之前方法区存放在堆中的永久代
Jdk8之后方法区放在元空间,位于直接内存,使用-XX:MaxMetaspaceSize=256m进行限制
字符串常量池
Jdk7之后的版本中,静态变量存放在堆中的Class对象中
直接内存
为了解决以下问题:
java堆中你的对象不使用会被回收,回收会影响对象的使用和创建。
IO操作中需要先把文件读入直接内存在复制到Java堆 。现在直接放入直接内存,java堆维护直接内存的引用。
直接创建内存上的数据:ByteBuffer.allocateDirect(size)
手动调整:-XX:MaxDirectMemorySize=N
自动垃圾回收
线程不共享的区域,都是随着线程的创建而创建,线程的销毁而销毁。方法的栈帧在执行完方法后就会自动弹出栈并释放掉对应内存
方法区的回收
方法区中回收的是不再使用的类
判定一个类可以被回收,需要同时满足:
此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象
加载该类的类加载器已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用
手动触发垃圾回收:System.gc()
堆内存的回收
如果java对象被引用了,说明该对象还在使用,不允许被回收
如何判断堆上的对象有没有被引用
引用计数法
为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1
缺点:
维护计数器对系统性能有一定影响
循环引用时会出现对象无法回收问题
查看垃圾回收日志:-verbose:gc
可达性分析法
java中使用的是此方法
其将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系
那些对象称之为GC Root对象:
线程Thread对象,引用线程栈帧中的方法参数,局部变量等
每一个线程对象维护一个栈内存
系统类加载器加载的java.lang.Class对象
监视器对象,用来保存同步锁synchronized关键字持有的对象
本地方法调用时使用的全局对象
查看GC Root:
使用arthas的heapdump命令将堆内存快照保存到本地磁盘中
使用MAT工具打开堆内存快照
选择GC Roots功能查看所有的GC Root
五种对象引用
强引用
GC Root对象对普通对象的引用关系
软引用
一个对象只有软引用时就会被回收。JDK1.2后提供了SoftReference类实现,软引用用于缓存中。
垃圾回收如果任然不能解决内存不足,就会回收软引用中的对象
Caffeine框架:
Caffeine.newBuilder.softValues().build()
SoftReference对象如果需要被回收,如何确定被回收的是哪些呢:
弱引用
类似软引用,但不管内存是否不足,都会被回收
WeakReference,主要在ThreadLocal中使用
虚引用
唯一用途:当对象被垃圾回收时可以接收对应的通知。
PhantomReference
直接内存和堆内存的关联
终结器引用
finalize方法
强引用自救
垃圾回收算法
如何回收:
找到内存中存活的对象
释放不再存活对象的内存,使得程序能再次利用这部分空间
历史和分类:
垃圾回收启用单独的GC线程,称之为Stop The World,会停止所有用户线程,如果STW时间过长则会影响用户的使用
评价标准:
吞吐量
吞吐量=执行用户代码的时间/(执行用户代码的时间+GC时间)
吞吐量越高,效率越高
最大暂停时间
STW时间最大值
堆使用效率
使用完整的堆内存效率大于使用一半的堆内存
上述标准不可兼得
堆内存越大,最大暂停时间越长。减少最大暂停时间,就会降低吞吐量
标记清除算法
标记所有存活对象。使用可达性分析算法,从GC Root开始通过引用链标记
删除没有被标记的对象
优点:实现简单
缺点:
内存碎片化
分配速度慢
复制算法
From、To空间,每次分配只能使用其中一块(From)
GC阶段,将From存活对象复制到To
将两空间名字互换
优缺点:吞吐量高、不会发生碎片化;内存使用效率低
标记整理算法/标记压缩算法
对标记清理算法中容易产生内粗碎片问题的一种解决方法
标记->整理(将存活对象移动到堆的另一端,清理掉存活对象的内存空间)
优缺点:内存使用效率高,不会发生碎片;整理阶段效率不高
分代GC算法
现代最优秀的垃圾回收算法
将内存区划为年轻代和老年代
JDK8:-XX:+UseSerialGC 使用分代垃圾回收器
老年代=堆大小-新生代大小
分代回收时,创建出来的对象首先会被放入Eden区,满了后会触发Minor GC或Young GC
根据可达性分析法把Eden中和From中需要回收的对象回收,把没有回收的放入To,接下来互换To,From,如果Eden满了,会继续Minor GC,此时会回收Eden和From中的对象,把存活的对象放入To
每次Minor GC后对象年龄+1,年龄到达阈值(最大15),对象就会晋升到老年代
老年代空间不足,先尝试Minor GC,如果还是不足就会Full GC
Full GC仍然无法回收掉,对象继续放入老年代时就会OOM
垃圾回收器
垃圾回收器是垃圾回收算法的具体实现
Serial垃圾回收器
单线程串行年轻代垃圾回收器
SerialOld垃圾回收器
如上,老年代版本
-XX:+UserSerialGC 新生代老年代都用串行回收器
ParNew垃圾回收器
Serial在多CPU下的优化
-XX:+UseParNewGC
CMS垃圾回收器(Councurrent Mark Sweep)
允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间
XX:+UseConcMarkSweepGC
Parallel Scavenge垃圾回收器
PS是JDK8默认的年轻代垃圾回收器,多线程并行,具备自动调整堆内存大小的特点,关注吞吐量
PS允许手动设置最大暂停时间和吞吐量,官方建议使用这个组合时不要设置堆内存最大值
Parallel Old垃圾回收器
PS的老年代版本
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:PrintCommandLineFlags 启动时打印所有参数
G1垃圾回收器
JDK9之后的默认垃圾回收器(Garbage First)
优点:
支持巨大的堆空间回收,并有较高吞吐量
支持多CPU并行垃圾回收
允许用户设置最大暂停时间
G1内存结构:
整个堆化为大小相等的区域,称之为Region,区域不要求是连续的。分为Eden、Survivor、Old区
大小通过堆空间大小/2048
也可以通过-XX:G1HeapRegionSize=32M指定
Region必须是2的指数幂,去值范围从1M到32M
两种回收方式:
Young GC
Mixed GC
Young GC执行流程:
年轻代区不足(max默认60%),无法分配对象时需要执行回收
标记出Eden和Survivor区域中存活的对象
根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区(年龄+1),清空这些区域
G1在进行Young GC的过程中会记录回收每个Eden区和Survivor区的平均耗时,作为下次回收时的参考依据,根据配置的最大暂停时间计算出本次回收最多能回收多少个Region
后续Young GC时与之前相同,只不过Survivor区中存活的对象会被搬运到另一个Survivor区
当某个存活对象的年龄达到阈值(默认15),将被放入老年代
部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。如果对象过大会横跨多个Region
多次回收后,会出现很多老年代区,总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyParcent默认45%)会触发Mixed GC。回收所有年轻代和部分老年代的对象以及Humongous区。采用复制算法
Mixed GC回收流程:
初始标记、并发标记、最终标记、并发清理
对老年代的清理会选择存活度最低的区域进行回收,可以保证回收效率最高
清理阶段采用复制算法,不会产生内存碎片
Full GC
-XX:+UseG1GC 开启G1
-XX:MaxGCPauseMillis=毫秒值 最大暂停时间
垃圾回收器组合的选择:
- 0
-
分享