# jvm-study **Repository Path**: wlyfree/jvm-study ## Basic Information - **Project Name**: jvm-study - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2019-03-11 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Java虚拟机 **JVM**:Java Virtual Machine,使用c++编写。并非直接关联物理硬件,而是运行在OS上层的Java虚拟机 ![体系结构](JVM体系结构.png) ### 本地方法接口 融合不同的语言为Java所用 ### 执行引擎 负责解释命令,提交给操作系统执行。 ### 程序计数器 线程私有,就是一个指针,指向方法区中方法字节码(用来存储指向下一条指令的地址,即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。 ### 本地方法栈 记录native方法,在执行引擎执行时,加载本地方法库。 ### 虚拟机栈 线程私有,随线程创建和销毁,生命周期与线程一致。**不存在垃圾回收的问题。** **栈内分配:8大基本类型变量 + 引用类型变量 + 实例方法** 存储内容:本地变量:方法输入、输出参数、方法内变量 栈操作:入栈、出栈 ### 方法区 线程间共享,方法区是一个接口级的概念,在不同的jdk版本中有不同的实现。jdk6、jdk7称之为永久代,jdk6在堆内,jdk7在堆外,jdk8称之为元空间,在堆外。 存储内容:虚拟机加载的Class、Interface的元数据 + 普通常量 + 静态变量 + 编译器编译后的代码 常量池:静态常量池和运行期常量池。 静态常量池:class文件中用于存放编译期生成的各种字面量和符号引用 运行期常量池:属于方法区的一部分,将静态常量池信息加载到运行期常量池中,并能动态加载常量,如String.intern() ### 堆 堆由新生代、年老代、方法区组成 新生代和年老代的默认比例:1:2 新生代:1个Eden区、2个survivor区,默认比例8:1:1 方法区在JDK1.8的实现为MetaSpace元空间 ![JDK1.8的堆](堆.png) ------ ## 对象创建流程 ### 创建流程 #### 定位类符号引用 首先在常量池中定位类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。若为加载,则需要执行类的加载过程。 #### 类加载、验证、初始化 > 详见下面的类加载章节 #### 为对象分配内存 分配内存的大小在类加载完成后便可确定。 ##### 分配方式 ###### 指针碰撞 ![](指针碰撞.png) 假定内存中数据是连续的,有一个指针记录相交的位置(或者对象),每次分配内存都顺序分配,移动指针。 ###### 空闲列表 ![](空闲列表.png) 每次分配时,都将分配的情况记录到一张表中。 ##### 内存分配的并发问题 ###### CAS 分配失败后重试,基于CAS保障分配内存的原子性。 ###### TLAB(Thread Local Allocation Buffer) 线程本地分配缓冲区,为每个线程预留一个内存分配的位置,哪个线程要分配内存,就在哪个线程的TLAB线程上分配。当TLAB用完并分配新的TLAB时,需要进行同步锁定。 **是否使用TLAB**:-XX:+/-UseTLAB #### 初始化零值 保证对象的实例字段在JAVA代码中不赋初始值也能使用。 #### 设置对象头 设置这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、GC分代年龄等。 #### 调用方法 按照程序员的意愿执行初始化。 ### 对象的结构 #### 对象头 ##### Mark word ![](Mark Word.png) ##### 类信息的指针 指向方法区的类元数据的指针,通过这个指针来确定对象是哪个类的实例 ##### 数组长度 只有数组有 #### 实例数据 对象真正存储的有效信息 #### 对象填充 若实例数据未对齐,使用对象填充来补全,无实际意义。 ### 对象的访问定位 #### 指针(Hotspot) #### 句柄 ### 堆 + 栈 + 方法区之间的关系 ![](堆-栈-方法区的关系.png) ------ ## 异常 ### 栈(虚拟机栈和本地方法栈) 虚拟机栈、本地方法栈都属于栈区,存在两种内存异常的场景StackOverflowError、OutOfMemoryError #### StackOverflowError ```java package com.wang.memoryexception.stackmemory; /** * 栈异常测试:StackOverFlowError * 递归层级过多容易出现这个问题 */ public class StackOverFlowErrorTest { public static void main(String[] args) { // 递归深度过大导致的StackOverFlow stackOverFlowError(); } /** * 无限递归 */ private static int stackOverFlowError(){ return stackOverFlowError(); } } ``` ```java // 控制台 Exception in thread "main" java.lang.StackOverflowError at com.wang.memoryexception.stackmemory.StackOverFlowErrorTest.stackOverFlowError(StackOverFlowErrorTest.java:16) at com.wang.memoryexception.stackmemory.StackOverFlowErrorTest.stackOverFlowError(StackOverFlowErrorTest.java:16) ... ``` #### OutOfMemoryError **windows上可能导致OS假死看不到效果,建议在linux上测试** ```java import java.util.concurrent.TimeUnit; /** * 栈异常测试:OutOfMemoryError * 频繁创建线程可能会导致栈OutOfMemoryError * 此示例建议在linux上测试,windows可能造成OS假死 */ public class StackOutOfMemoryErrorTest { public static void main(String[] args) { // OutOfMemory,windows可能导致os假死,在linux上能试出效果 outOfMemoryError(); } /** * 栈内存不足 */ private static void outOfMemoryError() { while(true){ new Thread(()->{ try { TimeUnit.HOURS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } } } ``` ```java // 控制台 Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:717) at StackErrorTest.outOfMemoryError(StackErrorTest.java:28) at StackErrorTest.main(StackErrorTest.java:14) ``` ### 堆 ```java jvm配置:-verbose:gc -Xms20m -Xmx20m ``` ```java package com.wang.memoryexception.heapmemory; /** * 堆内存溢出测试,直接分配超过年老代的大小 */ public class HeapErrorTest { public static void main(String[] args) { byte[] bytes = new byte[1024 * 1024 * 50]; } } ``` ```java // 控制台 [GC (Allocation Failure) 2102K->800K(19968K), 0.0017342 secs] [GC (Allocation Failure) 800K->816K(19968K), 0.0008004 secs] [Full GC (Allocation Failure) 816K->731K(19968K), 0.0090660 secs] [GC (Allocation Failure) 731K->731K(19968K), 0.0003733 secs] [Full GC (Allocation Failure) 731K->713K(19968K), 0.0107694 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.wang.memoryexception.heapmemory.HeapErrorTest.main(HeapErrorTest.java:8) ``` ### 方法区 方法区溢出的场景:分配的静态变量常量太多或者加载的类信息太多溢出 ```java jvm配置:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m ``` ```java package com.wang.memoryexception.methodareamemory; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * 方法区OutOfMemory测试 * 利用CGlib实现Metaspace的溢出 */ public class MethodAreaErrorTest { public static void main(String[] args) { while(true){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(o,args); } }); enhancer.create(); } } static class OOMObject{ } } ``` ```java // 控制台 Exception in thread "main" java.lang.OutOfMemoryError: Metaspace at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:467) at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339) at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492) at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117) at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294) at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480) at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305) at com.wang.memoryexception.methodareamemory.MethodAreaErrorTest.main(MethodAreaErrorTest.java:25) ``` ### 直接内存 ```java jvm配置:-Xmx20M -XX:MaxDirectMemorySize=10M ``` ```java package com.wang.memoryexception.directmemory; import sun.misc.Unsafe; import java.lang.reflect.Field; public class DirectErrorTest { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws IllegalAccessException { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while(true){ // 直接操作堆外内存 unsafe.allocateMemory(_1MB); } } } ``` ```java // 控制台 Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at com.wang.memoryexception.directmemory.DirectErrorTest.main(DirectErrorTest.java:15) ``` ## 垃圾回收 ### 如何判断垃圾 #### 引用计数法(reference couting) ![](引用计数.png) 给每个对象添加一个引用计数器,对象被引用,计数器+1,引用失效,计数器-1。 存在A-B循环引用的问题,因此没有到目前版本的JVM中。 #### 可达性分析算法(GC Roots) ![](可达性分析.png) 从GC Roots作为起点开始搜索,能连通的对象就是存活对象,不能连通的对象就是垃圾。 如图obj4和obj6相互引用,但是从GC Roots到obj4、obj6是不可达的,垃圾回收的时候obj4和obj6会被作为垃圾回收掉。 ##### 什么样的对象是GC Roots? 方法区中常量、静态变量引用的对象 虚拟机栈(栈帧中的局部变量,也叫局部变量表)引用的对象 本地方法栈JNI(Native方法)中引用的对象 ##### 验证JVM对相互引用对象的回收 ```java // 参数配置 -XX:+PrintGCDetails ``` ```java package com.wang.gc.test; import com.wang.gc.domain.A; import com.wang.gc.domain.B; import java.util.concurrent.TimeUnit; /** * 验证循环引用垃圾回收问题 */ public class LoopReferenceGC { public static void main(String[] args) throws InterruptedException { test(); TimeUnit.MINUTES.sleep(1); } /** * 测试循环引用 */ public static void test(){ A a = new A(); B b = new B(); a.setB(b); b.setA(a); System.gc(); } } ``` ```java // 控制台GC日志 [GC (System.gc()) [PSYoungGen: 2662K->840K(38400K)] 2662K->848K(125952K), 0.0031939 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 840K->0K(38400K)] [ParOldGen: 8K->733K(87552K)] 848K->733K(125952K), [Metaspace: 3438K->3438K(1056768K)], 0.0090100 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] ``` ### 垃圾回收的算法 #### 复制算法 将内存分为等大小的两块,垃圾回收时,将对象复制到另一块区域,简单高效,没有碎片问题。缺点:比较浪费内存。 #### 标记-清除算法 分为标记、清除两个阶段。首先标记要回收的对象,在标记完成后统一回收到被标记的对象。 缺点: 1. 标记、清除的效率都不高。 2. 可能产生大量不连续的内存碎片,在下一次分配大对象时,可能由于空间不足不得不触发一次垃圾回收动作。 #### 标记-整理算法 在标记-清除算法上做了改进,标记完成后并非清除掉这些对象,而是将存活的对象向一端移动,在移动的过程中清除掉可以回收的对象,这个过程叫整理。 优缺点: 相对标记-清除算法,没有内存碎片的问题。 效率与对象存活率有关,若对象存活率较低,则效率较低。 #### 分代垃圾回收 通常,实际情况是根据JVM各区情况不同采用不同的垃圾回收器。 ### 回收算法的实现 #### 枚举根节点 需要对GC Roots进行分析,GC Roots一般在栈帧的本地变量表或方法区中,有的应用方法区很大,这个时候枚举根节点会非常的耗时。虚拟机使用一种OopMap的结构解决这个问题,在JIT编译过程中会在特定的位置(安全点)记录下栈和寄存器中那些位置是引用,这样在GC扫描的时候就可以直接得知这些信息了。 ### 回收时间点 #### 安全点 一般具有“让程序长时间运行的特征”,如:方法调用、循环跳转、异常跳转等。 安全点太多会增大系统负荷,安全点太少会导致GC等待时间过长。 **如何让GC发生在所有线程都跑到安全点附近?** 抢占式中断:无需线程代码配合,GC时,直接中断所有线程(不友好) 主动式中断:设置一个标识,让线程主动轮询这个标识 #### 安全区域 在一段代码中,引用关系不会发生变化,这个区域内都是安全区域,可以看做是安全点的一个扩展。 ### 垃圾回收器 #### 名词介绍 **STW**:Stop the world,当垃圾回收时,应用线程全部暂停。 #### 垃圾回收器 ##### Serial 单线程的垃圾收集器,用于新生代,采用”复制“算法。 ##### Serial Old 同Serial,单线程的垃圾收集器,用于老年代,采用”标记-整理“算法。 ![Serial垃圾收集器](Serial、Serial Old垃圾回收器示意图.png) ##### ParNew Serial的多线程版本,同Serial一样,用于新生代,采用”复制算法“。 ![ParNew垃圾回收器示意图](ParNew垃圾回收器示意图.png) ##### Parallel Scavenge 并行的多线程收集器,仅用于新生代,采用“复制”算法,**不能与CMS共用**。 ##### Parallel Old Parallel Scavenge的老年代版本,仅用于老年代,采用“标记-整理算法”。 关注点:**吞吐量**。吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) ![Parallel Scavenge、Parallel Old垃圾收集器示意图](Parallel Scavenge、Parallel Old垃圾收集器示意图.png) ##### CMS(Concurrent mark Sweep) 作用于年老代,采用“标记-清除”算法,并发标记回收,低停顿。其中在初始标记、重新标记过程中会产生STW。 ###### 过程 **初始标记(CMS initial mark)**:标记GC Roots,停顿时间较短。 **并发标记(CMS concurrent mark)**:与用户线程并发执行,针对GC Roots做可达性分析。 **重新标记(CMS remark)**:修正并发标记阶段因用户线程执行而导致的标记变化。 **并发清除(CMS concurrent sweep)**:清理垃圾。 ###### 缺点 1. 对CPU资源比较敏感,并发标记阶段虽然不会让用户线程暂停,但是会占用很多CPU资源,从而导致CPU运算能力降低,吞吐量下降。 2. 无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。 什么是浮动垃圾?在并发清除阶段,用户线程还在运行中,也就是说GC线程标记过后,用户线程还会产生一部分垃圾。这部分垃圾在本次GC不会被回收,只能等下次GC时回收。所以使用CMS不能等年老代快满了再回首,需要预留一部分空间给用户线程使用。 3. 可能产生大量的空间碎片,标记-清除算法的问题。如果有大量碎片,分配一个需要占用连续空间的大对象就可能会失败,导致Full GC的产生。 ![CMS垃圾回收器示意图](CMS垃圾回收器示意图.png) ##### G1 版本:JDK1.7+ 作用于新生代和年老代,模糊新生代和年老代的界限,将整个堆内存做逻辑分区,分出多个等大小的Region。跨Region的垃圾回收使用复制算法,Region内垃圾回收使用“标记-整理”算法。 ###### 过程 **初始标记(Initial Marking)**:标记GC Roots,停顿时间较短。 **并发标记(Concurrent Marking)**:与用户线程并发执行,针对GC Roots做可达性分析。 **最终标记(Final Marking)**:修正并发标记阶段因用户线程执行而导致的标记变化。 **筛选回收(Live Data Counting Evacuation)**:对Region的回收价值和成本做排序,然后根据用户期望的GC时间做回收计划,默认需要STW,据说也可以做到与用户线程并发执行(对G1不熟悉,真实用到的话待考证)。 ###### 优点 1. 并发和并行:类似CMS,支持GC线程与用户线程并发操作,减少停顿。 2. 分代收集:针对Region内部和外部采用不同的算法实现。 3. 空间整合:区别于CMS,Region内部基于“标记-整理算法”,跨Region使用复制算法,这两种算法都不会产生空间碎片,不会导致Fu'll GC的提前产生。 4. 可预测的停顿:支持预测停顿。 ![G1垃圾回收器示意图](G1垃圾回收器示意图.png) ##### ZGC 版本:JDK11+ 特性:GC停顿时间不会超过10ms #### 组合使用情况 - Serial可以与Serial Old或CMS一起使用,Serial一般用于单核机器,省去多线程切换开销。 - ParNew可以与Serial Old、Parallel Old、CMS一起使用。 - Parallel Scavenge可以与Serial Old、Parallel Old一起使用。 **注意:Serial Old(MSC)可以作为CMS回收失败(Concurrent Mode Failture)的备用垃圾回收器。** ![](E:\myself\jvm-study\垃圾回收器组合使用情况.png) ### 垃圾回收日志 ![GC日志](GC日志.jpg) ![Full GC日志](Full GC日志.jpg) ### 内存分配策略 分配规则受很多因素影响,区别于:垃圾回收器组合、JVM中和内存相关的参数配置等。 #### 一般场景下的内存分配优先级 栈上分配 =》TLAB =》Eden区分配 =》老年代 栈上分配:JIT编译后被拆散为标量类型并间接地栈上分配。 TLAB:需要启用TLAB。 Eden区分配:分配内存小于Eden区内存,若内存不足则触发一次Minor GC再尝试分配到Eden区,若分配失败则进入年老代。 Old区分配:Eden区分配失败、动态年龄分配、GC存活年龄达到阈值、超过Eden区大小的大对象。 ### 空间分配担保 作用:防止Full GC过于频繁 在Minor GC之前,虚拟机会检查年老代的最大可用连续空间是否大于所有新生代的总空间,若成立则Minor GC是安全的,因为就算新生代所有对象都存活晋升到年老代也没问题! 若不成立,则需要看是否配置了空间分配担保?若开启了,则检查年老代的最大可用连续空间是否大于历次晋升到年老代对象的平均大小,若大于,则尝试进行Minor GC。若Minor GC后需要晋升到老年代则可能会触发一次Full GC。 若没有开启空间分配担保或者最大可用连续空间小于年老代对象的平均大小,则直接进行Full GC。 解释:采用相对安全的Minor GC的方式,尽量减少Full GC的场景。 ------ ## Class文件 ### 概念 class文件:字节码文件,是一组以8个字节为基础单位的二进制流,可以直接被java虚拟机加载执行。 注:并非只有java文件能编译成字节码文件,JRuby、Groovy等都可以,编译出来的class文件都可以被java虚拟机加载执行。 ### Class文件结构 #### 数据结构 只包含无符号数和表。 **无符号数**属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。 **表**是由多个无符号数或者其他表作为数据项构成的符合数据类型。 #### 文件内容 ##### 魔数 固定值,标记是不是一个可以被jvm解析的有效class文件。 ##### 主版本号 标识编译jdk的主版本号 ##### 次版本号 标识编译jdk的副版本号 ##### 常量池 ###### 字面量 文本字符串、常量值等 ###### 符号引用 - 类和接口的全限定名 - 字段的名称和描述符 - 方法的名称和描述符 ##### 访问标志 标识是一个类、接口或者枚举等,是否是final、是否是abstract类型 ##### 类索引、父类索引与接口索引集合 ###### 类索引 标识类的全限定名 ###### 父类索引 标识父类的全限定名 ###### 接口索引集合 描述类实现哪些接口 ##### 字段表集合 标识类或接口中的静态变量、实例变量,不包含局部变量。包含访问标识符、名称索引、描述索引、属性表集合等。 ##### 方法表集合 同字段表集合标识的内容基本一致。 ##### 属性表集合 Class文件、字段表、方法表中使用的属性表集合。 ### Class文件格式 **Java源码** ```java public class Test { public static final String HELLO_WORLD = "hello world"; public String haha = "你好"; public static void main(String[] args) { int a = 1; int b = 1; if(a == 1 ){ System.out.println("a = 1"); }else if(b == 1){ System.out.println("b = 1"); }else{ System.out.println("else"); } while(a-- > 0){ System.out.println("a自减"); } } } ``` #### 使用16进制编辑器查看(如WinHex) ![](WinHex查看class文件.png) **解读16进制文件**: 魔数(Magic Number):0~3固定字符,标识这个文件是否是能被虚拟机接受的class文件。 Class文件版本:用于标识class文件编译版本,4~5字节标识主版本号,6~7字节标识次版本号 常量池容量 常量标志位(常量类型) 常量结构 #### 使用java命令查看 ```shell javap -verbose Test.class ``` ```shell Classfile /C:/Users/Administrator/Desktop/Test.class Last modified 2019-3-20; size 696 bytes MD5 checksum d1f66c7b93efc862ea933f76bba6980f Compiled from "Test.java" public class Test minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #11.#26 // java/lang/Object."":()V #2 = String #27 // 你好 #3 = Fieldref #10.#28 // Test.haha:Ljava/lang/String; #4 = Fieldref #29.#30 // java/lang/System.out:Ljava/io/PrintStream; #5 = String #31 // a = 1 #6 = Methodref #32.#33 // java/io/PrintStream.println:(Ljava/lang/String;)V #7 = String #34 // b = 1 #8 = String #35 // else #9 = String #36 // a自减 #10 = Class #37 // Test #11 = Class #38 // java/lang/Object #12 = Utf8 HELLO_WORLD #13 = Utf8 Ljava/lang/String; #14 = Utf8 ConstantValue #15 = String #39 // hello world #16 = Utf8 haha #17 = Utf8 #18 = Utf8 ()V #19 = Utf8 Code #20 = Utf8 LineNumberTable #21 = Utf8 main #22 = Utf8 ([Ljava/lang/String;)V #23 = Utf8 StackMapTable #24 = Utf8 SourceFile #25 = Utf8 Test.java #26 = NameAndType #17:#18 // "":()V #27 = Utf8 你好 #28 = NameAndType #16:#13 // haha:Ljava/lang/String; #29 = Class #40 // java/lang/System #30 = NameAndType #41:#42 // out:Ljava/io/PrintStream; #31 = Utf8 a = 1 #32 = Class #43 // java/io/PrintStream #33 = NameAndType #44:#45 // println:(Ljava/lang/String;)V #34 = Utf8 b = 1 #35 = Utf8 else #36 = Utf8 a自减 #37 = Utf8 Test #38 = Utf8 java/lang/Object #39 = Utf8 hello world #40 = Utf8 java/lang/System #41 = Utf8 out #42 = Utf8 Ljava/io/PrintStream; #43 = Utf8 java/io/PrintStream #44 = Utf8 println #45 = Utf8 (Ljava/lang/String;)V { public static final java.lang.String HELLO_WORLD; descriptor: Ljava/lang/String; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: String hello world public java.lang.String haha; descriptor: Ljava/lang/String; flags: ACC_PUBLIC public Test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: ldc #2 // String 你好 7: putfield #3 // Field haha:Ljava/lang/String; 10: return LineNumberTable: line 1: 0 line 3: 4 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: iconst_1 1: istore_1 2: iconst_1 3: istore_2 4: iload_1 5: iconst_1 6: if_icmpne 20 9: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 12: ldc #5 // String a = 1 14: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 17: goto 44 20: iload_2 21: iconst_1 22: if_icmpne 36 25: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 28: ldc #7 // String b = 1 30: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 33: goto 44 36: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 39: ldc #8 // String else 41: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 44: iload_1 45: iinc 1, -1 48: ifle 62 51: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 54: ldc #9 // String a自减 56: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 59: goto 44 62: return LineNumberTable: line 5: 0 line 6: 2 line 7: 4 line 8: 9 line 9: 20 line 10: 25 line 12: 36 line 14: 44 line 15: 51 line 18: 62 StackMapTable: number_of_entries = 4 frame_type = 253 /* append */ offset_delta = 20 locals = [ int, int ] frame_type = 15 /* same */ frame_type = 7 /* same */ frame_type = 17 /* same */ } SourceFile: "Test.java" ``` ### 字节码操作指令 - 加载存储指令 - 运算指令 - 类型转换指令 - 对象创建与访问指令 - 操作数栈管理指令 - 控制转移指令 - 方法调用和返回指令 - 异常处理指令 - 同步指令 ## 类加载 ### 类加载过程 ![类加载过程](类加载过程.png) 其中,加载、验证、准备、初始化、卸载能保证有序进行。 解析阶段不一定,java运行时绑定要求在初始化后再解析。 #### 加载 1. 通过类的全限定名来获取定义此类的二进制字节流,如从ZIP获取、从网络获取、从DB获取、运算时动态生成等。 2. 通过字节流所代表的静态存储结构转换为方法区的运行时数据结构。 3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。 #### 验证 ##### 文件格式验证 class文件格式验证,包含魔数、版本号、常量池信息等。 ##### 元数据验证 语义分析,是否符合java规范,如是否有父类、是否多继承、是否重新了final字段等。 ##### 字节码验证 保证类型转换的有效性、保证跳转指令不会跳到方法体以外的字节码上的等。 ##### 符号引用验证 将符号引用转化为直接引用,这个动作发生在第三阶段(解析阶段),验证类、字段、方法的访问性是否可以被当前类访问、通过字符串描述的全限定名是否能找到对应的类等。 #### 准备 正式为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中分配。 #### 解析 将常量池的符号引用替换为直接引用。 #### 初始化场景 1. 遇到new、getstatic、putstatic、invokestatic字节码指令,如果类没有初始化,则先初始化。 2. 使用java.reflect包反射,如果类没有初始化,则先初始化。 3. 初始化一个类,其父类没有初始化,就先初始化它的父类。 4. 虚拟机启动时,需要先初始化要执行的主类(main()入口) 5. JDK1.7+,如果一个java.lang.invoke.MethodHandle实例最后解析的结果REF_getStatic、REF_putStatic、REF_invokeStaitc的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化。 ### 类加载器 **注意:每一个类加载器都有独立的类名称空间。同一个类被不同的类加载,这两个类必定不相等!** #### 常用的类加载器 - 启动类加载器(Bootstrap ClassLoader) - 扩展类加载器(Extension ClassLoader) - 应用程序类加载器(Application ClassLoader) - 自定义类加载器 ![](classLoader.png) #### 双亲委派模型 **双亲委派模型(Parents Delegation Model):之所以叫双亲委派,完全是翻译问题!** 每个类都有自己的类加载器,加载的时候先判断类是否被当前类加载器加载过,没有加载过就先交由父加载器加载,加载过就直接返回。如果到最顶层还没加载过就一层一层交由下层加载器加载。 ```java // 类加载过程详情查看java.lang.ClassLoader.loadClass(String name,boolean resolve) protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先检查这个类是否被加载过 Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 有父加载器就先委托给父加载器 c = parent.loadClass(name, false); } else { // 父加载器都没加载过,交由BootstrapClassLoader加载(c++) c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // 调用当前ClassLoader的findClass()查找 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } ``` #### 破坏双亲委派模型 目前有3次规模较大的破坏 1. ClassLoader在1.0版本出现,双亲委派是1.2版本出现,在此过程中用户手动实现ClassLoader的 2. 类似JNDI服务,JNDI本身只提供接口规范,厂商做实现。JNDI需要调用用户代码,而JNDI接口本身由父加载器加载,厂商的jar由子加载器加载。 解决方案:线程上下文加载器(Thread Context ClassLoader) 3. 代码热替换、模块热部署,如OSGI #### 虚拟机自带的类加载器 ```java public class User{ private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ``` ```java /** * 类加载机制 */ public class TestClassLoader { public static void main(String[] args) { ClassLoader classLoader = User.class.getClassLoader(); // 系统类加载器(System Class Loader),加载当前应用classpath的所有类,由java实现 System.out.println(System.getProperty("java.class.path")); System.out.println("自定义类加载器:" + classLoader); // 扩展类加载器(Extension Class Loader),可以使用System.getProperty("java.ext.dirs")查看加载目录,由java实现 System.out.println(System.getProperty("java.ext.dirs")); System.out.println("父亲加载器:" + classLoader.getParent()); // 启动类加载器(BootStrap Class Loader),可以使用System.getProperty("sun.boot.class.path")查看加载目录,由C++实现,所以打印不出来,输出null System.out.println(System.getProperty("sun.boot.class.path")); System.out.println("爷爷加载器:" + classLoader.getParent().getParent()); } } ``` ```shell # 操作步骤: # 将User.java和TestClassLoader.java放到桌面 # javac -encoding UTF-8 -d ./bin TestClassLoader.java # cd bin # java TestClassLoader ``` ```java // 控制台(格式化输出后) // java.class.path指向编译后的class文件的目录 java.class.path:. 自定义类加载器:sun.misc.Launcher$AppClassLoader@659e0bfd // java.ext.dirs指向jre/lib/ext目录 java.ext.dirs: D:\devTools\environment\java8\jre\lib\ext; C:\Windows\Sun\Java\lib\ext 父亲加载器:sun.misc.Launcher$ExtClassLoader@6d06d69c // sun.boot.class.path指向rt.jar等 sun.boot.class.path: D:\devTools\environment\java8\jre\lib\resources.jar; D:\devTools\environment\java8\jre\lib\rt.jar; D:\devTools\environment\java8\jre\lib\sunrsasign.jar; D:\devTools\environment\java8\jre\lib\jsse.jar; D:\devTools\environment\java8\jre\lib\jce.jar; D:\devTools\environment\java8\jre\lib\charsets.jar; D:\devTools\environment\java8\jre\lib\jfr.jar; D:\devTools\environment\java8\jre\classes 爷爷加载器:null ``` #### 用户自定义的类加载器 ##### 准备class文件 Demo:提前在nosrc包下添加编译好的class文件 ```java package wly.test; public class HaHa { private int id; private String name; public void sayHello(){ System.out.println("Hello"); } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ``` ##### 编写自定义类加载器 继承ClassLoader,重写findClass(),读取class文件并调用defineClass()载入jvm ```java package com.wang.classloader; import java.io.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * 用户自定义类加载器,需覆盖findClass() */ public class CustomerClassLoader extends ClassLoader { /** * 自定义class文件的路径 */ private String classPath; public CustomerClassLoader(String classPath) { this.classPath = classPath; } /** * 重写findClass方法,解析class文件 =》 类定义 * * @param name 包名 + 类名,如com.wang.Test * @return * @throws ClassNotFoundException */ @Override protected Class findClass(String name) throws ClassNotFoundException { // 类名 String className = name.substring(name.lastIndexOf(".") + 1); // 找到class文件 String path = classPath + File.separatorChar + className + ".class"; try (InputStream in = new FileInputStream(path); ByteArrayOutputStream out = new ByteArrayOutputStream()) { byte[] buffer = new byte[2048]; int len = 0; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } // 解析class文件 =》 byte[] byte[] byteArray = out.toByteArray(); // 调用defineClass()函数,将字节码转化为类定义 return defineClass(name, byteArray, 0, byteArray.length); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 测试 */ public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { // 加载工程外的class文件 CustomerClassLoader customerClassLoader = new CustomerClassLoader("E:\\myself\\jvm-study\\nosrc"); Class clazz = customerClassLoader.loadClass("wly.test.HaHa"); // 获取实例对象 Object o = clazz.newInstance(); Method method = clazz.getMethod("sayHello", null); method.invoke(o, null); System.out.println(clazz.getClassLoader().toString()); } } ``` ```java // 控制台:可见此类使用我们的自定义加载器加载 Hello com.wang.classloader.CustomerClassLoader@1540e19d ``` ------ ## 虚拟机工具 ### JDK自带命令 #### jps(JVM Process Status) ```shell # 作用:列出正在运行的虚拟机进程并显示虚拟机执行主类 # 常被用作看目前有哪些运行着的java进程 # 命令格式: jps [options] [hostid] ``` #### jstat(JVM Statistics Monitoring Tool) ```shell # 作用:查询虚拟机信息,包含类装载、垃圾收集、运行期编译状况等 # 常被用作用于看gc情况,如jstat -gcutil 13106 1000 10,查看进程13106的gc情况每秒输出1次,共输出10次 # 命令格式: jstat [option vmid [interval[s|ms] [count]]] ``` #### jinfo(Configuration Info for Java) ```shell # 作用:实时地查看和调整虚拟机的各项参数 # 常被用作查看虚拟机的默认配置、手动指定的配置,如:jinfo -flags 13106 # 命令格式: jinfo [option] pid ``` #### jmap(Memory Map for Java) ```shell # 作用:查看堆内存使用情况或转储堆内存快照 # 常被用作查看堆内存、方法区的使用情况,如:jmap -heap 13106 # 命令格式: jmap [option] id ``` #### jstack(Stack Trace for Java) ```shell # 作用:用于生成虚拟机当前时刻的线程快照 # 常被用作分析长时间停顿的原因,如死锁、死循环等 # 命令格式: jstack [option] vmid ``` #### jhat(JVM Heap Analysis Tool) ```shell # 作用:与jmap搭配使用,常被用来分析jmap生成的dump快照 # 分析dump文件,需要比dump文件更大的内存! # 命令格式: jhat [dumpFile] ``` ### JDK自带工具 在$JAVA_HOME/bin目录下,jconsole、jvisualvm等 ### 第三方分析工具 TProfiler 、Arthas等 ## 常见面试问题和解析 #### 什么样的对象会进入老年代? 1. 大对象:需要大量连续内存空间的java对象,比如数组或者长字符串。直接分配到老年代。 ```java -Xms50m -Xmx50m -Xmn10m -verbose:gc -XX:+PrintGCDetails // 整个Young区才10m,按照默认8:1:1,Eden区是8m大小,survivor是1m大小,分配20m的连续对象会直接分配到老年代 byte[] bytes1 = new byte[1024 * 1024 * 20]; ``` 2. 配置JVM参数,超过此值直接在老年代上分配-XX:PretenureSizeThreshold (有坑,1.8年老代默认是并行垃圾收集器,此参数无效,必须配合-XX:+UseSerialGC使用才能将大对象直接分配到年老代) ```java -Xms50m -Xmx50m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC // 设置3m的阈值,超过3m直接在年老带分配,需要使用-XX:+UseSerialGC参数才生效 // -XX:PretenureSizeThreshold参数和垃圾回收器有关 byte[] bytes1 = new byte[1024 * 1024 * 4]; ``` 1. 长期存活的对象:可以通过-XX:MaxTenuringThreshold配置,默认是15,每次Young GC,年龄会+1,超过配置年龄即进入老年代 2. 动态年龄判定,如果Survivor空间中相同年龄的对象总和大于Survivor区的一半,无需达到MaxTenuringThreshold,对象也可以进入老年代。