JVM 基础
程序计数器(Program Counter Register)CPU中的寄存器
作用: 记住下一条JVM指令
特点:
- 线程 私有
- 唯一一个不会出现内存溢出的区域
虚拟机 栈(Java virtual mechine Stack)
线程私有
每个线程运行时所需要的内存
每个栈由多个栈帧Frame组成,对应着每次方法调用时占的内存
每个线程只能有一个当前活动栈帧,对应着当前正在执行的方法
三个问题
一、垃圾回收是否涉及栈内存?
不会:垃圾回收的是堆内存的无用数据,栈帧调用完会自动弹出栈,不需要垃圾回收。
二、栈内存分配越大越好?
不是,物理内存一定,栈内存分配越大,线程数就会越少,并且较大的栈内存并不会提升运行速度,只不过像递归调用可以存储更多栈帧。可通过以下虚拟机参数调节栈内存大小
三、方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用访问,则线程安全
如果局部变量引用了对象,并逃离了方法的作用访问(如返回对象变量,如对象作为参数传入方法)需要考虑线程安全问题
栈内存溢出
两种情况,会抛出 Java .lang.StackOverflowError的异常
栈帧过多(一般出现在递归)
栈帧过大,栈内存分配较小(一般不容易出现)
线程运行诊断
CPU占用过高,定位哪个线程引起的
Linux系统下top命令找出对应的进程PID
ps H -eo pid,tid.%CPU | grep 进程ID (用ps命令进一步定位哪个线程占用CPU过高)
jstack 进程id 列出当前Java进程下的所有的线程,根据十六进制线程id进行定位线程
程序很长时间没有获得结果(常见死锁问题)
jstack 进程id 列出当前Java进程下的所有的线程,根据十六进制线程id进行定位线程
本地方法栈
特点:线程私有
作用:Java代码不能很好地与操作系统打交道,因此采用native关键字,API调用C/C++代码,实现对操作系统相关API调用,这些方法(如Object类的notyAll,wait方法)运行需要单独的栈内存空间,称之为本地方法栈。
堆
Heap堆,通过使用new关键字创建对象都会使用堆内存
特点:
线程共享,需要考虑线程安全问题
有垃圾回收机制(堆中不在引用的对象)
堆内存溢出
会抛出异常OutOfMemoryError:Java heap space
调节堆空间内存大小 -Xmx内存大小
方法区(概念)
特点:线程共享
定义:存储包含类的结构相关一些信息(包含运行时常量池,类的成员变量,方法数据,成员方法构造器方法的代码部分)和类的加载器的信息,
方法区在虚拟机启动时被创建。
JDK1.6 与JDK 1.8 的对比
- 1.8方法区的实现是元空间,占用的是操作系统的内存,并将常量池中的StringTable移到堆内存中。
- 1.6方法区的实现是永久代,占用的是堆内存。
内存溢出
也会抛出异常OutOfMemoryError:Metaspace 1.8及以后,OutOfMemoryError:PermGen space 1.7及之前
调节方法区内存大小参数
-XX:MaxMetaspaceSize内存大小 JDK1.8及以后
-XX:MaxPermSize内存大小 JDK1.7及以前
出现场景
spring或mybatis都会用到cglib动态代理产生很多代理类,可能会出现OOM。
运行时常量池
常量池:就是一张表,虚拟机根据这张表找到需要的执行的类名,方法名,参数类型,字面量等信息
运行时常量池 :位于*.class文件中,当类被加载,常量池信息就会加载到运行时常量池中,并将里面的符号转换成内存地址。
StringTable串池:
位于堆内存中, JDK1.6及以前位于常量池中,因为大量运用,老年代回收效率低,后续放在队中。
StringTable串池是延迟加载的,起初为空,用到才会转换新的String对象,且数组中对象不会重复转换【唯一】。
s1、s2是变量,引用的值可能改变,因此必须运行时进行StringBuilder拼接,返回一个新的对象
- s1+“b”,s2+“a”,s1+s2都与”ab”不相等,原因是不同对象
此时”a”+”b”值固定,已经在编译期间进行优化完成,不需要运行时解析变量拼接。
public static void main(String[] args) {
//串池对象【”a”】
//堆对象【new String(“a”)】
System.out.println(new String(“a”)==”a”); //false 说明串池与堆中对象不是同一个
}
public static void main(String[] args) {
//串池【”a”,”b”】一旦出现”a”就会创建串池对象
//堆池【ab,new String(“a”),new String(“b”)】
String ab =new String(“a”)+new String(“b”);
String abi = ab.intern(); //尝试将ab变量String对象放入串池,并返回串池的对象名为abi
//串池【”a”,”b”,”ab”】 因为串池中没有此”ab”值的常量对象,因此放入成功
System.out.println(ab ==”ab”); //true ab变量对象已在串池中
System.out.println(abi ==”ab”); //true abi变量引用返回的串池对象”ab”
}
public static void main(String[] args) {
String x =”ab”; //将”ab”放入串池
//串池【”a”,”b”,”ab”】一旦出现”a”就会创建串池对象
//堆池【ab,new String(“a”,new String(“b”)】
String ab =new String(“a”)+new String(“b”);
String abi = ab.intern(); //尝试将ab变量String对象放入串池,并返回串池的对象”ab”
//串池【”a”,”b”,”ab”】 因为串池中”ab”值的常量对象,因此放入失败
System.out.println(ab ==”ab”); //false 放入失败,ab变量对象不是串池对象
System.out.println(abi ==”ab”); //true abi变量引用指向返回的”ab”串池对象
}
上述代码最后一问补充:
//调换顺序后,结果为true,但如果是JDK1.6及以前都是false
x2.intern();
String x1 = “cd”;
垃圾回收
当堆内存满时,会对串池中的常量进行垃圾回收,没有引用的常量会被回收!
设置堆内存为10m,打印串池和垃圾回收详细信息
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
性能调优
-XX:StringTableSize=(桶个数)
对于常量字符串特别多又重复(读取很多单词的文件)的情况下,intern()方法将字符串入池,适当将-XX:StringTableSize调大,减少hash冲突,增加执行速度。
检测工具
jps ——-》 显示java进程id
jmap -heap 进程id ——-》显示堆内存的详细信息
jvisualvm ,jconsole ——-》图形化界面显示各线程详细信息
直接内存(Direct Memory)
- 常见于NIO操作,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
直接内存在Java堆内存与系统内存中共享,减少了数据的一次复制。
内存溢出
抛出异常:Java.lang.OutOfMemoryError: Direct buffer memory
分配和回收管理
获取Unsafe对象需要通过反射
直接内存使用底层Unsafe类管理(分配和释放等等)的,释放需要主动调用freeMemory 方法。
ByteBuffer 的实现内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被回收,那么就会由RefernceHandler线程通过 Cleaner 的 clean 方法调用 freeMemory来释放直接内存。
JVM参数
-XX:+DisableExplicitGC JVM参数禁用显式调用 System.gc() 垃圾回收,因为 System.gc() 是full GC 影响性能。
垃圾回收
如何判断一个垃圾可以回收
一、引用计数法
只要一个对象被一个变量引用,就让对象的计数加一,如果对象被引用两次,计数就变成2,如果某一个变量不在引用
这个对象了,就让计数减一,当对象引用计数等于0时,意味着是垃圾对象,等待垃圾回收。
问题:对象循环引用导致计数不会等于0,造成内存泄漏。
✳二、可达性分析 算法 【Java虚拟机采用】
判定对象是否存活,扫描堆中对象先确定一系列的根对象GC Roots,判断每个对象是不是直接或间接地被根对象引
用,如果没有任何引用,证明对象是可回收的。
固定根对象 GC Roots
- 虚拟机栈中引用的对象(参数,局部变量,临时变量等)
- 静态属性引用的对象
- 常量引用的对象(串池)
- 本地方法栈的引用对象
- 虚拟机内部的引用(基本数据类型对应的Class对象,异常对象)
- 同步所持有的对象(synchronized括号里的对象)
四种引用
强引用
只有强引用关系不存在时,即所有GC Root 都不通过强引用该对象,该对象才被垃圾回收。
Object obj = new Object(); //声明强引用
1
软引用
描述一些还有用,但非必须的对象,仅有软引用引用的对象,垃圾回收后,内存仍不足的情况下会回收。
可配合引用队列来释放软引用自身
SoftReference<Object> sf = new SoftReference<>(new Object());//创建软引用
1
弱引用
描述非必须的对象,仅被弱引用引用的对象,垃圾回收时,无论内存是否充足,都会回收该对象。
可配合引用队列来释放软引用自身
WeakReference<Object> wrf = new WeakReference<>(new Object());//创建弱引用
System.out.println(wrf.get()); // java.lang.Object@4b67cf4d
System.gc(); //执行gc
System.out.println(wrf.get()); //null
虚引用
必须配合引用队列释放自身,在创建时必须提供一个引用队列作为参数。主要配合ByteBuffer使用,被引用对象回收后,会将虚引用入队,由ReferenceHandler线程调用虚引用相关方法释放自身。
它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知
应用程序对象的回收情况。
ReferenceQueue<Object> phantomQueue = new ReferenceQueue<>(); //指定引用队列
PhantomReference<Object> prf = new PhantomReference<>(new Object(),phantomQueue);//创建弱引用
System.out.println(prf.get()); //null
终结引用
它用于实现对象的finalize()方法,也可以称为终结器引用。
无需手动编码,其内部配合引用队列使用。
在GC时, 终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。
垃圾回收算法
一、标记清除算法
特点:速度快
缺点:造成内存碎片
二、标记整理算法
特点:涉及内存地址的移动,速度慢
优点:不会造成内存碎布
三、复制算法
特点:没有内存碎片,速度最快
缺点:造成双倍内存空间的占用
分段理论
不同对象生命周期不一样,把Java堆分成新生代和老年代,根据各个年代的特点,使用不同的三种垃圾回收算法的集合。
Stop the world
每一次垃圾回收会触发Stop the World 暂停一切用户线程,因为对象的迁移涉及内存地址的转换,避免出现混乱,垃圾回收完成后,用户线程恢复!
新生代
每次垃圾收集都会有大批对象死去,每次回收后存活的少量对象,会通过寿命+1来晋升,触发Stop the world。
垃圾回收:Minor GC 【标记+复制算法】
老年代
寿命=15的对象会迁移至老年代。寿命在对象头以4bit表示,最大值为15 ,触发Stop the world。
垃圾回收:Major GC 【标记+整理算法】
整体回收
Full GC 新生代和老年代一起进行垃圾回收,之后触发Stop the world。触发Stop the world
具体步骤
① 对象首先分配在伊甸园区,新生代内存不足时,会触发一次 Minor GC ,伴随着 Stop the World
② 伊甸园和From幸存区中存活的对象会Copy到To区中,并且交换From和To区域,其存活对象的寿命会加1.
③ 当寿命超过阈值时,会晋升到老年代区域。阈值最大是15,有些情况(新生代内存严重不足)会提前。
④ 当老年代内存空间不足时,先尝试触发Minor GC ,如果内存还不足,触发 Full GC,Stop the world 时间会更长。
垃圾回收JVM参数整理
大对象直接晋升老年代
新生代内存一定不够,老年代内存空间可以容纳的情况下
OOM
新生代和老年代的空间都不足以放入当前大对象,抛出OOM异常,不同线程抛出OOM不会影响其他线程的执行!
垃圾回收器
一、串行
Serial /SerialOld收集器
新生代采用 Serial 利用复制算法,效率高,老年代采用 SerialOld 标记整理算法。新生代+老年代回收两次STW。
用户线程在达到安全点时,其中一个线程抢占执行垃圾回收任务(还有stop the world),其他线程阻塞,完成之后恢复。
- 单线程
- 堆内存较小,适合个人电脑
二、吞吐量优先
Parallel Scavenge/ Parallel Old 收集器
新生代采用 Parallel Scavenge 利用复制算法,效率高,老年代使用 Parallel Old 多线程和 “标记-整理” 算法。新生代+老年代回收两次STW。
用户线程在达到安全点时,所有线程并行执行垃圾回收任务(还有stop the world),完成之后恢复
多线程
堆内存较大,多核CPU
目的单位时间内,尽可能减少Stop the world 的时间
区别 1小时的单位时间
吞吐量优先:0.2+0.2(共执行2次)
响应时间优先:0.1+0.1+0.1+0.1+0.1(共执行5次)
垃圾回收线程个数与CPU核心数相同,执行垃圾回收线程时,CPU占用率会大幅提高。
三、响应时间优先
CMS收集器
基于标记清除算法,对于造成的内存碎片,采用基于标记整理算法的Serial Old回收器执行Full GC作为补偿。产生浮动
垃圾【并发清理其他用户线程产生的垃圾对象需下次清理】
适用场景
- 多线程
- 堆内存较大,多核CPU
- 目的 :尽可能让每次Stop the wordl 的时间减少
G1收集器
G1适合作为服务端垃圾收集器,应用在多处理器和大内存的条件下,可以实现高吞吐量的同时,尽可能满足垃圾收集
较短可控的暂停时间。
整体基于标记清除算法,局部采用复制算法。
垃圾收集器采用分区(不连续的Region),除了Full GC 外,还包含年轻代和部分老年代的Mixed GC,分为4阶段。
- 初始标记 【标记根对象】伊甸园区域空间不足时,年龄较大的对象放入老年区,G1采用复制算法,触发Minor GC和STW,时间很短。【单一线程运行,其余阻塞】
- 并发标记 【随着根对象的引用标记其他对象 SATB算法】
- 老年代占用堆空间达到阈值时,进行并发标记,不会STW,【JVM参数可调阈值】。
- 最终标记 【并发标记中漏掉的对象】规定时间内,标记并发标记中新变化的回收价值高的区域
- 筛选回收 【复制标记存活的对象进入新的老年代区域,清除旧内存】
- 并发失败后,即老年代区域仍不够,就会进入多线程收集器执行Full GC!
应用
一般用于自定义类加载器(框架)会采用!
为避免Full GC,可控制老年代垃圾回收的阈值,JVM参数
-XX:initiatingHeapOccupancyPercent=45% 设置 【默认】
JDK 9 中设置后可进行自动的动态调整。
算法细节
卡表减少GC Root的搜寻时间
新生代回收需要寻找根对象,根对象有一部分是来自老年代的,老年代对象多,便历直接搜对象效率太低,因此采用卡
表的数据结构将老年代细分更小的单位(卡),如果老年代中卡中存有根对象引用了新生代,标记为脏卡,通过写屏 障更新脏卡,避免找整个老年代。
多线程下增加写屏障保证重新标记阶段安全
当对象引用发生改变时,JVM会加入写屏障,只要对象引用发生改变,写屏障代码会执行,会将对象置为待处理的状
态,并放入队列,并发标记结束后再判断是否需要回收。
类加载
加载阶段
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 右: _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
注意
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中可以通过前
面介绍的 HSDB 工具查看
链接
验证
验证类是否符合 JVM规范,安全性检查(例如用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行异常)
准备
javap -v -p class文件路径 //反编译
为 static 变量分配空间,设置默认值
static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror (类对象)末尾
static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
解析
将常量池中的符号解析为直接引用。
初始化
()V 方法 【类的构造方法】
初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全
发生的时机
概括地说,类初始化是【懒惰的】
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 人类初始化,如果父类还没初始化,会引发
- 人类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
类加载器
双亲委派模式
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则 ,防止恶意篡改类,保证了类的唯一。
//loadclass实现双亲委派模式的算法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
}else {
// 3. 如果没有上级了(ExtClassLoader),则委派
BootstrapClassLoader c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 – t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
启动类加载器
根据虚拟机参数 -Xbootclasspath 表示设置 bootclasspath
其中 /a:. 表示将当前目录追加至 bootclasspath
java -Xbootclasspath:
java -Xbootclasspath/a:<追加路径>
java -Xbootclasspath/p:<追加路径>
扩展类加载器
将 Jar 包拷贝到 JAVA_HOME/jre/lib/ext 目录下,启动 Jar 包,其包含的类就采用扩展类加载器加载。
应用程序类加载器
用户指定的路径下的类采用此类加载器加载。
自定义类加载器
什么时候需要自定义类加载器?
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
继承 ClassLoader 父类
要遵从双亲委派机制,重写 findClass 方法
注意不是重写 loadClass 方法,否则不会走双亲委派的机制
读取类文件的字节码
调用父类的 defineClass 方法来加载类
使用者调用该类加载器的 loadClass 方法
打破双亲委派机制—-SPI
JDK提供了一种帮第三方实现者加载服务(如数据库驱动,日志库)的便捷方式,只要遵循约定,在Jar包中把类名写
在/META-INF文件夹里,在调用forName加载,当前类的ClassLoader是没办法加载的,那么就把他加载到当前线程的
线程上下文加载器【属于应用程序加载器】。
JDBC 的 驱动类
classpath/a:<追加路径>**
java -Xbootclasspath/p:<追加路径>
扩展类加载器
将 jar 包拷贝到 JAVA_HOME/jre/lib/ext 目录下,启动 Jar 包,其包含的类就采用扩展类加载器加载。
应用程序类加载器
用户指定的路径下的类采用此类加载器加载。
自定义类加载器
什么时候需要自定义类加载器?
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
继承 ClassLoader 父类
要遵从双亲委派机制,重写 findClass 方法
注意不是重写 loadClass 方法,否则不会走双亲委派的机制
读取类文件的字节码
调用父类的 defineClass 方法来加载类
使用者调用该类加载器的 loadClass 方法
打破双亲委派机制—-SPI
JDK提供了一种帮第三方实现者加载服务(如数据库驱动,日志库)的便捷方式,只要遵循约定,在Jar包中把类名写
在/META-INF文件夹里,在调用forName加载,当前类的ClassLoader是没办法加载的,那么就把他加载到当前线程的
线程上下文加载器【属于应用程序加载器】。
JDBC 的 驱动类