JVM(jvm面试题)

  本篇文章为你整理了JVM(jvm面试题)的详细内容,包含有jvm调优 jvm面试题 jvm是什么意思 jvm原理 JVM,希望能帮助你了解 JVM。

  每个线程运行需要的内存空间,称为虚拟机栈

  每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存

  每个线程只能有一个活动栈帧,对应着当前正在执行的方法(栈顶部的第一个方法)

  代码

  

public class Main {

 

   public static void main(String[] args) {

   method1();

   private static void method1() {

   method2(1, 2);

   private static int method2(int a, int b) {

   int c = a + b;

   return c;

  }Copy

  

 

  在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点

  垃圾回收是否涉及栈内存?

  不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。

  
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

  
top命令,查看是哪个进程占用CPU过高

  ps H -eo pid, tid(线程id), %cpu grep 刚才通过top查到的进程号通过ps命令进一步查看是哪个线程占用CPU过高

  jstack 进程id通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换

  
3、本地方法栈

  一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法

  通过new关键字创建的对象都会被放在堆内存

  所有线程共享,堆内存中的对象都需要考虑线程安全问题

  有垃圾回收机制

  堆内存溢出

  java.lang.OutofMemoryError:java heap space. 堆内存溢出

  堆内存诊断

  jps

  jmap

  jconsole

  jvirsalvm

  5、方法区

  1.8以前会导致永久代内存溢出

  1.8以后会导致元空间内存溢出

  二进制字节码的组成:类的基本信息、常量池、类的方法定义(包含了虚拟机指令)

  通过反编译来查看类的信息

  
输入javac 对应类的绝对路径

  

F:\JAVA\JDK8.0\bin javac F:\Thread_study\src\com\nyima\JVM\day01\Main.javaCopy

 

  

 

  输入完成后,对应的目录下就会出现类的.class文件

  
在控制台输入 javap -v 类的绝对路径

  

javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy

 

  

 

  
虚拟机中执行编译的方法(框内的是真正编译执行的内容,**#号的内容需要在常量池中查找**)

  
就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息

  
常量池是*.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

  
常量池中的字符串仅是符号,只有在被用到时才会转化为对象

  利用串池的机制,来避免重复创建字符串对象

  字符串变量拼接的原理是StringBuilder

  字符串常量拼接的原理是编译器优化

  可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

  注意:无论是串池还是堆里面的字符串,都是对象

  用来放字符串对象且里面的元素不重复

  

public class StringTableStudy {

 

   public static void main(String[] args) {

   String a = "a";

   String b = "b";

   String ab = "ab";

  }

 

  

  常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串

  

0: ldc #2 // String a

 

  2: astore_1

  3: ldc #3 // String b

  5: astore_2

  6: ldc #4 // String ab

  8: astore_3

  9: returnCopy

 

  

  当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)

  当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中

  当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中

  最终StringTable [“a”, “b”, “ab”]

  注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

  使用拼接字符串变量对象创建字符串的过程

  

public class StringTableStudy {

 

   public static void main(String[] args) {

   String a = "a";

   String b = "b";

   String ab = "ab";

   //拼接字符串对象来创建新的字符串

   String ab2 = a+b;

  }

 

  

  

 

 

  

 

  通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()

  最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中

  

 

 

  

 

  使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。

  使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建

  intern方法 1.8

  调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  如果串池中没有该字符串对象,则放入成功

  如果有该字符串对象,则放入失败

  无论放入是否成功,都会返回串池中的字符串对象

  注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

  

 

 

  

 

  intern方法 1.6

  调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中

  如果有该字符串对象,则放入失败

  无论放入是否成功,都会返回串池中的字符串对象

  注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象

  StringTable 垃圾回收

  StringTable在内存紧张时,会发生垃圾回收

  StringTable调优

  
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间

  

-XX:StringTableSize=xxxxCopy

 

  

 

  
文件读写流程

  使用了DirectBuffer

  直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率

  直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放

  通过

  

//通过ByteBuffer申请1M的直接内存

 

  ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);Copy

 

  申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?

  allocateDirect返回一个DirectByteBuffer对象,里面申请内存,通过虚引用对象释放内存

  DirectByteBuffer类

  

DirectByteBuffer(int cap) { // package-private

 

   super(-1, 0, cap, cap);

   boolean pa = VM.isDirectMemoryPageAligned();

   int ps = Bits.pageSize();

   long size = Math.max(1L, (long)cap + (pa ? ps : 0));

   Bits.reserveMemory(size, cap);

   long base = 0;

   try {

   base = unsafe.allocateMemory(size); //申请内存

   } catch (OutOfMemoryError x) {

   Bits.unreserveMemory(size, cap);

   throw x;

   unsafe.setMemory(base, size, (byte) 0);

   if (pa (base % ps != 0)) {

   // Round up to page boundary

   address = base + ps - (base (ps - 1));

   } else {

   address = base;

   cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //通过虚引用,来实现直接内存的释放,this为虚引用的实际对象

   att = null;

  }Copy

  

 

  这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer)被回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存

  

public void clean() {

 

   if (remove(this)) {

   try {

   this.thunk.run(); //调用run方法

   } catch (final Throwable var2) {

   AccessController.doPrivileged(new PrivilegedAction Void () {

   public Void run() {

   if (System.err != null) {

   (new Error("Cleaner terminated abnormally", var2)).printStackTrace();

   System.exit(1);

   return null;

   }Copy

  

 

  对应对象的run方法

  

public void run() {

 

   if (address == 0) {

   // Paranoia

   return;

   unsafe.freeMemory(address); //释放直接内存中占用的内存

   address = 0;

   Bits.unreserveMemory(size, capacity);

  }Copy

  

 

  直接内存的回收机制总结

  使用了Unsafe类来完成直接内存的分配回收,回收需要主动调用freeMemory方法

  ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存

  三、垃圾回收

  1、如何判断对象可以回收

  引用计数法

  弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放

  可达性分析算法

  JVM中的垃圾回收器通过可达性分析来探索所有存活的对象

  扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收

  可以作为GC Root的对象

  虚拟机栈(栈帧中的本地变量表)中引用的对象。

  方法区中类静态属性引用的对象

  方法区中常量引用的对象

  本地方法栈中JNI(即一般说的Native方法)引用的对象

  
 

  只有GC Root都不引用该对象时,才会回收强引用对象

  如上图B、C对象都不引用A1对象时,A1对象才会被回收

  当GC Root指向软引用对象时,在内存不足时,会回收软引用所引用的对象

  如上图如果B对象不再引用A2对象且内存不足时,软引用所引用的A2对象就会被回收

  软引用的使用

  

public class Demo1 {

 

   public static void main(String[] args) {

   final int _4M = 4*1024*1024;

   //使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用

   List SoftReference byte[] list = new ArrayList ();

   SoftReference byte[] ref= new SoftReference (new byte[_4M]);

  }Copy

  

 

  如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理

  如果想要清理软引用,需要使用引用队列

  

public class Demo1 {

 

   public static void main(String[] args) {

   final int _4M = 4*1024*1024;

   //使用引用队列,用于移除引用为空的软引用对象

   ReferenceQueue byte[] queue = new ReferenceQueue ();

   //使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用

   List SoftReference byte[] list = new ArrayList ();

   SoftReference byte[] ref= new SoftReference (new byte[_4M]);

   //遍历引用队列,如果有元素,则移除

   Reference ? extends byte[] poll = queue.poll();

   while(poll != null) {

   //引用队列不为空,则从集合中移除该元素

   list.remove(poll);

   //移动到引用队列中的下一个元素

   poll = queue.poll();

  }Copy

  

 

  大概思路为:查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合)

  只有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象

  如上图如果B对象不再引用A3对象,则A3对象会被回收

  弱引用的使用和软引用类似,只是将SoftReference 换为了 WeakReference

  当虚引用对象所引用的对象被回收以后,虚引用对象就会被放入引用队列中,调用虚引用的方法

  虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存

  如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存

  终结器引用

  所有的类都继承自Object类,Object类有一个finalize方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize方法。调用以后,该对象就可以被垃圾回收了

  如上图,B对象不再引用A4对象。这是终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize方法。调用以后,该对象就可以被垃圾回收了

  软引用和弱引用可以配合引用队列

  在弱引用和虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象

  
标记-清除

  定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间

  这里的腾出内存空间并不是将内存空间的字节清0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存

  缺点:容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢

  标记-整理

  标记-整理 会将不被GC Root引用的对象回收,清楚其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是因为整体需要消耗一定的时间,所以效率较低

  将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。

  3、分代回收

  新创建的对象都被放在了新生代的伊甸园中

  当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做Minor GC

  Minor GC 会将伊甸园和幸存区FROM存活的对象先复制到幸存区 TO中, 并让其寿命加1,再交换两个幸存区

  再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1

  如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中

  如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收

  GC 分析

  大对象处理策略

  当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

  线程内存溢出

  某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行

  这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常

  4、垃圾回收器

  并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

  并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上

  吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

  内存较小,个人电脑(CPU核数较少)

  安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象

  因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

  Serial 收集器

  Serial收集器是最基本的、发展历史最悠久的收集器

  特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)

  ParNew 收集器

  ParNew收集器其实就是Serial收集器的多线程版本

  特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题

  Serial Old 收集器

  Serial Old是Serial收集器的老年代版本

  特点:同样是单线程收集器,采用标记-整理算法

  吞吐量优先

  堆内存较大,多核CPU

  单位时间内,STW(stop the world,停掉其他所有工作线程)时间最短

  JDK1.8默认使用的垃圾回收器

  Parallel Scavenge 收集器

  与吞吐量关系密切,故也称为吞吐量优先收集器

  特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)

  该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)

  GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

  Parallel Scavenge收集器使用两个参数控制吞吐量:

  XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间

  XX:GCRatio 直接设置吞吐量的大小

  Parallel Old 收集器

  是Parallel Scavenge收集器的老年代版本

  特点:多线程,采用标记-整理算法(老年代没有幸存区)

  响应时间优先

  堆内存较大,多核CPU

  尽可能让单次STW时间变短(尽量不影响其他线程运行)

  CMS 收集器

  Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器

  特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片

  应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务

  CMS收集器的运行过程分为下列4步:

  初始标记:标记GC Roots直接关联的对象

  并发标记:遍历整个对象图

  重新标记:修正并发标记期间由于用户线程运作而标记产生变动的那一部分对象。仍然存在Stop The World问题

  并发清除:对标记的对象进行清除回收

  CMS收集器的内存回收过程是与用户线程一起并发执行的

  Garbage First

  JDK 9以后默认使用,而且替代了CMS 收集器

  同时注重吞吐量和低延迟(响应时间)

  超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域

  整体上是标记-整理算法,两个区域之间是复制算法

  相关参数:JDK8 并不是默认开启的,所需要参数开启

  G1垃圾回收阶段

  新生代伊甸园垃圾回收—– 内存不足,新生代回收+并发标记—– 回收新生代伊甸园、幸存区、老年代内存—— 新生代伊甸园垃圾回收(重新开始)

  Young Collection

  分区算法region

  分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间

  E:伊甸园 S:幸存区 O:老年代

  Young Collection + CM

  CM:并发标记

  在 Young GC 时会对 GC Root 进行初始标记

  在老年代占用堆内存的比例达到阈值时,对进行并发标记(不会STW),阈值可以根据用户来进行设定

  Mixed Collection

  会对E S O 进行全面的回收

  -XX:MaxGCPauseMills:xxx 用于指定最长的停顿时间

  为什么不是所有的老年代会被回收?

  因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

  Full GC

  G1在老年代内存不足时(老年代所占内存超过阈值)

  如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理

  如果垃圾产生速度快于垃圾回收速度,便会触发Full GC

  

  5、GC 调优

  查看虚拟机参数命令

  

"F:\JAVA\JDK8.0\bin\java" -XX:+PrintFlagsFinal -version findstr "GC"Copy

 

  

 

  可以根据参数去查询具体的信息

  CPU占用

  低延迟/高吞吐量? 选择合适的GC

  CMS G1 ZGC

  ParallelGC

  最快的GC是不发生GC

  首先排除减少因为自身编写的代码而引发的内存问题

  查看Full GC前后的内存占用,考虑以下几个问题

  数据是不是太多?

  数据表示是否太臃肿

  
新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降

  新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长

  
幸存区需要能够保存当前活跃对象+需要晋升的对象

  晋升阈值配置得当,让长时间存活的对象尽快晋升

  老年代调优

  四、类加载与字节码技术

  1、类文件结构

  首先获得.class字节码文件

  方法:

  在文本文档里写入java代码(文件名与类名一致),将文件类型改为.java

  java终端中,执行javac X:…\XXX.java

  以下是字节码文件

  

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 

 

  0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

  0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

  0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63

  0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01

  0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63

  0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f

  0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16

  0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72

  0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13

  0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69

  0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61

  0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46

  0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64

  0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

  0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64

  0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74

  0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c

  0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61

  0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61

  0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f

  0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72

  0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76

  0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d

  0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a

  0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b

  0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

  0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01

  0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00

  0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00

  0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00

  0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00

  0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a

  0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b

  0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00

  0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00

  0001120 00 00 02 00 14Copy

  

 

  根据 JVM 规范,类文件结构如下

  

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];Copy

  

 

  

  2、字节码指令

  

  javap工具

  Oracle 提供了javap工具来反编译 class 文件

  

javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy

 

  F:\Thread_study javap -v F:\Thread_study\src\com\nyima\JVM\day5\Demo1.class

  Classfile /F:/Thread_study/src/com/nyima/JVM/day5/Demo1.class

   Last modified 2020-6-6; size 434 bytes

   MD5 checksum df1dce65bf6fb0b4c1de318051f4a67e

   Compiled from "Demo1.java"

  public class com.nyima.JVM.day5.Demo1

   minor version: 0

   major version: 52

   flags: ACC_PUBLIC, ACC_SUPER

  Constant pool:

   #1 = Methodref #6.#15 // java/lang/Object." init ":()V

   #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;

   #3 = String #18 // hello world

   #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V

   #5 = Class #21 // com/nyima/JVM/day5/Demo1

   #6 = Class #22 // java/lang/Object

   #7 = Utf8 init

   #8 = Utf8 ()V

   #9 = Utf8 Code

   #10 = Utf8 LineNumberTable

   #11 = Utf8 main

   #12 = Utf8 ([Ljava/lang/String;)V

   #13 = Utf8 SourceFile

   #14 = Utf8 Demo1.java

   #15 = NameAndType #7:#8 // " init ":()V

   #16 = Class #23 // java/lang/System

   #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;

   #18 = Utf8 hello world

   #19 = Class #26 // java/io/PrintStream

   #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V

   #21 = Utf8 com/nyima/JVM/day5/Demo1

   #22 = Utf8 java/lang/Object

   #23 = Utf8 java/lang/System

   #24 = Utf8 out

   #25 = Utf8 Ljava/io/PrintStream;

   #26 = Utf8 java/io/PrintStream

   #27 = Utf8 println

   #28 = Utf8 (Ljava/lang/String;)V

   public com.nyima.JVM.day5.Demo1();

   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 7: 0

   public static void main(java.lang.String[]);

   descriptor: ([Ljava/lang/String;)V

   flags: ACC_PUBLIC, ACC_STATIC

   Code:

   stack=2, locals=1, args_size=1

   0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;

   3: ldc #3 // String hello world

   5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

   8: return

   LineNumberTable:

   line 9: 0

   line 10: 8

  }Copy

  

 

  

  通过字节码指令来分析问题

  

 

 

  

 

  cinit()V

  

public class Demo3 {

 

   static int i = 10;

   static {

   i = 20;

   static {

   i = 30;

   public static void main(String[] args) {

   System.out.println(i); //结果为30

  }Copy

  

 

  编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V :

  

stack=1, locals=0, args_size=0

 

   0: bipush 10

   2: putstatic #3 // Field i:I

   5: bipush 20

   7: putstatic #3 // Field i:I

   10: bipush 30

   12: putstatic #3 // Field i:I

   15: returnCopy

  

 

  init()V

  

public class Demo4 {

 

   private String a = "s1";

   b = 20;

   private int b = 10;

   a = "s2";

   public Demo4(String a, int b) {

   this.a = a;

   this.b = b;

   public static void main(String[] args) {

   Demo4 d = new Demo4("s3", 30);

   System.out.println(d.a);

   System.out.println(d.b);

  }Copy

  

 

  编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后

  

Code:

 

   stack=2, locals=3, args_size=3

   0: aload_0

   1: invokespecial #1 // Method java/lang/Object." init ":()V

   4: aload_0

   5: ldc #2 // String s1

   7: putfield #3 // Field a:Ljava/lang/String;

   10: aload_0

   11: bipush 20

   13: putfield #4 // Field b:I

   16: aload_0

   17: bipush 10

   19: putfield #4 // Field b:I

   22: aload_0

   23: ldc #5 // String s2

   25: putfield #3 // Field a:Ljava/lang/String;

   //原始构造方法在最后执行

   28: aload_0

   29: aload_1

   30: putfield #3 // Field a:Ljava/lang/String;

   33: aload_0

   34: iload_2

   35: putfield #4 // Field b:I

   38: returnCopy

  

 

  

 

 

  

 

  3、编译期处理

  所谓的语法糖,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利

  

  默认构造函数

  

public class Candy1 {

 

  }Copy

  

 

  经过编译期优化后

  

public class Candy1 {

 

   //这个无参构造器是java编译器帮我们加上的

   public Candy1() {

   //即调用父类 Object 的无参构造方法,即调用 java/lang/Object." init ":()V

   super();

  }Copy

  

 

  自动拆装箱

  基本类型和其包装类型的相互转换过程,称为拆装箱

  在JDK 5以后,它们的转换可以在编译期自动完成

  

public class Demo2 {

 

   public static void main(String[] args) {

   Integer x = 1;

   int y = x;

  }Copy

  

 

  转换过程如下

  

public class Demo2 {

 

   public static void main(String[] args) {

   //基本类型赋值给包装类型,称为装箱

   Integer x = Integer.valueOf(1);

   //包装类型赋值给基本类型,称谓拆箱

   int y = x.intValue();

  }Copy

  

 

  泛型集合取值

  泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了Object类型来处理:

  

public class Demo3 {

 

   public static void main(String[] args) {

   List Integer list = new ArrayList ();

   list.add(10);

   Integer x = list.get(0);

  }Copy

  

 

  对应字节码

  

Code:

 

   stack=2, locals=3, args_size=1

   0: new #2 // class java/util/ArrayList

   3: dup

   4: invokespecial #3 // Method java/util/ArrayList." init ":()V

   7: astore_1

   8: aload_1

   9: bipush 10

   11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

   //这里进行了泛型擦除,实际调用的是add(Objcet o)

   14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

   19: pop

   20: aload_1

   21: iconst_0

   //这里也进行了泛型擦除,实际调用的是get(Object o)

   22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;

  //这里进行了类型转换,将Object转换成了Integer

   27: checkcast #7 // class java/lang/Integer

   30: astore_2

   31: returnCopy

  

 

  所以调用get函数取值时,有一个类型转换的操作

  

Integer x = (Integer) list.get(0);Copy

 

  

 

  如果要将返回结果赋值给一个int类型的变量,则还有自动拆箱的操作

  

int x = (Integer) list.get(0).intValue();Copy

 

  

 

  

public class Demo4 {

 

   public static void foo(String... args) {

   //将args赋值给arr,可以看出String...实际就是String[]

   String[] arr = args;

   System.out.println(arr.length);

   public static void main(String[] args) {

   foo("hello", "world");

  }Copy

  

 

  可变参数String…args 其实是一个String[]args ,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:

  

public class Demo4 {

 

   public Demo4 {}

  
 

 

  注意,如果调用的是foo(),即未传递参数时,等价代码为foo(new String[]{}),创建了一个空数组,而不是直接传递的null

  foreach

  

public class Demo5 {

 

   public static void main(String[] args) {

   //数组赋初值的简化写法也是一种语法糖。

   int[] arr = {1, 2, 3, 4, 5};

   for(int x : arr) {

   System.out.println(x);

  }Copy

  

 

  编译器会帮我们转换为

  

public class Demo5 {

 

   public Demo5 {}

   public static void main(String[] args) {

   int[] arr = new int[]{1, 2, 3, 4, 5};

   for(int i=0; i arr.length; ++i) {

   int x = arr[i];

   System.out.println(x);

  }Copy

  

 

  如果是集合使用foreach

  

public class Demo5 {

 

   public static void main(String[] args) {

   List Integer list = Arrays.asList(1, 2, 3, 4, 5);

   for (Integer x : list) {

   System.out.println(x);

  }Copy

  

 

  集合要使用foreach,需要该集合类实现了Iterable接口,因为集合的遍历需要用到迭代器Iterator

  

public class Demo5 {

 

   public Demo5 {}

   public static void main(String[] args) {

   List Integer list = Arrays.asList(1, 2, 3, 4, 5);

   //获得该集合的迭代器

   Iterator Integer iterator = list.iterator();

   while(iterator.hasNext()) {

   Integer x = iterator.next();

   System.out.println(x);

  }Copy

  

 

  switch字符串

  

public class Demo6 {

 

   public static void main(String[] args) {

   String str = "hello";

   switch (str) {

   case "hello" :

   System.out.println("h");

   break;

   case "world" :

   System.out.println("w");

   break;

   default:

   break;

  }Copy

  

 

  在编译器中执行的操作

  

public class Demo6 {

 

   public Demo6() {

   public static void main(String[] args) {

   String str = "hello";

   int x = -1;

   //通过字符串的hashCode+value来判断是否匹配

   switch (str.hashCode()) {

   //hello的hashCode

   case 99162322 :

   //再次比较,因为字符串的hashCode有可能相等

   if(str.equals("hello")) {

   x = 0;

   break;

   //world的hashCode

   case 11331880 :

   if(str.equals("world")) {

   x = 1;

   break;

   default:

   break;

   //用第二个switch在进行输出判断

   switch (x) {

   case 0:

   System.out.println("h");

   break;

   case 1:

   System.out.println("w");

   break;

   default:

   break;

  }Copy

  

 

  过程说明:

  在编译期间,单个的switch被分为了两个

  第一个用来匹配字符串,并给x赋值

  字符串的匹配用到了字符串的hashCode,还用到了equals方法

  使用hashCode是为了提高比较效率,使用equals是防止有hashCode冲突(如BM和C.)

  
* 定义一个合成类(仅 jvm 使用,对我们不可见)

   * 用来映射枚举的 ordinal 与数组元素的关系

   * 枚举的 ordinal 表示枚举对象的序号,从 0 开始

   * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1

   static class $MAP {

   //数组大小即为枚举元素个数,里面存放了case用于比较的数字

   static int[] map = new int[2];

   static {

   //ordinal即枚举元素对应所在的位置,MALE为0,FEMALE为1

   map[SEX.MALE.ordinal()] = 1;

   map[SEX.FEMALE.ordinal()] = 2;

   public static void main(String[] args) {

   SEX sex = SEX.MALE;

   //将对应位置枚举元素的值赋给x,用于case操作

   int x = $MAP.map[sex.ordinal()];

   switch (x) {

   case 1:

   System.out.println("man");

   break;

   case 2:

   System.out.println("woman");

   break;

   default:

   break;

  enum SEX {

   MALE, FEMALE;

  }Copy

  

 

 

  

enum SEX {

 

   MALE, FEMALE;

  }Copy

  

 

  转换后的代码

  

public final class Sex extends Enum Sex { 

 

   //对应枚举类中的元素

   public static final Sex MALE;

   public static final Sex FEMALE;

   private static final Sex[] $VALUES;

   static {

   //调用构造函数,传入枚举元素的值及ordinal

   MALE = new Sex("MALE", 0);

   FEMALE = new Sex("FEMALE", 1);

   $VALUES = new Sex[]{MALE, FEMALE};

   //调用父类中的方法

   private Sex(String name, int ordinal) {

   super(name, ordinal);

   public static Sex[] values() {

   return $VALUES.clone();

   public static Sex valueOf(String name) {

   return Enum.valueOf(Sex.class, name);

  }Copy

  

 

  匿名内部类

  

public class Demo8 {

 

   public static void main(String[] args) {

   Runnable runnable = new Runnable() {

   @Override

   public void run() {

   System.out.println("running...");

  }Copy

  

 

  转换后的代码

  

public class Demo8 {

 

   public static void main(String[] args) {

   //用额外创建的类来创建匿名内部类对象

   Runnable runnable = new Demo8$1();

  //创建了一个额外的。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: