类加载机制
类加载流程
加载
通过类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为访问这个类在方法区各种数据的入口
类加载的时机
new关键字、读取或设置一个类的静态字段时(被final修饰的除外)
使用反射调用时,如 Class.forName()
初始化子类,父类未初始化时,先初始化父类
虚拟器启动,初始化main()方法的类
当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
验证
校验字节码文件的正确性
文件格式验证
元数据验证
字节码验证
符号引用验证
解析
将常量池内的符号应用替换为直接引用
符号引用
直接引用
初始化
执行类构造器<clinit>()方法
使用
卸载
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载必须按这个顺序按部就班的开始,只是开始,因为这些阶段都是相交叉的混合执行,会在一个阶段的执行过程中调用、和激活另一个阶段
类加载器
引导类加载器
加载存放在JRE_HOME/lib目录下存放的类库
扩展类加载器
加载JRE_HOME/lib/ext目录下存放的类库
应用程序类加载器
加载classpath下的所有类库
类加载器初始化过程
程序启动时创建JVM启动器实例sun.misc.Launcher
由Launcher创建另外两个类加载器
扩展类加载器
应用类加载器
双亲委派机制
流程
先检查请求加载的类型是否已经被加载过
没有则调用父加载器的loadClass()方法
父加载器为空则默认使用引导类加载器作为父加载器
父类加载器加载失败,调用自己的findClass()方法尝试进行加载
父子关系是通过类中的parent属性实现的
作用
沙箱安全机制
自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载
当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
全盘负责委托机制
“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入
自定义类加载器
继承ClassLoader类
重写findClass方法
如何打破双亲委派机制
在自定义类加载器的基础上重写loadClass方法
内存模型
类装载子系统
根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区的元空间中
运行时数据区
元空间
存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码缓存
参数
-XX:MetaspaceSize
‐XX:MaxMetaspaceSize
本地方法栈
结构与虚拟机栈类似
执行的是native方法
对象的创建
分配内存
对象所需内存的大小在类加载完成后便可完全确定
并发问题的解决方式
CAS
本地线程分配缓冲(默认)
-XX:+UseTLAB
设定虚拟机是否使用TLAB,默认启用
设置对象头
Mark World标记字段
哈希码
GC分代年龄
锁状态标志
线程持有的锁
偏向线程ID
Kclass Pointer 类型指针
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
执行<clinit>方法
即对象按照程序员的意愿进行初始化。<br>对应到语言层面上讲,就是为属性赋值(由程序员赋的值),和执行构造方法以及静态代码块。<br>
对象大小与指针压缩
代码
图
为什么进行指针压缩?
在64位平台的HotSpot中使用32位指针,内存使用会减少1.5倍左右<br>
使用较大指针在主内存和缓存之间移动数据, 占用较大宽带,同时GC也会承受较大压力
堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,所以堆内存不要大于32G
对象内存分配
流程图
对象栈上分配
逃逸分析
就是分析对象动态作用域,当一个对象在方法中被定义后,它可能不被外部方法所引用,<br>只在本方法中使用
代码
alloc方法中的user只在该方法中使用,作用域明确,可以随着栈帧的关闭而销毁
标量替换
通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,<br>而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,<br>这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。<br>
标量与聚合量
标量即不可被进一步分解的量, 如基础数据类型
聚合量就是可以被进一步分解的量, 如对象
逃逸分析用于分析对象是否可以在栈上分配,标量替换是对象在栈上分配的手段,二者缺一不可
对象在Eden区分配
大多数情况下,对象在新生代中 Eden 区分配
Eden区内存不够时,触发Young Gc/Minor Gc
Young Gc后存活的对象进入Survivor区
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象
如果对象超过大小会直接进入老年代
参数:-XX:PretenureSizeThreshold
设置大对象的大小
数只在Serial和ParNew两个收集器下有效
为什么要让大对象直接进入老年代?
通常大对象都是长期存活的对象,如大数组,静态变量
对于长期存活的对象,如果还是像普通对象一样在Eden区和Survivor区反复gc后才进入老年代,这是没有意义且白白消耗资源的事情
让大对象直接进入老年代,可以避免这样的问题,提升gc效率
长期存活的对象进入老年代
对象头中记录了对象的年龄
对象每并经过一次Young GC后仍然能够存活, 则年龄+1
年龄达到15(默认值)后进入老年代
参数
-XX:MaxTenuringThreshold
设置年龄阈值
如果清楚知道业务中的对象不会经过n次gc就会被回收,<br>那么可以将该阈值设置小一点,比如n*2
为什么可以设置小一点?
比如我明确知道业务中对象绝对不会超过3次gc就会被回收<br>那么反过来说,超过3次gc还没有被回收的对象,就是些可以长期存活的对象<br>长期存活的对象应该让它尽早进入老年代,不要在年轻代影响其他对象的内存分配和gc<br>但为了保险一点,我设置阈值为5,这样对象的年龄超过了5,就去老年代了<br>
动态年龄判断机制
如果在Young GC后,存活的对象放入Survivor区时,这一批存活的对象总大小大于这块Survivor区域内存大小的50%,<br>那么此时按照对象年龄从小到大排序,大于内存区域50%的对象,就直接进入老年代<br>
如一批存活对象放入Survivor区,这一批对象内存总大小达到了Survivor区的70%,<br>按照年龄排序,比如年龄1的对象占30%,年龄2占10%, 年龄3占10%,那么大于等于年龄3的对象全部进入老年代
目的同样是让长期存活的对象尽早进入老年代
老年代空间分配担保机制
流程
参数
-XX:-HandlePromotionFailure(默认设置)
为什么要这样做?
假设没有这套机制,就会发生Young GC后,有一些要放入老年代的存活对象,但是由于老年代剩余空间不够,导致又要触发一次Full GC
使用这套机制,可以避免多发生一次Young GC, 直接进行Full GC
代码
对象内存回收
可达性分析算法
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
常见引用类型
软引用
将对象用SoftReference软引用类型的对象包裹
弱引用
将对象用WeakReference软引用类型的对象包裹
虚引用
finalize()方法
如果使用可达性分析算法,那些被标记可回收的对象,可使用finalize方法进行自救
观察一些源码发现,常用于兜底的资源释放,如IO流,线程池
代码
如何判断一个类是无用的类
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
加载该类的 ClassLoader 已经被回收
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法