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)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次发出垃圾回收,回收软引用对象
可以配合引用队列释放软引用对象自身
//-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)
只有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列释放弱引用对象自身
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进程的结束。
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<byte[]> list = new ArrayList<>(); list.add(new byte[_8M]); list.add(new byte[_8M]); } }).start(); System.out.println("主线程睡一秒钟"); Thread.sleep(1000l); } }
垃圾回收器
串行
- 单线程
- 堆内存较小,适合个人电脑
- -XX:+
吞吐量优先
多线程
堆内存较大,多核CPU
单位时间内让STW的时间最短
-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的单次时间最短
-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
- 整体上是标记+整理算法,两个区域之间是复制算法
配置
-XX:+UseG1GC -XX:G1HeapRegionSize=size -XX:MaxGCPauseMillis=time
垃圾回收调优
新生代调优
- 新生代特点
- 所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- 死亡对象的回收代价为0
- 大部分对象用过即死
- Minor GC 的时间远远低于Full GC
- 所有的new操作的内存分配非常廉价
- 调优
- 新生代能容纳所有[并发量*(请求-响应)]的数据
class文件结构
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];
}
执行流程
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 查看字节码
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."<init>":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#6 = Class #31 // test/HelloWorld
#7 = Class #32 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Ltest/HelloWorld;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 HelloWorld.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 test/HelloWorld
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (I)V
{
public test.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltest/HelloWorld;
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 static int i = 10; static { i = 20; } static { i = 30; }
编译器会按照从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法
()V 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方法会在类加载的初始化阶段被调用 ()V 编译器会按照从上至下的顺序,收集所有{}代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后
方法调用
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(); } }
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,他在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
- 执行方法的字节码
异常
public class Demo { public static void main(String[] args) { int i = 0; try { i = 10; }catch (Exception e){ i = 20; } } }
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类的方式都被看做是对类的被动使用,都不会导致类的初始化
- 主动使用(六种):
单例例子
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 双亲委派
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; } }
线程上下文类加载器
是当前线程使用的类加载器
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<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } //1.使用serviceLoader机制加载驱动,即SPI AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { 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方法
例子
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("类文件未找到"); } } }