jvm学习笔记
java虚拟机与程序的生命周期
在如下几种情况下,java虚拟机将结束生命周期:
-
执行了System.exit()方法
-
程序正常执行结束
-
程序在执行过程中遇到了异常或错误而异常终止
-
由于操作系统出现错误而导致java虚拟机进程结束
加载.class文件的方式:
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将java原文件动态编译为.class文件
线程运行诊断
- Cpu 占用过多
- 用top定位那个进程对cpu的占用过高。
- ps H -eo pid,tid,%cpu | grep 进程id(用ps进一步定位是那个线程引起的cpu占用过高)
-
Jstack 进程id
- 可以根据线程id找到问题的线程,进一步定位问题代码的源码行数。
-
堆内存
- jps 查看当前系统中有哪些java进程
- Jmap 查看堆内存占用情况 jmap -heap 进程id
-
Jconsole 图形界面
-
StringTable特性
-
常量池中的字符串仅是符号,第一次用到时才变成对象。
- 利用串池的机制,来避免重复创建字符串对象。
- 字符串拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译器优化。
- 可以使用intern方法,主动将串池中还没有的字符串放入串池
- 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池;两种情况都会把串池中的对象返回。
- 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份放入串池;两种情况都会把串池中的对象返回。
- 性能调优
- 调整-XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
四种引用
-
强引用
只有所用GC Roots 对象都不强引用该对象,该对象才能被垃圾回收。
-
软引用(SoftReference)
-
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次发出垃圾回收,回收软引用对象
-
可以配合引用队列释放软引用对象自身
java //-Xmx20m -XX:+PrintGCDetails -verbose:gc public static void soft(){ List<SoftReference<byte[]>> list = new ArrayList<>(); ReferenceQueue<byte[]> queue = new ReferenceQueue(); for (int i = 0; i < 5; i++) { //关联了引用队列,当软引用关联的byte[]被回收时,软引用自己会加入到queue中去 SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB],queue); System.out.println(ref.get()); list.add(ref); System.out.println(list.size()); } //从队列中获取无用的软引用对象并移除 java.lang.ref.Reference<? extends byte[]> ref = queue.poll(); while (ref != null){ list.remove(ref); ref = queue.poll(); } System.out.println("循环结束:"+list.size()); System.out.println("============================"); for (SoftReference<byte[]> softReference : list) { System.out.println(softReference.get()); } }
-
-
弱引用(WeakReference)
-
只有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
-
可以配合引用队列释放弱引用对象自身
java public static void weak(){ List<WeakReference<byte[]>> list = new ArrayList<>(); //ReferenceQueue<byte[]> queue = new ReferenceQueue(); for (int i = 0; i < 6; i++) { //关联了引用队列,当软引用关联的byte[]被回收时,软引用自己会加入到queue中去 WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]); System.out.println(ref.get()); list.add(ref); for (WeakReference<byte[]> weakReference : list) { System.out.println(weakReference.get()+" "); } System.out.println(list.size()); } //从队列中获取无用的软引用对象并移除 // java.lang.ref.Reference<? extends byte[]> ref = queue.poll(); // while (ref != null){ // list.remove(ref); // ref = queue.poll(); // } System.out.println("循环结束:"+list.size()); System.out.println("============================"); }
-
-
虚引用(PhantomReference)
- 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存。
-
终结器引用(FinalReference)
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用他的finalize方法,第二次GC时才能回收被引用对
## 垃圾回收
-
相关VM参数
含义 参数 堆初始大小 -Xms 堆最大大小 -Xmx或-XX:MaxHeapSize=size 新生代大小 -Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size) 幸存区比例(动态) -XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy 幸存区比例 -XX:SurvivorRatio=ratio 晋升阈值 -XX:MaxTenuringThreshold=threshold 晋升详情 -XX:+PrintTenuringDistribution GC详情 -XX:+PrintGCDetails -verbose:gc FullGC前minorGC -XX:+ScavengeBeforeFullGC -
特性
-
大对象直接放到老年代。
-
线程中的OOM不会引起java进程的结束。
```java public class Gc { private static final int _512KB = 512 * 1024; private static final int _1M = 1 * 1024 * 1024; private static final int _6M = 6 * 1024 * 1024; private static final int _7M = 7 * 1024 * 1024; private static final int _8M = 8 * 1024 * 1024; public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { List
list = new ArrayList<>(); list.add(new byte[_8M]); list.add(new byte[_8M]); } }).start(); System.out.println("主线程睡一秒钟"); Thread.sleep(1000l); }
} ```
-
-
垃圾回收器
-
串行
- 单线程
- 堆内存较小,适合个人电脑
- -XX:+
-
吞吐量优先
-
多线程
-
堆内存较大,多核CPU
-
单位时间内让STW的时间最短
shell -XX:+UseParallelGC(开启后会自动使用parallelOldGC) -XX:+UseParallelOldGC -XX:ParallelGCThreads=n (设置垃圾回收线程数,默认和cpu核数相同) -XX:UseAdaptiveSizePolicy 自适应调整新生代大小 -XX:GCTimeRatio=ratio 1/1+ratio(默认为99,一般设置为19,表示100分钟内允许5分钟的时间可以用来进行垃圾回收) 为垃圾回收时间所占的比例 -XX:MaxGCPauseMillis=ms 每次垃圾回收使用的最大时间
-
-
响应时间优先
-
多线程
-
堆内存较大,多核CPU
-
尽可能让STW的单次时间最短
shell -XX:+UseConcMarkSweepGC(老年代) --> 如果失败,会退化成SerialOld垃圾回收器 -XX:UseParNewGC (新生代) -XX:ParallelGCThreads=n (设置垃圾回收线程数,默认和cpu核数相同) -XX:ConcGCThreads=n/4 -XX:CMSInitiatingOccupancyFraction=percent 执行垃圾回收的内存占比 -XX:+CMSScavengeBeforeRemark
-
-
G1(Garbage First 取代cms回收器)
-
2017年JDK9 默认的垃圾回收器
-
适用场景
-
同时注重吞吐量和低延迟,默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region
-
整体上是标记+整理算法,两个区域之间是复制算法
-
配置
shell -XX:+UseG1GC -XX:G1HeapRegionSize=size -XX:MaxGCPauseMillis=time
-
-
垃圾回收调优
#### 新生代调优
- 新生代特点
- 所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- 死亡对象的回收代价为0
- 大部分对象用过即死
- Minor GC 的时间远远低于Full GC
- 所有的new操作的内存分配非常廉价
- 调优
- 新生代能容纳所有[并发量*(请求-响应)]的数据
class文件结构
shell
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
## 执行流程
java
package test;
public class HelloWorld {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
javap 查看字节码
```java
Classfile /Users/haominglfs/IdeaProjects/leetcode/out/production/leetcode/test/HelloWorld.class
Last modified 2020-1-10; size 602 bytes
MD5 checksum 470a732b94645485adf09a48f8e9bdd2
Compiled from "HelloWorld.java"
public class test.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 4: 0
line 5: 3
line 6: 6
line 7: 10
line 8: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
} SourceFile: "HelloWorld.java" ```
图解分析
-
加载常量池到运行时常量池,方法字节码加载到方法区
-
bipush 10
-
将一个byte压入操作数栈(其长度会补齐4个字节)
-
sipush将一个short压入操作数栈
-
ldc 将一个int压入操作数栈
-
Ldc2_w 将一个long压入操作数栈(分两次压入,因为long是两个字节)
-
这里小的数字都是和字节码指令存在一起,超过short范围的数字存入常量池
-
-
Istore_1 将操作数栈顶数据弹出,存入局部变量表的slot1
-
ldc #3 从常量池加载#3数据到操作数栈
-
istore_2
-
load_1
-
Iload_2
-
iadd
-
Istore_3
-
getstatic #4
将堆中的System.out对象的引用压入操作数栈
-
iload_3
-
Invokevirtual #5
-
找到常量池第五项
-
定位到方法区java/io/PrintStream.println:(I)V方法
-
生成新的栈帧(生成locals、stack等)
-
传递参数,执行新栈帧中的字节码
-
执行完毕,弹出栈帧
-
清除main操作数栈内容
-
-
return
- 完成main方法调用,弹出main栈帧
- 程序结束
构造方法
()V
java
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
编译器会按照从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法
java
0: bipush 10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return
()V
编译器会按照从上至下的顺序,收集所有{}代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后
- 方法调用
```java public class Demo { public Demo(){} private void test1(){} private final void test2(){} public void test3(){} public static void test4(){}
public static void main(String[] args) {
Demo d = new Demo();
d.test1();
d.test2();
d.test3();
d.test4();
Demo.test4();
}
} ```
java
0: new #2 // class test/Demo 堆空间分配内存,引用放入操作数栈
3: dup //复制栈顶引用
4: invokespecial #3 // Method "<init>":()V 调用构造方法
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop //入栈又出栈,通过对象调用静态方法会产生两次不必要的指令
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return
构造方法、私有方法、final方法使用invokespecial;普通public方法使用invokevirtual(动态绑定);静态方法使用invokestatic
- 多态
当执行invokevirtual指令时
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际Class
- class结构中有vtable,他在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
-
执行方法的字节码
-
异常
java
public class Demo {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e){
i = 20;
}
}
}
java
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
可以看出多出来一个Exception table的结构,[from,to)是前闭后开的监测范围,一旦这个范围内的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入target所指示行号。
8行的字节码指令astore_2是将异常对象引用存入局部变量表的slot 2 位置。
类加载
加载步骤
- 加载
-
链接
-
验证
-
准备(为static变量分配空间,设置默认值)
- static 变量在JDK7之前存储在instanceKlass末尾,从JDK7开始,存储于java_mirror的末尾(堆中)
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果变量是final的基本类型或字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
-
解析
符号引用解析为直接引用
-
初始化
-
初始化即调用
()V,虚拟机会保证这个类的[构造方法]的线程安全 -
初始化时机
-
主动使用(六种):
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如 Class.forName("info.haominglfs.test"))
- 初始化一个类的子类
- java虚拟机启动时被表明为启动类的类(含有main方法)
- 被动使用,除了以上六种情况,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化
- 访问类的static final静态常量(基本类型和字符串)不会触发初始化
- 类对象.class不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的loadClass方法
- Class.forName的参数2为false时
-
所有的java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化他们,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化
-
单例例子
java class Singleton { //懒惰式单例模式 private Singleton() { } //1.首次使用时才会触发初始化 //2.静态类可以访问外部类的构造方法 private static class HolderClass { private final static Singleton instance = new Singleton(); } //由类加载器来保证线程安全 public static Singleton getInstance() { return HolderClass.instance; } }
-
类加载器
- 以jdk8为例:
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Application |
- 双亲委派
```java 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) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader }
if (c == null) {
//4.每一层找不到,调用findClass方法(每个类加载器自己扩展)来加载
long t1 = System.nanoTime();
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;
}
} ```
- 线程上下文类加载器
是当前线程使用的类加载器
```java public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
/* Prevent the DriverManager class from being instantiated. */
private DriverManager(){}
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
//初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
//2.使用jdbc.drivers定义的驱动名加载驱动
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
//这里的ClassLoader.getSystemClassLoader()就是应用程序类加载器
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
public static <S> ServiceLoader<S> load(Class<S> service) {
//线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
```
-
自定义类加载器
-
什么时候需要自定义类加载器
- 想加载非classpath随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计中
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
-
步骤
- 继承ClassLoader父类
- 要遵从双亲委派模型,重写findClass方法(不是重写loadClass方法,否则不会走双亲委派机制)
- 读取类文件的字节码
- 调用父类的defineClass方法来加载类
- 使用者调用该类加载器的loadClass方法
-
例子
java public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String path = "/myclasspath/"+name+".class"; try { ByteOutputStream os = new ByteOutputStream(); Files.copy(Paths.get(path),os); byte[] bytes = os.toByteArray(); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("类文件未找到"); } } }