JVM(Java Virtual Machine) 学习笔记
1 类生命周期
类的加载-》连接-》初始化-》使用-》卸载
- 类的加载
查找并加载类的二进制数据(将生成.class文件加载到Jvm) - 连接
- 验证 .class文件正确性校验
- 准备
static静态变量分配内存,并初始化默认值 static int num = 10 这里会使 num = 0准备阶段只有类,没有对象 初始化:static、非static、构造 - 解析 把类中的符号引用(com.zhj.pojo.User代替User对象)转为直接引用(直接使用内存地址)
- 初始化
static int num = 10; 这里会把num = 0, 变为num = 10 - 使用
对象初始化、垃圾回收、对象销毁 - 卸载
jvm 结束声明周期时机
- 正常结束
- 异常结束
- System.exit()
- 操作系统异常
2 JVM 内存模型(JMM[Java Memory Model])
用于定义变量(所有线程的共享变量,不能使局部变量)的访问规则
线程调用会使用工作内存,将主内存的数据拷贝一份到工作内存
分为两个区域 主内存区域、工作内存区域
- 线程不可用直接访问主内存与其他变量的内存
- 不同线程,可以通过主内存间接访问其他工作内存(线程安全问题,锁机制就是线程独占)
- Lock:将主内存变量,设为线程独占状态
- Read:将主内存读取到工作内存在
- Load:将读取到的变量写为工作副本
- Use:把工作副本变量拿去给线程使用
- Assign:把线程正在使用的变量,传递工作内存中的变量副本中
- Store:把工作内存中的值传递给主内存
- Write:将变量副本作为主内存中的变量进行存储
- Unlock: 解决线程独占状态
要求以上动作是原子性的,对于64位的数据类型(long、double)有些非原子性协议
3 volatile
概念:JVM提高的轻量级同步机制
作用:
- 防止JVM对long、double等64位的原子性协议进行误操作(读取半个)
- 可以使变量对线程立即可见(修改了工作内存中的变量副本,会立即同步其他线程的工作内存)
- 禁止指令的重排序
原子性 num = 10;
非原子性:
- int num = 10; int num; num=10;
- instance = new Singleton(); 1. JVM分配内存地址和内存空间 2. 使用构造实例化对象 3. instance = 分配的地址
重排序: 排序对象是原子性的(不是原子性的需要先转化成原子性)操作,目的是为了提高执行效率
单例会静止重排序,内存屏障
- 写操作前StoreStore
- 写操作后StoreLoad
- 读操作前LoadLoad
- 读操作后LoadStore
重排序不会影响单线程的执行结果
volatile 不能保证线程安全与原子性
4 JVM 内存区域
线程私有(对应JMM的工作内存)
- 程序技术器(Program Counter Register)
- 指向当前线程所执行字节码指令的地址
- 调用本地方法是undefined
- 不会产生内存溢出异常的
- 虚拟机栈(VM Stack)
描述方法执行的内存模型- 方法执行会在虚拟机栈加一个栈帧
- 栈帧中包含:方法的局部变量 操作数栈
- 本地方法栈(Native Method Stack)
线程共享(对应JMM的主内存) - 方法区
存放:类的元数据(描述类的信息)、常量池、方法信息(方法数据,方法代码)
工程: 类的元数据、常量池
方法区太多也会OOM
存放编译期间产生的 - 堆(heap)
- 存放对象实例(对象,数组)
- 存放对象实例虚拟机最大的一块
- JVM启动就创建完成
- GC主要管理区域
- 本身是线程共享的。可以画出多个线程私有的缓冲区
- 允许空间不连续
- 对可以分为新生代,老生代 大小比例1:2
- 新生代(eden:8 (s0:1 s1:1幸存者))大小比例 8:1:1
- 新生代使用率90%,使用时只能使用一个eden 与 一块s
- 新生代,存放生命周期比较短的对象、比较小的对象 对象大小设置-XX: PretenureSizeThredshold。一般而言,大对象一般是集合、数组、字符串
MinorGC回收新生代中的对象,没回收走第一次转移到区,再回收就年龄就加1,年龄到达一定数字就会转入老生代
新生代在使用时只能同时使用一个区,为了避免碎片产生
老生代:生命周期比较长的对象 2大的对象
新生代特点:
- 大部分对象在新生代
- 新生代回收频率高,效率高
老生代特点: - 空间大
-增长速度慢
-频率低
意义:可以根据项目中对象大小的数量,调节比例
对象太多也可能导致异常
虚拟机参数
-Xms64m
-Xmn32m
-Xmx128m 总大小
java -X
-Xmixed 混合模式执行(默认)
-Xint 仅解释模式执行
-Xbootclasspath:<用 ; 分隔的目录和 zip/jar 文件>
设置引导类和资源的搜索路径
-Xbootclasspath/a:<用 ; 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 ; 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag 显示附加诊断消息
-Xnoclassgc 禁用类垃圾收集
-Xincgc 启用增量垃圾收集
-Xloggc:<file> 将 GC 状态记录在文件中(带时间戳)
-Xbatch 禁用后台编译
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小
-Xss<size> 设置 Java 线程堆栈大小
-Xprof 输出 cpu 分析数据
-Xfuture 启用最严格的检查,预计会成为将来的默认值
-Xrs 减少 Java/VM 对操作系统信号的使用(请参阅文档)
-Xcheck:jni 对 JNI 函数执行其他检查
-Xshare:off 不尝试使用共享类数据
-Xshare:auto 在可能的情况下使用共享类数据(默认)
-Xshare:on 要求使用共享类数据,否则将失败。
-XshowSettings 显示所有设置并继续
-XshowSettings:system (仅限 Linux)显示系统或容器 配置并继续
-XshowSettings:all 显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties 显示所有属性设置并继续
-XshowSettings:locale 显示所有与区域设置相关的设置并继续
注意:导致内存溢出的异常,除了虚拟机的4个区域以外,在NIO会使用直接内存
5 类的使用方式
类的初始化:首次主动使用时,才会初始化
主动使用
- new 构造类的使用
- 访问类的静态成员
特殊情况:- 有final 不会初始化,如果有final是随机数 会初始化
- 反射使用的类 Class.forName(A.class.getName()); 主动使用
- 初始化一个子类时
- 动态代理在执行所涉及的类
6 助记符
反编译
aload_0 装载了一个引用类型
invokespecial init 初始化代码位置
getstatic 获取静态成员
bipush -128<x<127 8位带符号整数
sipush 16位 去掉bipush
无论是定义int 或者 short等
特殊情况 0-5 iconst_(0-5) -1 是iconst_m1
ldc : int float String 常量 ldc2_w long double
7 JVM四种引用级别
Object obj = new Object(); --强引用
根据引用的强弱关系: 强引用>软引用>弱引用>虚引用
四种引用的出处 强 new 软Soft弱Weak虚Phantom Reference
7.1 强引用
Object obj = new Object();
强引用什么时候失效?
- 生命周期结束(作用域失效)
class A {
public void method() {
Object obj = new Object();
} // 当方法执行完毕,强引用指向的new Object()就会等待被回收
}
- 设置为null
obj = null;
除以上两种情况以外gc任何时候都不会回收强引用对象
7.2 软引用
根据JVM内存情况,如果JVM内存不足。GC就会回收软引用对象
7.3 弱引用
GC开始就会回收弱引用对象
7.4 虚引用(幻影引用或幽灵引用)
是否使用虚引用和引用对象本身没有任何关系 无法通过虚引用来获取对象本身。
引用get() -> 引用对象
虚引用get() -> null
虚引用不会单独使用,一般和引用队列一起使用
价值: GC -> 回收 将虚引用放入引用队列中,出队时才会再回该对象 在对象被回收之前,进行一些额外的操作
特殊情况:
对象重写 finalize() 方法 // 虚引用会延迟入队
7.5 引用的作用
使用引用失效缓存的淘汰策略
java - > 缓存(90%-60%)-> db(iphone)
LRU 一般淘汰策略
根据容量/缓存个数+ LRU进行淘汰
在java中还可以用引用失效淘汰策略
8 双亲委派机制
类加载:
- 连接 static静态变量初始化默认值
- 初始化 给static变量赋正确的值
总结:初始化 一定要注意顺序问题 (静态变量 构造方法的顺序问题)
双亲委派:JVM自带的类加载器(在JVM的内部所包含)、用户自定义类加载器
JVM自带的加载器
- 根加载器 Bootstrap - 加载 jre\lib\rt.jar (包含了平时编写代码时 大部分jdk api);指定加载某一个jar( -Xbootclasspath=a.jar)
- 扩展类加载器,Extension:C:\Java\jdk1.8.0_101\jre\lib\ext\*.jar ;指定加载某一个jar(-Djava.ext.dirs= …)
- AppClassLoader/SystemClassLoader,系统加载器(应用加载器):加载classpath;指定加载(-Djava.class.path= 类/jar)
用户自定义加载器(myLoader1 myLoader2)
双亲委派:当一个加载器要加载类的时候,自己先不加载,而是逐层向上交由双亲去加载;当双亲中的某一个加载器 加载成功后,再向下返回成功。如果所有的双亲和自己都无法加载,则报异常。
自定义加载器
二进制名binary names:
"java.lang.String"
"javax.swing.JSpinner$DefaultEditor"
"java.security.KeyStore$Builder$FileBuilder$1"
"java.net.URLClassLoader$3$1"
$代表内部类:
$数字:第几个匿名内部类
The class loader for an array class,
as returned by {@link* Class#getClassLoader()} is
the same as the class loader for its element* type;
if the element type is a primitive type,
then the array class has no* class loader.
1.数组的加载器类型 和数组元素的加载器类型 是相同
2.原声类型的数组 是没有类加载器的
如果加载的结果是null: 可能是此类没有加载器(int[]) , 也可能是 加载类型是“根加载器”
<p> However, some classes may not originate from a file;
they may originate* from other sources, such as the network,
or they could be constructed by an* application.
The method {@link #defineClass(String, byte[], int, int)* <tt>defineClass</tt>}
converts an array of bytes into an instance of class* <tt>Class</tt>.
Instances of this newly defined class can be created using*
{@link Class#newInstance <tt>Class.newInstance</tt>}.
xxx.class文件可能是在本地存在,也可能是来自于网络 或者在运行时动态产生(jsp)
<p> The network class loader subclass must define the methods {@link
* #findClass <tt>findClass</tt>} and <tt>loadClassData</tt> to load a class
* from the network. Once it has downloaded the bytes that make up the class,
* it should use the method {@link #defineClass <tt>defineClass</tt>} to
* create a class instance. A sample implementation is:
*
* <blockquote><pre>
* class NetworkClassLoader extends ClassLoader {
* String host;
* int port;
*
* public Class findClass(String name) {
* byte[] b = loadClassData(name);
* return defineClass(name, b, 0, b.length);
* }
*
* private byte[] loadClassData(String name) {
* // load the class data from the connection
* . . .
* }
* }
如果class文件来自原Network,则加载器中必须重写findClas()和loadClassData().
自定义类加载器的实现
实现流程:
1.public class MyClassLoaderImpl extends ClassLoader
2.findClass(String name){…} :直接复制文档中的NetworkClassLoader中的即可
3.loadClassData(String name){…} :name所代表的文件内容->byte[]
4.细节:
loadClassData(String name): 是文件形式的字符串a/b/c.class,并且开头out.production…
findClass(String name):是全类名的形式 a.b.c.class,并且开头 是: 包名.类名.class
package com.zhj.jvm.test.classload;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @author zhj
*/
public class MyClassLoaderImpl extends ClassLoader {
// 默认是系统加载器
public MyClassLoaderImpl() {
super();
}
// 可以干预加载器
public MyClassLoaderImpl(ClassLoader classLoader) {
super(classLoader);
}
public Class findClass(String name) {
System.out.println(name);
byte[] b = loadClassData(name);
if (b != null) {
return defineClass(name, b, 0, b.length);
}
return null;
}
private byte[] loadClassData(String name) {
name = dotToSplit("target.classes." + name ) + ".class";
File file = new File(name);
byte[] bytes = null;
if (file.exists()) {
FileInputStream fileInputStream = null;
ByteArrayOutputStream outputStream = null;
try {
fileInputStream = new FileInputStream(file);
outputStream = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = -1;
while ((len = fileInputStream.read(buf)) != -1) {
outputStream.write(buf, 0, len);
}
bytes = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bytes;
}
public static String dotToSplit(String className){
return className.replace(".","/") ;
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader myClassLoad = new MyClassLoaderImpl();
Class<?> myClass = myClassLoad.loadClass("com.zhj.jvm.test.classload.MyDefineCL");
System.out.println(myClass.getClassLoader());
System.out.println(myClass.getName());
MyDefineCL o = (MyDefineCL) (myClass.newInstance());
o.say();
}
}
class MyDefineCL{
public void say() {
System.out.println("hello ,world");
}
}
自定义加载 会委派给父类加载
如果加载一个不存在的类,父类都加载不了,就会自己加载,自己抛出异常
使用自己的类加载器
操作思路:
要先将 .class文件从classpath中删除,之后才可能用到 自定义类加载器;否在classpath中的.class会被 APPClassLoader加载
package com.zhj.jvm.test.classload;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @author zhj
*/
public class MyClassLoaderImpl extends ClassLoader {
private String path = null;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
// 默认是系统加载器
public MyClassLoaderImpl() {
super();
}
// 可以干预加载器
public MyClassLoaderImpl(ClassLoader classLoader) {
super(classLoader);
}
public Class findClass(String name) {
System.out.println(name);
System.out.println("findClass...");
byte[] b = loadClassData(name);
if (b != null) {
return defineClass(name, b, 0, b.length);
}
return null;
}
private byte[] loadClassData(String name) {
if (path != null) {
name = path + name.substring(name.lastIndexOf(".") + 1) + ".class";
} else {
name = dotToSplit("target.classes." + name) + ".class";
}
File file = new File(name);
byte[] bytes = null;
if (file.exists()) {
FileInputStream fileInputStream = null;
ByteArrayOutputStream outputStream = null;
try {
fileInputStream = new FileInputStream(file);
outputStream = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = -1;
while ((len = fileInputStream.read(buf)) != -1) {
outputStream.write(buf, 0, len);
}
bytes = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bytes;
}
public static String dotToSplit(String className){
return className.replace(".","/") ;
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoaderImpl myClassLoad = new MyClassLoaderImpl();
// Class<?> myClass = myClassLoad.loadClass("com.zhj.jvm.test.classload.MyDefineCL");
myClassLoad.setPath("E:\\data_file\\");
Class<?> myClass = myClassLoad.loadClass("com.zhj.jvm.test.classload.MyDefineCL");
System.out.println(myClass.getClassLoader());
System.out.println(myClass.getName());
/*MyDefineCL o = (MyDefineCL) (myClass.newInstance());
o.say();*/
}
}
代码流程:
loadClass() -> findClass() -> loadClassData()
一般而言,启动类加载loadClass();
实现自定义加载器,只需要:
1.继承 ClassLoader
2.重写 findClass()
说明,类加载器 只会把同一个类 加载一次; 同一个class文件 加载后的位置
结论:
自定义加载器 加载.class文件的流程:
先委托APPClassLoader加载,APPClassLoader会在classpath中寻找是否存在,如果存在 则直接加载;如果不存在,才有可能交给 自定义加载器加载。
自定义一个与JDK内重名的类,在该类定义一个method01() 系统找不到自己写的方法method01()
根据双亲委派,越上层越先加载,JDK中没有这个方法
双亲委派可以防止自定义类与JDK类重名
存在继承关系
A.class: classpath
B.class: classpath
原因
同一个AppClassLoader 会同时加载A.class和B.class
--
A.class: d:\
B.class: classpath
原因
A.class:自定义加载器加载
B.class:被AppClassLoader加载
因此,加载A.class和B.class的不是同一个加载器
IllegalAccess
---
A.class: classpath
B.class: d:\
NoClassDefFoundError
原因:
A.class: 被AppClassLoader加载
B.class: 自定义加载器加载
因此,加载A.class和B.class的不是同一个加载器
--
A.class d:\
B.class d:\
TestMyClassLoader2 can not access a member of class com.yanqun.parents.A with modifiers "public"
A.class/B.class: 自定义加载器加载
原因是 main()方法所在类在 工程中(APPClassLoader),而A和B不在工程中(自定义加载器)。
造成这些异常的核心原因: 命名空间(不是由同一个类加载器所加载)
----
没有继承关系
X.class: D: 自定义加载器
Y.class: classpath 系统加载器
Y被加载了,加载器是: sun.misc.Launcher$AppClassLoader@18b4aac2
X被加载了,加载器是: com.yanqun.parents.MyClassLoaderImpl@74a14482
---
X.class: classpath 系统加载器
Y.class: D: 自定义加载器
java.lang.NoClassDefFoundError: com/yanqun/parents/Y
--
如果存在继承关系: 继承的双方(父类、子类)都必须是同一个加载器,否则出错;
如果不存在继承关系: 子类加载器可以访问父类加载器加载的类(自定义加载器,可以访问到 系统加载器加载的Y类);反之不行(父类加载器 不能访问子类加载器)
核心: 双亲委派
如果都在同一个加载器 ,则不存在加载问题; 如果不是同一个,就需要双亲委派。
如果想实现各个加载器之间的自定义依赖,可以使用ogsi规范
OSGi:
1.网状结构的加载结构
2.屏蔽掉硬件的异构性。例如,可以将项目部署在网络上,可以在A节点上 远程操作B节点。在操作上,可以对硬件无感。也可以在A节点上 对B节点上的项目进行运维、部署,并且立项情况下 在维护的期间,不需要暂时、重启。
类的卸载
1.系统自带(系统加载器、扩展加载器、根加载器):这些加载器加载的类 是不会被卸载。
2.用户自定义的加载器,会被GC卸载
9 JVM监测工具
jps: 查看Java进程 (java命令)
jstat:只能查看当前时刻的内存情况;可以查看到 新生代、老年代中的内存使用情况
jmap:查看堆内存的占用情况;也可以执行dump操作
jconsole:图形的监控界面
例如:如果通过jconsole中的"执行gc"按钮发现 GC回收的内存太少,就说明当前进程是存在问题的(至少是可以被优化的)
jvisualvm: 监视 - 堆Dump -查找最大对象,从中可以发现 当前进程中是哪个对象 占据了最大的内存,从而对这个对象进行分析。
通过VM参数实现: 当内存溢出时,自动将溢出时刻的内存dump下来。
-Xmx100m
-Xms100m
-XX:+HeapDumpOnOutOfMemoryError
10 GC调优
Java开发者为什么不把所有的参数调到最优?非得让我们手工去调?
取舍。
调优实际是是一种取舍,以xx换xx的策略。因此在调优之前,必须明确方向:低延迟?高吞吐量呢?
有两种情况需要考虑:
1.在已知条件相同的前提下, 牺牲低延迟 来换取 高吞吐量,或者反之。
2.随着软件硬件技术的发展,可能 二者都升高。
GC的发展:
JVM自身在GC上进行了很多次的改进升级:
JVM默认的GC: CMS GC(在jdk9以后被逐步废弃) -> G1 GC(jdk9) -> Z GC(jdk11)
-
Serial GC:
串行GC,是一种在单核环境下的串行回收器。当GC回收的时刻,其他线程必须等待。一般不会使用。
-
Parallel GC:
在Serial 的基础上,使用 了多线程技术。 提高吞吐量。
-
CMS GC
CMS使用了多线程技术,使用“标记-清除”算法,可以极大提升效率 (尤其在低延迟上有很大的提升)。繁琐,参数太多,对开发者的经验要求太高。
-
G1 GC
jdk9开始使用的默认GC。特点:将堆内存划分为很多大小相等region,并会对这些区域的使用状态进行标记。以便GC在回收时,能够快速的定位出哪些region是空闲的,哪些是有垃圾对象,从而提升GC的效率。G1使用的算法是“标记-整理”算法。
-
Z GC
jdk11开始提供全新的GC。回收TB级别的垃圾 在毫秒范围。
如果从生命周期角度划分,GC也可以划分成:Minor GC,和Full GC
- Minor GC:回收新生代中的对象
- Full GC:回收整个堆空间(新生代、老年代)
案例:
如果通过监测工具发现: Minor GC和Full GC都在频繁的回收,如何优化?
Minor GC为什么会频繁执行?因为 新生代中的对象太多了 Minor GC->短生命周期的对象太多了->造成逃逸到老年代中的对象越多-> 新生代多+老年代多->Full GC
Minor GC:可以尝试调大 新生代的最大空间
再调大 新生代晋升到老年代的阈值,从而降低 短生命周期的对象 从新生代转移到老年代的概率。