为什么要学JVM?
学习JVM是开发Java应用程序必不可少的一部分,因为JVM是Java平台的核心。以下是为什么要学习JVM的几个原因:
跨平台性:JVM可以在不同的系统上运行Java代码,使Java应用程序具备了可移植性和跨平台性。
内存管理:JVM的内存管理机制自动回收不再使用的内存,保证了Java程序的稳定性和可靠性。
性能优化:通过学习JVM的内部机制和优化技术,可以提高Java程序的性能。
调试技能:学习JVM可以帮助开发人员更好地理解Java程序的运行机制,从而调试和排查程序中的问题。
应用监控:JVM自带的监控工具可以提供Java应用程序的性能数据,帮助开发人员优化程序和调整系统参数。
综上所述,学习JVM是Java开发人员必备的基础技能,有助于提高程序开发和优化能力,从而构建高效、稳定的Java应用程序。
其实对于一个还没毕业的学生来讲–就是为了面试。但也不是完全为了面试。所以本片文章则偏向于面试目的的学习。
JVM(Java Virtual Machine)是Java程序的运行环境,它是一个虚拟的计算机,可以在不同的平台上运行Java程序,实现了Java程序的跨平台性。JVM包括类加载器、解释器、即时编译器、垃圾收集器等组件。
类加载器:类加载器负责将Java类文件加载到JVM中,Java程序在运行时会根据需要动态加载类文件。JVM中有三种类加载器,分别是Bootstap ClassLoader、Extension ClassLoader和System ClassLoader。
解释器:解释器负责将Java字节码解释成机器可以执行的指令,实现了Java程序的跨平台性。Java程序在运行时不是直接运行源代码,而是将源代码编译成Java字节码(即.class文件),JVM中的解释器会将Java字节码解释成机器可以执行的指令。
即时编译器:即时编译器(JIT,Just-In-Time Compiler)是JVM的一个重要组件,它可以将Java字节码编译成与特定硬件平台相关的本地代码,从而提高Java程序的执行效率。
垃圾收集器:垃圾收集器是JVM的一个重要组件,它负责自动回收不再使用的Java对象的内存,从而避免了手动管理内存的麻烦。JVM中的垃圾收集器具有自动、周期性、无需干预等特点。
JVM还实现了Java程序的安全性和隔离性,可以对Java程序进行安全管理和控制,保证程序的安全性和可靠性。除了Java语言,JVM也支持其他语言,如Groovy、Scala、Kotlin等
JVM(Java Virtual Machine)是Java程序的运行环境,它有自己的内存管理系统。在JVM中,内存分为几个部分,主要包括以下几种:
堆(Heap):堆是JVM中最大的一块内存,它被所有线程共享,用于存储对象实例。当一个对象被创建时,它被存储在堆中,并返回该对象的引用(reference)。
栈(Stack):栈是JVM中的一块线程私有内存,每个线程都有自己的栈。栈用于存储基本类型的变量和对象引用。当一个方法被调用时,该方法的参数和局部变量都被存储在该线程的栈中。当方法执行完毕后,它使用的栈空间被释放。
方法区(Method Area):方法区也是JVM中的一块共享内存,用于存储已经被加载的类的信息、常量、静态变量、即时编译器(JIT)编译后的代码等。
本地方法栈(Native Method Stack):本地方法栈与栈类似,但用于存储与本地方法(native methods)相关的数据。
JVM内存管理是自动的,也就是说,Java开发人员无需手动分配和管理内存。JVM会自动对内存进行回收和清理,从而使应用程序更加稳定和可靠。
程序计数器(Program Counter Register)是JVM中的一块内存区域,用于存储正在执行的Java虚拟机字节码指令的地址。每个线程都有自己的程序计数器,以便于同时执行多个线程。每当线程执行一个新的Java虚拟机字节码指令时,程序计数器就会相应地更新当前线程正在执行的指令位置。程序计数器是JVM中最小的一块内存区域,它的大小通常是固定的,一般为4字节或8字节。
程序计数器的主要作用包括以下几个方面:
它是线程私有的,确保线程之间的独立性,从而保证了多个线程之间的执行不会出现混乱。
程序计数器保存当前线程所执行的Java虚拟机字节码指令的地址,可作为线程恢复的依据。
在Java虚拟机中,程序计数器作为垃圾收集器的判断依据之一。如果程序计数器的值为0,则表示该线程目前没有执行任何Java虚拟机字节码指令,因此可以被垃圾收集器作为回收的对象。
是记住下一条jvm指令的执行地址。在Java虚拟机中,由于线程切换导致上下文切换,程序计数器用于帮助线程恢复正在执行的位置,防止线程切换后需要重新从头开始执行,降低了线程切换的成本。
总之,程序计数器是JVM中非常重要的一部分,它可以保证线程之间的独立性,同时也是JVM实现其他高级特性的基础。它的作用就是记录当前线程在执行字节码时的位置,支持Java虚拟机的多线程编程和执行,并且不会内存溢出。
虚拟机栈(Virtual Machine Stack)是Java虚拟机中一块内存区域,用于存储Java方法执行的信息。每当Java方法被调用时,JVM就会为该方法创建一个帧(Frame),并将该帧压入当前线程的虚拟机栈中。每个帧包含了该方法的局部变量表、操作数栈、动态链接、方法出口等信息。当方法执行完毕时,该帧被出栈,恢复原有帧的上下文环境。
概述:每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
虚拟机栈的主要作用包括以下几个方面:
实现了Java方法的深度优先调用:虚拟机栈通过存储每个方法的帧确保方法的调用和返回按照预期的方式执行,从而实现了Java方法的深度优先调用。
维护方法的上下文环境:每个帧包含了该方法的局部变量表、操作数栈、动态链接、方法出口等信息,虚拟机栈的作用就是在方法的调用和返回时维护该方法的上下文环境。
帮助线程在方法执行过程中恢复现场:虚拟机栈记录了线程执行方法的顺序和方法的调用链,能够在方法执行出现异常或其他情况下,帮助线程在方法执行过程中恢复现场。
线程私有的:每个线程都有自己的虚拟机栈,确保线程之间的独立性,避免线程之间的干扰。
总之,虚拟机栈是Java虚拟机非常重要的一部分,它保证了方法的调用和返回按照预期的方式执行,实现了Java方法的深度优先调用。虚拟机栈还通过存储每个方法的帧维护方法的上下文环境,支持线程在方法执行过程中恢复现场。虚拟机栈的大小是不固定的,它可以根据实际需要动态扩展或缩小。
本地方法栈(Native Method Stack)是JVM在执行本地方法时,为其分配的内存空间,用于存储本地方法执行的相关信息,包括本地方法的调用和返回状态、本地方法的参数和局部变量等。与Java虚拟机栈不同的是,本地方法栈不参与Java方法的调用,它是为在Java虚拟机之外执行的本地方法分配内存的。
本地方法栈是线程私有的,当一个线程调用本地方法时,JVM会为该线程创建一个本地方法栈。本地方法栈的大小可以通过JVM参数 -Xss 来指定,同Java虚拟机栈一样,本地方法栈也会防止线程堆栈溢出的问题。
需要注意的是,本地方法栈与本地方法库之间是有联系的,当JVM调用本地方法时,实际上是通过本地方法库加载本地方法并执行,而在执行过程中,本地方法栈中的相关信息也会传递给本地方法库。因此,在编写本地方法时,需要严格遵循本地方法调用的规范,避免出现不可预知的错误。
在Java虚拟机中,堆(Heap)是一个用于存储对象实例的运行时数据区域(new 的对象都会使用堆内存)。在整个Java程序的内存分配中,最大的一部分就是堆内存。堆内存是由垃圾回收器进行自动垃圾回收和释放的。这就使得开发人员无需手动管理内存的分配和释放,可以集中精力在应用程序的开发和实现上。
堆内存是由Java虚拟机在程序运行时动态分配的,也就是说,在程序运行时,堆内存的大小是可以变化的。堆内存通常被划分为“新生代”和“老年代”两个部分,其中“新生代”用于存放新创建的对象,而“老年代”用于存放已经存在一段时间的对象。
Java虚拟机的垃圾回收器通过标记清除(Mark and Sweep)或标记复制(Mark and Copy)等算法来进行垃圾回收。当对象不再被任何引用所指向时,垃圾回收器就会将其所占用的内存空间标记为可回收的,等待下一次垃圾回收时进行释放。
虽然Java程序可以自动进行内存管理,但是如果程序中存在内存泄漏、对象过多等问题,堆内存依旧会被耗尽,导致程序崩溃。因此,开发人员还应该考虑如何对程序的内存使用进行优化,减少不必要的对象创建和内存占用,以保证程序的稳定性和性能。
特点:
堆(Heap)内存溢出是指当程序需要向堆中分配内存时,堆内存无法满足要求而抛出内存溢出异常。通常情况下,堆内存溢出是由以下原因引起的:
程序中创建了大量的对象- 如果程序中存在过多的对象,那么就会占用大量的堆内存空间。在此期间,内存占用的大小会超过指定的堆内存大小而导致内存溢出异常。
长时间运行的Java进程- 如果程序在长时间运行的过程中,一直不释放对象所占用的内存,就有可能导致堆内存耗尽而抛出内存溢出异常。
堆内存区域配置不当- 如果分配给堆内存区域的内存不够或者分配的大小小于程序的要求,就会出现内存溢出的情况。
在Java虚拟机中,当出现堆内存溢出时,Java虚拟机就会抛出OutOfMemoryError异常。通常来说,可以通过增加堆内存的容量或者优化程序的内存使用情况来避免内存溢出的问题。如果程序出现堆内存溢出的情况,在分析日志之后通常可以辨认出是否需要增加堆内存容量还是需要优化程序的内存使用情况。
进行堆内存诊断通常需要使用一些工具来进行。下面介绍几种堆内存诊断方法:
内存分析工具
使用内存分析工具(例如Eclipse Memory Analyzer或VisualVM)可以生成Dump文件,然后对其进行分析、寻找问题所在。
设置JVM参数
通过设置JVM参数,可以收集并输出关于堆内存使用情况的日志信息。例如,设置JVM参数-XX:+HeapDumpOnOutOfMemoryError可以在出现内存溢出错误时自动记录一个heap dump文件。
分析GC日志
通过分析GC日志,可以了解更多有关Java虚拟机中的对象创建和垃圾回收的信息。可以使用GC日志分析工具(例如GCEasy)来帮助分析GC日志。
手工排查代码
手工排查代码是一种最基本的堆内存诊断方法。通过检查代码中有没有循环创建对象实例,重复执行的操作等,找出内存泄漏的原因。
进行压力测试
通过对程序进行压力测试,可以模拟程序在不同负载情况下的性能表现,发现程序运行过程中所使用的堆内存大小是否超出了预期。
总之,堆内存诊断需要多方面的考虑,结合不同方法得出的结果进行分析,才能快速定位问题并解决问题。
常用方法如下:
在Java虚拟机中,方法区(Method Area)又称为永久代(Permanent Generation),是用于存储类的元数据的区域,是堆内存的一部分,专门用来存储已加载的类、常量、静态变量、即时编译器编译后的代码等信息。方法区被所有线程共享,它的大小在启动时就已经确定,并且会在运行时动态调整。从JDK 8之后,Java虚拟机中的方法区被移除,被元数据区(MetaSpace)所取代。
Java虚拟机中的方法区(永久代)通常包括以下几个重要的部分:
类的元数据信息:包括类的名称、属性和方法的特征和访问权限等信息。
静态变量:由所有实例共享的数据,通常包括静态变量和常量。
字符串常量池:Java虚拟机中的字符串常量池保存着字符串类型的常量值。
即时编译器编译后的代码:即时编译器将Java代码编译成本地代码执行,这些编译后的代码也存放在方法区中。
在Java虚拟机的运行过程中,方法区的大小会动态调整以适应运行的需要。方法区内存的分配和垃圾回收都由Java虚拟机自行管理,并且不需要程序员显式地管理它。但是,如果方法区中存放的类或常量过多,也可能导致内存泄漏或堆内存溢出等问题。因此,在Java虚拟机中,通过调整方法区的大小和设置垃圾回收策略来管理方法区内存使用已变得尤为重要。
1.8和1.8之前的内存结构对比图:
在JVM中,方法区(也称为永久代)是存储类、方法、常量等元数据的地方。如果一个应用程序使用的是大量的类(例如,有许多大型框架和库),那么可能会导致方法区的内存使用量急剧增加。如果方法区的内存使用量超过了JVM为其分配的内存,就会发生方法区内存溢出。
下面列举了几种可能导致JVM中方法区内存溢出的场景:
大规模地使用反射:反射是指在运行时动态地加载类或调用类中的方法。使用反射可能会导致大量的类加载和卸载,导致方法区内存使用量增加。如果应用程序中存在大量的反射操作,则可能会导致方法区内存溢出。
大规模使用动态代理:动态代理是一种机制,通过代理来完成一些操作。在运行时生成代理对象以及相应的代码可能会导致大量的类加载和卸载,导致方法区内存使用量增加。
大规模使用字符串常量:在Java中,字符串常量被维护在方法区中。如果应用程序中存在大量的字符串常量,且没有被垃圾回收,可能会导致方法区内存溢出。
大规模使用大型框架和库:一些大型框架和库可能会带来大量的类、方法、常量等元数据。如果应用程序使用了过多的框架和库,可能会导致方法区内存溢出。
代码动态生成:如果应用程序中存在动态生成代码的情况,例如使用Javassist等工具动态生成类,也可能导致方法区内存溢出。
方法区内存溢出的解决办法可能有以下几种:
增加方法区的内存:可以通过修改JVM参数,增加方法区的最大内存限制。例如,在启动应用程序时添加以下参数:-XX:MaxPermSize=256m
优化代码和库的使用:可以通过减少不必要的类、方法、常量等元数据的使用来降低方法区的内存使用量。
使用新的垃圾回收器:一些新的垃圾回收器对方法区的内存使用效率更高。例如,JVM 7引入了G1垃圾回收器,可以更好地管理方法区内存。
Java虚拟机的运行时常量池是一种在运行时存储常量的表,Java 编译器将常量池数据存储在 .class 文件的常量池中,并且也可以动态的添加。它包含类和接口的常量池,每个类和接口都有独立的常量池。
运行时常量池中存储的是类或者接口的描述符、字段、方法、接口名称等等,也可以存储字面量,例如字符串,数字,布尔值等等。
在Java程序运行时,运行时常量池的数据会被加载到JVM的内存中,在内存中被放置在方法区(JVM被各个线程共享的内存区域)。这个过程是Java程序在执行过程中一次性装载的,而且是在程序启动时自动完成的。
使用运行时常量池的一个最大优点是其使用方便。开发人员可以简单地使用字面量,而无需创建新的String或数字对象,并且Java编译器会自动为常量创建常量表达式,从而使程序更加容易理解和可读。
但是在一些特定的情况下(比如字符串拼接等)会导致频繁的内存使用,影响性能。此时可以通过自动的JIT编译器的优化来缓解这种影响。
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量
池,并把里面的符号地址变为真实地址
StringTable是JVM中一种特殊类型的哈希表,为了提高字符串处理性能优化的一种手段,用于存储字符串对象的引用。StringTable是JVM内部实现的一种优化手段,它会对字符串进行自动去重。在这个表中,每个字符串对象只会有一个唯一的实例存在于内存中。具体实现方式是:当一个字符串创建时,如果StringTable中已有与之对应的字符串,就直接指向已有字符串的实例,否则,就新创建一个字符串,然后添加到StringTable中。由于每个字符串具有唯一的引用,JVM可以更容易地进行优化,并且减少内存的使用,同时提高程序的运行效率。
在JVM中,StringTable会存在于堆内存中,根据JVM的版本不同,StringTable可以使用不同的实现方式。在JDK8之前,StringTable的默认实现方式是固定大小的哈希表;JDK8之后,引入了叫做“红黑树”的新实现方式,以解决哈希表的性能问题。
需要注意的是,由于StringTable是在堆内存中占用空间的,而通常堆内存比较紧张,因此需要多方面考虑字符串的使用,避免不必要的字符串对象的产生,否则会导致内存的浪费和程序性能的下降。
StringTable特性:
StringTable在JVM启动时创建,并且一直存在于JVM中。
StringTable是一种线程安全的结构,多个线程可以同时访问它。
当JVM加载一个新的类时,它会检查类文件中的常量池,并将其中的字符串添加到StringTable中。
如果StringTable中已经存在相同散列码的字符串,则直接返回对该字符串的引用。
StringTable的大小可以通过命令行参数“-XX:StringTableSize”进行调整,默认大小为32768。
在JVM中,String.intern()方法可以将一个字符串添加到StringTable中,如果该字符串在StringTable中已经存在,则返回一个对该字符串的引用。
总之,StringTable是JVM中管理字符串对象的重要机制之一。它实现了字符串的共享,避免了重复创建字符串对象,提高了程序的性能和内存利用率。
StringTable 垃圾回收
由于StringTable是一个固定大小的散列表,它不是一个动态数据结构。因此,JVM不会在StringTable中对字符串对象进行垃圾回收。StringTable中的字符串对象只能在没有对它们的引用时由垃圾回收器回收。
有时候,当应用程序使用大量字符串时,StringTable可能会导致堆内存的膨胀,因为StringTable会一直保存字符串对象的引用,即使这些字符串对象已经不再使用。在这种情况下,可以通过手动调整StringTable的大小来缓解这个问题。您可以使用JVM参数-XX:StringTableSize=来增加或减少StringTable的大小。
总之,JVM不会对StringTable中的字符串对象进行垃圾回收。StringTable中的字符串对象只有在没有对它们的引用时才能被垃圾回收器回收。如果StringTable导致堆内存膨胀,可以通过手动调整它的大小来缓解这个问题。
StringTable 性能调优
StringTable是用于存储字符串常量池中字符串对象的散列表。它允许多个字符串引用相同的字符串对象,使得这些对象只分配一次且在整个JVM中共享,从而提高了内存使用效率。下面是一些StringTable的性能调优技巧:
增加StringTable大小: 默认情况下,StringTable的最大大小为1009,这可能太小,导致哈希冲突。可以使用JVM参数"-XX:StringTableSize=number" 来增加StringTable的大小,从而减少哈希冲突的概率。
禁用StringTable: 如果应用程序中没有大量字符串比较,禁用StringTable可能会提高性能。可以使用JVM参数 “-XX:-UseStringTable” 来禁用StringTable。
禁用字符串重复: 这个选项允许JVM删除具有相同内容的字符串,从而减少内存使用。使用"-XX:+OptimizeStringConcat" 禁用字符串重复操作。
监控StringTable性能: 可以使用JVM参数 “-XX:+PrintStringTableStatistics” 来监视StringTable性能,以及散列表的大小、聚集度等信息。
使用StringBuilder而不是String拼接:String的拼接操作可以通过StringBuilder来优化,这样可以更少地创建临时字符串,减少StringTable中字符串的数量,从而减小了字符串常量池的大小。
总之,在JVM中,StringTable是一个非常重要的机制,对于性能方面也有很大的影响。通过选择合适的大小、禁用StringTable等优化手段,可以提高性能并减少内存使用。但是需要注意,这些调优方法需要根据具体应用场景进行选择。
JVM直接内存(Direct Memory)是一种直接分配在操作系统的地址空间中的内存(也就是系统内存),不受JVM堆大小的限制。与JVM堆内存不同,JVM直接内存是由JVM外部的内存管理库分配的,并由JVM直接访问,而不是通过JVM堆内存进行中介,这样可以在一定程度上提高Java应用程序的性能和效率。
在Java应用程序中,通常使用NIO库实现直接内存的分配和使用。通过使用ByteBuffer或MappedByteBuffer这样的Buffer类,可以直接从JVM里分配一块直接内存,还可以使用DirectByteBuffer类来操作这些直接内存,这些特殊的Buffer类也可以通过JNI调用C或C++程序来操作直接内存。
与JVM堆内存不同,JVM直接内存的分配和回收通常比较复杂,需要开发人员手动管理,否则可能会导致内存泄漏等问题。Java 9引入了一种新的API,jdk.internal.misc.Unsafe,通过这个API实现对JVM直接内存的显式分配和释放,同时也提供了更加高效的内存控制能力。
总之,JVM直接内存是一种直接分配在操作系统地址空间中的内存,主要用于提高Java应用程序的性能和效率。虽然使用JVM直接内存可以带来很多好处,但开发人员需要手动管理JVM直接内存的分配和回收,才能保证程序的稳定性和高效性。
JVM的直接内存是指使用NIO库进行I/O操作时,调用的java.nio.ByteBuffer类中的allocateDirect()方法所分配出的内存空间。直接内存的分配和回收由操作系统负责。
在Java中,有两种内存管理机制,一种是JVM中的堆内存管理,另一种是直接内存管理。直接内存管理不属于JVM内存,而是由操作系统管理,使用Java的NIO库时会经常使用,它与堆内存不同的地方在于分配的内存会被直接分配到物理内存中而不是进程的堆中。
直接内存分配时,可以通过ByteBuffer类的allocateDirect()方法来分配所需大小的内存块。回收时,由于直接内存是由操作系统来管理的,所以JVM是无法直接回收的,需要通过调用DirectByteBuffer.cleaner()方法来触发垃圾回收。
具体来说,每当JVM中的直接内存对象不再被程序使用时,会将其加入到DirectByteBuffer的ReferenceQueue队列中,然后在后台通过Registries类中的register方法来实现垃圾回收。当需要手动回收时,可以使用System.gc()方法或者在代码中显式调用ReferenceQueue对象的poll()方法来执行回收操作。
需要注意的是,直接内存的分配比较耗资源,建议在使用时按需分配,并且需要手动回收。
概括:
JVM(Java Virtual Machine)是Java语言的核心,它是一个运行Java字节码的虚拟机。JVM中垃圾回收是Java程序中的重要概念,在Java中使用垃圾回收机制可以提高内存使用效率和程序性能,降低程序出错几率。
垃圾回收是指自动扫描动态分配的内存区域,找出那些已经不再使用的对象并且将它们释放,以便供将来的内存分配。JVM中垃圾回收的实现主要依赖于下面四个算法:
标记-清除算法
这是最基本的算法,它的核心思路:先标记出所有需要回收的对象,然后统一清除。这种方法的缺点显而易见,清理后会产生大量不连续的碎片化空间,如果内存碎片太多,可能会导致以后需要分配大对象时空间不足。解决方法是进行内存空间整理,将存活的对象往一端移动然后直接清理掉端部的垃圾。
复制算法
复制算法的思路:将内存分为大小相等的两个区域,每次只使用其中的一部分,当这部分内存用完了,就将还活着的对象全部复制到另一部分的未使用区域中,并将原来的区域全部清空。这种算法每次可以清理一半的垃圾,但是需要对内存空间进行大量的复制,如果存活对象太多,会导致效率降低。
标记-整理算法
标记-整理算法的思路:标记出需要回收的对象,然后死亡对象的空间全部向一端移动,然后直接清理掉端部的垃圾,这样可以解决标记-清除算法产生碎片的问题。
分代收集算法
分代收集算法的思路:将Java堆分为新生代和老年代,按照不同的算法来处理不同区域的对象。新生代通常用复制算法回收,老年代通常采用标记-整理算法进行回收。
垃圾回收是JVM内存管理的重要概念。虽然各种算法在实现上有不同的优缺点,但是在实践中需要根据使用环境来灵活地选择不同的算法。具体介绍如下:
在JVM中,垃圾回收器通过判定对象是否能被回收来决定是否对其进行回收。判断对象是否 “可回收” 通常需要执行下面两个步骤:
引用计数算法是最简单的垃圾回收算法之一,它的思路很简单:给对象添加一个引用计数器,当对象新建或引用它时,计数器值加一,当对象的引用被移动或被删除时,计数器值减一。当计数器计数值为0时,就认为对象可以被垃圾回收器回收。
但是,引用计数算法很难解决对象之间的循环依赖的问题,循环依赖的对象会相互引用,导致计数器的值不为0,导致无法被回收。因此,Java中不使用引用计数算法进行垃圾回收。
现在Java中的垃圾回收主要使用可达性分析算法(Reachability Analysis)。即从进行垃圾回收的对象开始,沿着对象之间的引用链,通过一系列的复杂规则判断对象是否仍然需要被保留。如果无法被递归引用,就可以被垃圾回收器回收。
在这个算法中,“根集” 是一个固定清单,包括对象的静态变量引用、活动线程栈中的引用和JNI引用,具体介绍如下:在 Java 中,GC Root 对象是指不能回收的对象,可以作为 GC Root 的对象包括以下几类:
JVM 栈(也称为线程栈)
JVM 栈中保存线程运行时的状态,包括局部变量、方法参数、返回地址等信息。JVM 栈中的对象是最短命的对象,当线程结束,对象也就不存在了,它们可以作为 GC Root。
方法区
方法区是 JVM 运行时存储类信息、常量池等数据的区域,包括静态变量、类属性等。在方法区中的对象是被所有线程共享的,它们可以作为 GC Root。
本地方法栈
本地方法栈是为 Java 语言编写的本地方法而分配的内存区域,与 JVM 栈相似。本地方法栈中的对象也是最短命的对象,当本地方法结束,对象就不存在了,它们可以作为 GC Root。
JNI 引用类型
JNI(Java Native Interface)是一种允许 Java 代码与本地代码进行交互的机制,JNI 中用的是本地引用(Local Reference)类型的对象。这些对象在本地方法中会被申请,它们可以作为 GC Root 对象。
GC Root 对象是指不能回收的对象,只要它们是被虚拟机所引用的,就不会被垃圾回收器回收。JAVA 中用作 GC Root 的对象主要包括 JVM 栈、方法区、本地方法栈和 JNI 引用类型。
如果一个对象不在根集中,也不被在根集中的对象直接或间接引用,那么这个对象就是不可达的,被判定为垃圾对象,并且会被垃圾回收器定期从内存中清理出去。
总的来说,可达性分析算法弥补了引用计数算法的缺陷,并且是现代JVM垃圾回收算法的主要方法。它对于Java程序员而言是透明的,因为Java自己控制管理着引用,而开发人员不用关注对象的回收。
在Java类库中,引用是一种对象,用于存储与对象关联的指针或引用。在JVM中,Java语言定义了4种引用类型:强引用、软引用、弱引用和虚引用。下面对它们逐一进行介绍:
Object object = new Object(); // 该对象具有强引用应用场景:在正常情况下使用,例如像我们平时引用对象时所使用的变量。
示例代码:
Object object = new Object();
SoftReference
示例代码:
Object object = new Object();
WeakReference
示例代码:
Object object = new Object();
ReferenceQueue
总的来说,Java中的四种引用类型各自具有不同的特点和应用场景。软引用和弱引用可以用来实现高速缓存以及对一些内存占用较大的对象进行优化管理等;虚引用主要用于跟踪对象被垃圾回收器回收这个过程。
除上述之外,还有在 自强引用和终结器引用。这两种引用类型虽然都不常用,但是在某些场景下还是很有用的。
自强引用是 Java 8 中引入的一种引用类型,也称为终结引用或者 finalize reference,被用来描述只能被引用一次的对象。使用自强引用,可以创建一个对象并只将它使用一次,因为在使用之后就不能再次更换所引用的对象了。相比于强引用而言,自强引用并不阻止被引用对象被垃圾回收器回收。一旦使用了某个对象的自强引用,就必须等待它被删除,才能被垃圾回收器回收。
终结器引用(Final Reference)是 Java 中的一种特殊引用类型,与对象的终结器相关。如果我们要用一个终结器来清理无用的对象,就需要创建一个与该对象绑定的终结器引用。当垃圾回收器准备回收具有终结器的对象时,它将首先注册要执行的终结器。然后,如果已经没有任何强引用对象引用该对象,垃圾回收器就会调用对应的终结器对象的 finalize() 方法。
区别:
总结:
自强引用和终结器引用都是 Java 中的引用类型,它们都有一定的使用场景。自强引用主要用于描述只能被引用一次的对象,而终结器引用主要用于清理无用对象。开发人员应该根据实际需求选择相应的引用类型。
JVM垃圾回收算法指的是 JVM(Java虚拟机)中用于回收垃圾并释放内存的算法。JVM垃圾回收算法根据不同的实现方式,可以分为以下几种:
标记-清除算法(Mark-Sweep Algorithm)是一种简单的垃圾回收算法。该算法的基本思路是分两个阶段来进行垃圾回收操作,首先标记所有的存活对象,然后清除所有未被标记的垃圾对象。
具体来说,标记-清除算法的操作过程如下:
标记阶段(Marking Phase):从根对象(如虚拟机栈中的对象、静态变量等)开始遍历堆中所有对象,将所有存活的对象标记为“存活”。
清除阶段(Sweeping Phase):扫描堆中所有对象,将未被标记为“存活”的对象归入垃圾对象,并释放其占用的内存。由于标记-清除算法释放掉的内存不连续,会产生内存碎片,因此需要进行内存碎片整理操作。
标记-清除算法的优点在于可以执行任意类型的对象的回收,且速度较快,但缺点是可能会产生内存碎片,导致堆内存不连续,进而影响JVM的性能。此外,标记-清除算法的另一个缺点是无法处理循环依赖的对象所产生的“孤岛”,这些孤岛会一直保留在内存中,导致内存泄漏。具体如下图:
标记-整理算法(Mark-Compact Algorithm)是另一种常用的垃圾回收算法。与标记 - 清除算法和复制算法不同,标记-整理算法不是按照对象生存或空闲列表的方式保留或回收空间,而是将所有存活对象压缩到堆的一端,然后清理剩余的内存。
具体来说,标记-整理算法的操作过程如下:
遍历堆中所有对象,标记处所有存活的对象。
将所有存活对象移到堆的一端。
清理堆末尾以外的所有内存,将这些内存设为空闲。
更新内存指针,使其指向空闲列表中的第一个空闲字节。
标记-整理算法相对于标记-清除算法和复制算法的优势在于它可以避免内存碎片的形成和内存利用率的下降。但是,标记-整理算法需要遍历整个堆从而需要更多的处理时间,同时,由于需要将所有的对象压缩到堆的一边,它可能会导致存活对象的移动,因此不适用于大量活动对象的情况。具体如下图:
复制算法(Copying Algorithm)是一种基于内存分配和复制的垃圾回收算法。该算法将可用内存分为两个区域,每次只使用其中一个区域。当一个区域用尽之后,将还存活的对象拷贝到另一个区域中,然后将该区域全部清空,作为下一轮内存分配使用。
具体来说,复制算法的操作过程如下:
将堆内存分为两个大小相等的区域,分别称为From Space和To Space。
当内存申请请求到达时,将新对象分配到From Space中。
当From Space占满时,执行垃圾回收,遍历From Space中存活的对象,将这些对象复制到To Space中。
在复制时,将对象按顺序逐个复制到To Space,并整理到一起,使得To Space中没有任何内存碎片。
清理From Space,将这个区域全部重置为可用的内存空间,作为下一轮内存分配使用。
将From Space和To Space的角色互换,这样下一次内存分配时,就会使用上一次复制时未使用的To Space。
复制算法的优点在于,它能够避免实现内部的碎片化,提高了内存的利用率,并且回收垃圾时非常高效。但是,它的缺点也比较明显,就是需要复制所有存活的对象,在大型内存时,这可能会成为瓶颈,且复制过程可能会浪费50%的空间。具体见下图:
分代回收算法(generational garbage collection)是一种垃圾回收算法,它假设大部分对象的生命周期都很短暂,只有少部分对象的生命周期较长。基于这个假设,分代回收算法将所有的对象分为几代(通常是2代或3代),每一代对象的生命周期不同,垃圾回收策略也不同。
一般分代回收算法分为三代:
每一代对象的垃圾回收策略不同。将第 0 代看成是“年轻代”,因为大部分对象都是在这里被创建和销毁的,因此垃圾回收的频率也比较高。而将第 1 代和第 2 代看成是“老年代”,因为这些对象的生命周期比较长,因此垃圾回收的频率会比较低。
分代回收算法的优点是:它可以针对对象的不同生命周期实行不同的垃圾回收策略,从而提高垃圾回收的效率。缺点是:需要维护多个不同的代,增加了算法的复杂度。
虚拟机参数(VM参数)是用来调整Java虚拟机(JVM)运行时的参数设置。下面列出了一些常见的虚拟机参数,以及它们的作用和默认值:
这些参数可以在启动JVM时通过命令行参数的形式传递给JVM。需要注意的是,这些参数的设置可能会对程序的性能、稳定性和安全性产生影响,需要谨慎使用。同时,不同版本的JVM可能会支持不同的参数,具体参见相应版本的文档。
在JVM(Java虚拟机)中,有如下几种垃圾回收器:
Serial Garbage Collector是JVM默认的垃圾回收器,使用单线程进行垃圾回收,并暂停所有用户线程。这种垃圾回收器适用于小型或临时的应用程序。
-XX:+UseSerialGC = Serial + SerialOld
Parallel Garbage Collector是Serial GC的一个改进版本,使用多线程进行垃圾回收,并且在回收时会暂停所有用户线程。在多核处理器的系统中,Parallel GC能够更好地利用硬件资源,提高回收效率。
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC (默认开启)
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
:Concurrent Mark-Sweep Garbage Collector是一种并发的垃圾回收器,它不会暂停用户线程,所以适用于对系统响应时间有高要求的应用程序。但是CMS GC有一定的局限性,例如在空间碎片化比较严重的情况下,可能会导致回收效率下降。
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld(标记清除)
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
G1 (Garbage-First) 是一种垃圾收集器,旨在为大型Java应用程序提供可预测、高效且低延迟的垃圾收集。它是从JDK 9开始默认使用的垃圾收集器,但对于Java 8和更新的版本,它也可以手动启用。
G1将整个Java堆划分为大小相等的页面,并将它们分为若干个区域。使用这种划分方式与其他垃圾回收器的工作原理不同,它可以避免产生大量的内存碎片,同时可以更好地控制垃圾回收的代价。同时,G1内置了一种应用于垃圾回收的增量算法,它可以避免在应用程序中产生任何明显的停顿影响。
G1的主要目标是缩短垃圾收集暂停时间,以及减少诸如Full GC这样的垃圾收集操作的次数。在G1中,我们可以通过指定以下参数来调整垃圾收集器的行为:
回收阶段
G1是一种适用于大型Java堆的高性能、低延迟的垃圾收集器。在优化性能时,G1的应用可以提高代码的稳定性,并缓解内存碎片、停顿等问题。
G1(Garbage-First)垃圾收集器在进行垃圾回收的过程中,主要分为以下3个阶段:
初始标记(Initial Mark)
初始标记阶段是一个短暂的静止阶段,STW(Stop-the-World)。在此阶段,G1会标记所有的Young区域对象(也就是进行了垃圾收集的区域),以及直接引用老年代对象的根对象。这个过程一般较快。
并发标记(Concurrent Mark)
在初始标记之后,G1就开始了一系列并发的标记作业。这个过程中,G1不断扫描堆的各个区域,并标记包含活动对象的区域。这个阶段会对应用程序的执行产生一定的损耗,但不会中断它们的执行。
最终标记(Final Mark)和清除(Cleanup)
在并发标记阶段结束时,我们可以得到一个标记集,用于确定哪些对象是活动的,哪些对象是不需要保留的。最终标记阶段就是需要进行STW(停顿)的阶段,它会标记在上一个标记周期之后仍活动的对象,并且将这些对象从需要进行清除的集合中排除出去。在这个阶段结束后,G1就可以开始清理未标记的数据,以便将它们供其它进程使用。这个阶段也是一个STW阶段。
在最终标记和清除阶段完成后,G1垃圾收集器就会将所有的内存重新组合在一起,以便下一次垃圾收集。整个收集过程相对较慢,但由于G1采用了增量收集的机制,因此可以在不产生过长暂停的同时,尽快地标记和收集对象,从而减少其可接受范围内的停顿时间。
垃圾回收(Garbage Collection)是一种自动的内存管理机制,在程序执行过程中自动回收不再使用的内存。垃圾回收调优是为了提高垃圾回收的性能和效率。
以下是一些垃圾回收调优的建议:
选择适合场景的垃圾回收算法:不同的垃圾回收算法适用于不同的场景,如 CMS算法适用于低停顿、大堆内存,G1算法适用于多核环境下。
调整垃圾回收器参数:可以通过调整参数优化垃圾回收的性能,如堆大小、堆的初始大小、最大堆大小、Young区大小、Eden与Survivor区之间的比例、晋升年龄等。
避免过度分配对象:过多的对象分配,会导致频繁的垃圾回收,可以通过设计避免或优化算法来减少对象的分配。
最大化对象的重用: 如果一个对象被频繁分配和回收,为了减少内存分配和对象的回收,可以将其状态重置并重用。
尽量减少长时间存活的对象:长时间存活的对象(如缓存对象、静态变量、单例对象等)需要占用较大的内存,并且容易导致Full GC。因此,它们应该被设计成不可变对象、进行合适的过期清理等。
充分利用日志:通过日志了解垃圾回收的状况,根据日志分析,尝试调整堆大小、调整GC策略等方法来优化回收过程。
使用多线程垃圾回收器:在多处理器的环境中,使用多线程垃圾回收器可以显著提高垃圾回收的效率。
总之,垃圾回收调优需要根据实际情况和应用程序的特殊要求进行分析和调整,需要综合考虑垃圾回收算法、堆内存大小、对象分配和长时间存活的对象等因素。
JVM(Java Virtual Machine)调优是Java应用程序性能调优的一部分,它可以提升Java程序的执行效率、减少应用程序的出错率、降低系统负荷等。下面列出了JVM调优领域的一些主要内容:
堆内存的调整:在Java应用程序中,对象都是在堆内存中分配的,因此,堆内存大小的设置对应用程序的性能和稳定性至关重要。当堆内存的空间不足时,就会频繁地进行垃圾回收操作,影响应用程序的性能,所以需要合理调整堆内存大小。
垃圾回收算法的调整:JVM采用多种垃圾回收算法,如标记-清除算法、复制算法、标记-整理算法和分代收集算法等。不同的算法适合不同的场景和需求,应根据所开发应用的实际情况和需求选择适合的垃圾回收算法。
垃圾回收器的调整:除了垃圾回收算法,JVM还提供了多种垃圾回收器,如Serial、Parallel、CMS、G1等垃圾回收器。不同的垃圾回收器适用于不同的情况,通过调整垃圾回收器,可以有效地提高应用程序的性能。
调整编译器参数:JVM还提供了多种即时编译器,如C1、C2、Graal等,通过调整编译器参数,可以有效地提升应用程序的性能。比如增加编译线程数、调整代码缓存大小等。
线程调优:线程是Java应用程序的基本执行单位,线程的数量和优化对应用程序的性能和稳定性至关重要。通过设置最大线程数、调整线程池大小、提高代码并发度等方法,可以进一步提高应用程序的性能。
I/O调优:I/O(Input/Output)是Java应用程序的重要组成部分,I/O的优化对应用程序的性能影响非常大。通过使用NIO(New I/O)、使用缓存区、设置适当的I/O线程池大小等方式,可以提高应用程序的I/O性能。
总之,在JVM调优领域,需要综合分析应用程序的性能瓶颈,针对性地调整Java虚拟机的参数和算法,提高Java应用程序的性能和可用性。
进行 JVM 调优时,我们通常需要确定以下几个目标:
增加吞吐量:某些应用程序需要处理大量事务,因此需要优化 JVM 以提高吞吐量,即单位时间内完成的任务数量。
减少延迟:某些应用程序需要快速响应用户请求,因此需要优化 JVM 以减少延迟,即在转发请求后等待的时间。
降低内存消耗:某些应用程序需要在有限的服务器上运行,因此需要优化 JVM 以降低内存消耗,从而提高应用程序的可扩展性。
优化应用程序的响应时间:某些应用程序需要及时处理某些事件,例如低延迟交易系统,需要尽快处理客户的订单请求。在这种情况下,JVM 调优可以帮助提高应用程序的响应时间。
提高稳定性:某些应用程序可能会出现意外的内存溢出或崩溃。通过对 JVM 进行调优,可以帮助减少这些错误的发生。
避免大型 GC(Garbage Collection)的执行:在大型 GC 的执行时,JVM 会暂时停止应用程序的执行,因此需要通过调整 JVM 的参数来避免大型 GC 的执行,以减少停顿时间。
在确定目标之后,可以根据目标来制定具体的调优策略和措施,以达到优化 JVM 的性能的目的。
有关GarbageFirst后续回单独总结一篇博客
类加载和字节码技术是Java虚拟机(JVM)中的两个重要概念。下面是对它们的一些解释:
JVM类加载(Class loading)是Java虚拟机(JVM)将编写好的Java源代码转化为可执行字节码的过程。当Java程序执行时,JVM会在需要的时候动态地加载类并将其链接和初始化。JVM类加载主要涉及到以下三个步骤:
加载 (Loading) :在加载阶段,JVM需要找到并加载类的二进制字节码,通常是从磁盘文件系统或者网络资源中读取。类加载的目标是一个类,它有一个唯一的类名称,例如 “com.example.MyClass”,并以.class文件的形式存在于某个位置上。
链接 (Linking):在链接阶段,JVM会对类的二进制字节码进行验证、准备和解析。当类对象第一次使用时,JVM会对其进行验证以确保其二进制数据的正确性和安全性。Java虚拟机还需要在内存中为类的静态变量分配空间,这被称为准备(Preparing)阶段。解析(Resolution)阶段是将类和方法的符号引用解析为直接引用。
初始化(Initialization) :在最后一个阶段,JVM会执行类的静态初始化器,并初始化该类的静态变量。这些静态初始化器在类的所有静态变量初始化之后执行,并且只会被执行一次。
JVM类加载器负责执行类加载的过程并创建类的实例。JVM有三个不同的类加载器:引导类加载器、扩展类加载器和应用程序类加载器。其中,引导类加载器负责加载JVM运行时所需的基础类,扩展类加载器负责加载JVM扩展库中的类,而应用程序类加载器则负责加载应用程序中的类。JVM的类加载过程是一个动态、灵活的过程,使得Java语言具有了良好的跨平台和可扩展性。
JVM字节码技术是Java语言的一项核心技术,是通过将Java源代码编译成字节码格式,然后运行在Java虚拟机(JVM)上的过程。以下是JVM字节码技术的一些简要说明:
字节码:字节码是一种中间文件格式,是Java程序最终编译后的结果。字节码是一种面向对象的程序代码,JVM可以将它们转化为程序的可执行代码。字节码是可移植的,使得Java程序具有良好的跨平台能力。
字节码的执行:JVM将字节码加载到内存中并执行程序的指令。JVM密切关联字节码与操作系统,并且在运行时执行优化,以提高程序的性能。
字节码的优势:由于字节码的可移植性和灵活性,Java程序可以声明和调用其他平台上的代码文件,并且可以进行动态代码生成和类加载。此外,字节码技术还可以将Java语言的优点延伸到其他语言,例如Groovy、Scala等。
字节码工具:JVM提供了许多字节码工具,如javap、JVM 统计和调试等,这些工具可以帮助开发人员更好地理解和优化Java程序。
JVM字节码技术是Java语言的一项基本技术,通过将Java源代码编译成字节码格式,并在JVM虚拟机上运行,提供了良好的跨平台能力和灵活性。
JVM类文件是Java源代码最终编译后的产物,是一种与平台无关的、二进制格式的文件。JVM类文件按照一定的结构进行组织,下面是JVM类文件的大致结构:
魔数 (Magic Number):JVM类文件的前四个字节是一个魔数字,它用于标识JVM类文件。Java中的魔数是一个16进制的数字0xCAFEBABE,它用于标识JVM类文件的二进制属性。
版本号 (Version Number):JVM类文件的第5到第8字节分别表示JVM的版本号,其中前两个字节表示次版本号,后两个表示主版本号。
常量池 (Constant Pool):常量池是一个表结构,它包含了各式各样的常量,例如字符串常量、类常量、字段名、方法名等。常量池实质上是类或接口的一个常量值的有序集合。
访问标志 (Access Flags):访问标志是一个16位的无符号整数,用于表示类或接口的访问级别和属性。
类索引、父类索引和接口索引集合:这些索引用于描述类的继承关系和实现接口信息。
字段表和方法表:字段表和方法表是描述类中属性和方法的结构体系,其中方法表还包括指向字节码的引用。
属性表:属性表是一种通用的结构,被用于扩展类和接口的信息。
JVM类文件的结构非常清晰且严谨,这使得JVM能够快速且可靠地执行字节码,在Java程序中具有广泛的应用。
JVM(Java虚拟机)字节码指令是一种抽象的机器指令,是Java语言和JVM之间进行交互的基础,用于定义各种操作,例如变量赋值、函数调用、控制流程等。
以下是常用字节码指令:
加载和存储指令:主要用于对局部变量和数组元素进行操作,例如aload、aload_0、aload_1 等。
算术和逻辑指令:主要用于对数字、布尔类型等数据进行操作,例如iadd、isub、ior等。
类型转换指令:主要用于不同类型之间的转换,例如i2f、f2i、l2i等。
控制流指令:主要用于控制程序的执行流程,例如if_icmpge、goto、tableswitch等。
对象创建和操作指令:主要用于创建和操作对象,例如new、putfield、getfield等。
方法调用和返回指令:主要用于调用方法和返回值,例如invokestatic、invokevirtual、return等。
异常处理指令:主要用于处理异常,例如athrow、try、catch等。
JVM字节码指令是Java语言跨平台的关键之一,保证了Java程序可以在所有兼容JVM的平台上运行,并且字节码指令具备一定的优化能力,可以提高程序的性能和可靠性。
字节码含义:参考文献
自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件
Java工具包中的javap是一个命令行工具,它用于反汇编Java类文件,并提供类的相关信息,如类名称、超类名称、方法和域信息以及常量池等。
一般来说,使用javap工具可以检查Java代码是否正确地编译成Java类文件,并理解Java虚拟机如何使用这些类。
以下是几个常用的javap命令示例:
显示类的基本信息: javap MyClass
显示类及其超类的方法信息: javap -c MyClass
显示类及其超类的所有成员信息: javap -private MyClass
显示类的内部类信息: javap -inner MyClass
显示所有实现接口的信息: javap -verbose MyClass
以上命令是javap命令的一些常见用法。对于更多细节和选项,可以使用javap --help命令来查看javap的完整帮助信息。
[root@localhost ~]# javap -v HelloWorld.class
Classfile /root/HelloWorld.classLast modified Jul 7, 2019; size 597 bytesMD5 checksum 361dca1c3f4ae38644a9cd5060ac6dbcCompiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorldminor version: 0major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #6.#21 // java/lang/Object."":()V#2 = Fieldref #22.#23 //
java/lang/System.out:Ljava/io/PrintStream;#3 = String #24 // hello world#4 = Methodref #25.#26 // java/io/PrintStream.println:
(Ljava/lang/String;)V#5 = Class #27 // cn/itcast/jvm/t5/HelloWorld#6 = Class #28 // java/lang/Object#7 = Utf8 #8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 LocalVariableTable#12 = Utf8 this#13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;#14 = Utf8 main#15 = Utf8 ([Ljava/lang/String;)V#16 = Utf8 args#17 = Utf8 [Ljava/lang/String;#18 = Utf8 MethodParameters#19 = Utf8 SourceFile#20 = Utf8 HelloWorld.java#21 = NameAndType #7:#8 // "":()V#22 = Class #29 // java/lang/System#23 = NameAndType #30:#31 // out:Ljava/io/PrintStream;#24 = Utf8 hello world#25 = Class #32 // java/io/PrintStream#26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V#27 = Utf8 cn/itcast/jvm/t5/HelloWorld#28 = Utf8 java/lang/Object#29 = Utf8 java/lang/System#30 = Utf8 out#31 = Utf8 Ljava/io/PrintStream;#32 = Utf8 java/io/PrintStream#33 = Utf8 println#34 = Utf8 (Ljava/lang/String;)V
{public cn.itcast.jvm.t5.HelloWorld();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."
":()V4: returnLineNumberTable:line 4: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field
java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String hello world5: invokevirtual #4 // Method
java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 6: 0line 7: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;MethodParameters:Name Flagsargs
}
JVM(Java虚拟机)编译期是指将Java源代码编译成字节码文件的过程,分为以下两个步骤:
编译
在编译期,Java源代码由Java编译器将其编译成字节码文件(.class),这些字节码文件是可以在JVM上运行的二进制文件。编译器对Java源代码进行类型检查和错误检查,如果代码中存在语法或逻辑错误,编译器将抛出编译异常,并提示错误信息。
优化
经过编译后的字节码文件不是最优化的代码,因为编译器需要考虑到代码的可读性和可维护性等因素,并且并未考虑JVM的执行环境。因此,JVM需要对字节码文件进行优化,以提高代码的性能和效率。JVM的优化主要是通过JIT(即时编译)技术来实现的,JIT会将经常执行的代码编译成本地机器码,从而提高程序的运行速度。
总的来说,JVM编译期处理是将Java源代码转换为可以在JVM上运行的字节码文件,并进行性能优化,以提高代码的运行效率和性能。
public class Candy1 {
}
编译成class后的代码:
public class Candy1 {// 这个无参构造是编译器帮助我们加上的public Candy1() {super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."":()V}
}
这个特性是 JDK 5 开始加入的, 代码片段1 :
public class Candy2 {public static void main(String[] args) {Integer x = 1;int y = x;}
}
这段代码在 JDK 5 之前是无法编译通过的,必须改写为 代码片段2 :
public class Candy2 {public static void main(String[] args) {Integer x = Integer.valueOf(1);int y = x.intValue();}
}
显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2
JVM类加载是JVM在执行Java程序时,将字节码文件加载到内存中,并将其转换为Java对象的过程。它是Java程序运行的一个关键环节。
JVM类加载过程包括了以下几个步骤:
加载(Load):查找并加载类的二进制数据。
验证(Verify):确保被加载的类的正确性,例如检查类的结构、语义等。
准备(Prepare):为类的变量分配内存,并设置默认的初始值。
解析(Resolve):将符号引用解析为直接引用,例如将类的名称解析为类的地址。
初始化(Initialize):执行类的初始化代码,包括为静态变量赋值等等。
JVM类加载器能够加载以下几种类型的类:
系统类:即由JVM自带的类,如java.lang.String等。
扩展类:即由扩展类加载器加载的类,可通过java.ext.dirs系统属性指定扩展类库的路径。
应用类:位于classpath路径下的类,如应用程序自定义的类等。
JVM类加载器是JVM的重要组成部分,它能够支持Java程序的运行和动态加载,是Java程序的基石之一。类加载阶段是JVM在使用一个类的时候,需要将这个类的二进制文件(如.class文件)加载到内存中,并解析为运行时数据结构的过程。这个过程概括以下三个阶段:
JVM会通过类加载器加载类的二进制文件,将其读入JVM内存中,并生成用于描述该类的Class对象。此时,类的静态变量和静态代码块也会被初始化。
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
在这个阶段,JVM会将类中的符号引用(如字段和方法名)解析为具体的内存地址。这个过程可以在类加载阶段之后,但在执行代码之前。验证类是否符合 JVM规范,安全性检查用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行
当类被加载完成,并且静态变量和静态代码块也被初始化后,就会进入初始化阶段。在这个阶段,JVM会执行类中的静态代码块,并且初始化静态变量。这个阶段是JVM启动程序时执行的最后一步。
需要注意的是,类加载阶段是整个JVM执行过程中的一部分,而非Java语言本身的一部分。此外,JVM会根据需要进行加载,解析和初始化操作,具体过程可以根据JVM的实现而有所差异。
Java虚拟机(JVM)中的类加载器(class loader)是一个重要的组件,负责将.class文件加载到Java虚拟机中。一般来说,类加载器会将类文件字节码加载到内存,然后进行解析、验证、初始化等步骤,最终生成对应的类信息,用于在Java虚拟机中创建对象并执行方法。
Java虚拟机所使用的类加载器可以分为三种:
引导类加载器(Bootstrap Class Loader):这是JVM自身所使用的类加载器,它用来加载核心类库,比如java.lang.*等。
扩展类加载器(Extension Class Loader):这个类加载器主要用于加载JVM扩展目录中的类,即$JAVA_HOME/lib/ext目录下的类。
应用程序类加载器(Application Class Loader):这个类加载器是应用程序所使用的类加载器,它负责将classpath指定的目录中的类库加载到Java虚拟机中。
这三种类加载器在JVM中形成类加载器层次结构,也被称为类加载器隔离机制,这个机制的目的是隔离不同的类定义,因为可能会存在同名类,而且不同的类加载器之间是互相独立的,它们加载的类互相不可见。类的唯一性是由其全限定名和定义它的类加载器共同决定的,即同一个类加载器加载了同一个全限定名的类,在内存中只有一份类定义。
在JVM中使用类加载器,通常会先指定一个启动类,即main()方法所在的类。JVM会通过该类的类加载器(一般是应用程序类加载器)加载该类,然后通过该类中的代码来加载其它类。
如果需要使用自定义类加载器,则需要继承ClassLoader类并重写findClass()方法,在该方法中实现自定义的类加载逻辑。如果需要多个类加载器协作完成类加载任务,还需要重写loadClass()方法或使用双亲委派机制。同时,还需要注意类加载器的内存泄漏问题,即被加载的类不能被垃圾回收机制回收导致内存泄漏。
在Java虚拟机中,有三种类加载器:启动类加载器、扩展类加载器和应用程序类加载器。启动类加载器(Bootstrap Class Loader)是Java虚拟机内置的类加载器,主要负责加载JRE(Lib)库中的类,包括Java的核心库(java.、javax.)等。
启动类加载器是所有类加载器中最顶层的类加载器,它没有父加载器。它是由C++编写的,并且在Java虚拟机启动时,它会被创建并加载必要的类。启动类加载器是Java虚拟机的一部分,因此它不属于Java类,而是由Java虚拟机实现的一部分,可以确保Java平台的同一性。
该类加载器将加载JRE(Lib)库的类,此类加载器是Java虚拟机的一部分,它是由Java虚拟机的实现者设计的,它也是Java虚拟机中第一个被加载和初始化的类加载器。
启动类加载器具有以下特点:
启动类加载器是Java虚拟机的一部分,是由C++编写的,不是Java类。
启动类加载器负责加载Java平台核心库,如rt.jar等。
启动类加载器没有父类加载器,是Java类加载器的祖先。
启动类加载器加载的类对于Java应用程序可用。
所有的Java应用程序共享同一个启动类加载器。
总结一下,启动类加载器是Java平台的一部分,在Java虚拟机启动时由Java虚拟机实装载操作系统相关的基本类,不能被Java程序直接引用。启动类加载器没有父类加载器,是Java类加载器的祖先。它主要的功能是加载Java平台核心库,如rt.jar等。反映了Java虚拟机的内部结构,保证了虚拟机的安全和稳定性。
JVM运行期优化(JIT编译期)是指JVM在运行Java代码时,通过监视代码执行情况和分析代码结构,自动对代码进行优化的过程。
在JIT编译期阶段,JVM会将Java字节码转换成本地机器码,这些机器码能够在本地机器上执行,因此可以加速代码的执行。
JVM运行期优化可以帮助程序在运行时获得更好的性能表现。以下是一些JVM运行期优化的示例:
编译器优化:JVM会对代码进行编译优化,将一些重复执行的代码进行合并,同时消除一些数据流中的无效计算,提高代码的执行效率。
热点代码识别:JVM会识别程序中频繁执行的代码块,并优化这些代码,以尽可能地减少执行时间。
边界检查消除:JVM会通过运行时的类型信息,判断代码中是否需要执行边界检查,如果不需要则会消除这些检查,从而提高代码的执行速度。
方法内联:JVM会将一些频繁调用的方法直接嵌入到调用者的代码块中,从而避免了方法调用的开销。
总之,JVM运行期优化是一个自然的过程,可以显著提高Java程序的执行效率。
1.垃圾回收是否涉及栈内存?
垃圾回收通常涉及堆内存而非栈内存。因为堆内存的生命周期是由开发人员在程序运行时动态分配和释放的,而栈内存的生命周期是由程序自动管理的。在程序执行时,栈内存中的变量和函数参数等数据,在该数据所在的函数执行完成后就会被自动删除。因此,栈内存不需要进行垃圾回收处理。
相反,堆内存中的对象不会被自动删除,因此可能会导致内存泄漏等问题。垃圾回收器的作用就是定期地检查堆内存中无用对象,将它们标记为垃圾,并释放它们所占用的内存。
2.栈内存分配越大越好吗?
栈内存分配越大有一定的优点,如自动分配内存、处理方便、分配速度快等,同时也具有自动释放的功能,避免了手动管理内存的风险。但栈内存大小受限制,存储大型数据容易溢出,对于长期保留的数据也不太合适。在实际编程中,应根据具体需求和内存大小等因素综合考虑,选择合适的内存分配方式。
3.方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
4.栈内存溢出?
一般来讲,栈帧过多导致内存溢出,距离来讲就是方法出现了,递归调用。
定位
printf "%x\n" pif
将线程pid转为16进制一个简单的程序
public class HelloJvm{public static void main(String[] args) {System.out.println("Hello JVM!");}
}
执行 javac -parameters -d . HelloJvm.java
编译为 HelloJvm.class 后是这个样子的:
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 14
根据 JVM 规范,类文件结构如下:
ClassFile {u4 magic; --魔数u2 minor_version; --小版本号u2 major_version; --主版本号u2 constant_pool_count; --常量池的信息cp_info constant_pool[constant_pool_count-1]; --常量池的信息u2 access_flags; --访问修饰(public)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];
}
0~3 字节,表示它是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
4~7 字节,表示类的版本 00 34(52) 表示是 Java 8
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18
8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项来获得这个成员变量的【所属类】和【成员变量名】
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
第#3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中 #24 项
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
第#4项 0a 表示一个 Method 信息,00 19(25) 和 00 1a(26) 表示它引用了常量池中 #25 和 #26项来获得这个方法的【所属类】和【方法名】
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
第#5项 07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
第#6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项
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
第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【 】
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
第#8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【()V】其实就是表示无参、无返回值
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
第#9项 01 表示一个 utf8 串,00 04 表示长度,43 6f 64 65 是【Code】
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
第#10项 01 表示一个 utf8 串,00 0f(15) 表示长度,4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65是【LineNumberTable】
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
第#11项 01 表示一个 utf8 串,00 12(18) 表示长度,4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61
62 6c 65是【LocalVariableTable】
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
第#12项 01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是【this】
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
第#13项 01 表示一个 utf8 串,00 1d(29) 表示长度,是【Lcn/itcast/jvm/t5/HelloWorld;】
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
第#14项 01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是【main】
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
第#15项 01 表示一个 utf8 串,00 16(22) 表示长度,是【([Ljava/lang/String;)V】其实就是参数为字符串数组,无返回值
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
第#16项 01 表示一个 utf8 串,00 04 表示长度,是【args】
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
第#17项 01 表示一个 utf8 串,00 13(19) 表示长度,是【[Ljava/lang/String;】
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
第#18项 01 表示一个 utf8 串,00 10(16) 表示长度,是【MethodParameters】
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
第#19项 01 表示一个 utf8 串,00 0a(10) 表示长度,是【SourceFile】
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
第#20项 01 表示一个 utf8 串,00 0f(15) 表示长度,是【HelloWorld.java】
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
第#21项 0c 表示一个 【名+类型】,00 07 00 08 引用了常量池中 #7 #8 两项
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
第#22项 07 表示一个 Class 信息,00 1d(29) 引用了常量池中 #29 项
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
第#23项 0c 表示一个 【名+类型】,00 1e(30) 00 1f (31)引用了常量池中 #30 #31 两项
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
第#24项 01 表示一个 utf8 串,00 0f(15) 表示长度,是【hello world】
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
第#25项 07 表示一个 Class 信息,00 20(32) 引用了常量池中 #32 项
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
第#26项 0c 表示一个 【名+类型】,00 21(33) 00 22(34)引用了常量池中 #33 #34 两项
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
第#27项 01 表示一个 utf8 串,00 1b(27) 表示长度,是【cn/itcast/jvm/t5/HelloWorld】
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
第#28项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/Object】
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
第#29项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/System】
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
第#30项 01 表示一个 utf8 串,00 03 表示长度,是【out】
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
第#31项 01 表示一个 utf8 串,00 15(21) 表示长度,是【Ljava/io/PrintStream;】
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
第#32项 01 表示一个 utf8 串,00 13(19) 表示长度,是【java/io/PrintStream】
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
第#33项 01 表示一个 utf8 串,00 07 表示长度,是【println】
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
第#34项 01 表示一个 utf8 串,00 15(21) 表示长度,是【(Ljava/lang/String;)V】
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
21 表示该 class 是一个类,公共的
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
05 表示根据常量池中 #5 找到本类全限定名
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
06 表示根据常量池中 #6 找到父类全限定名
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
表示接口的数量,本类为 0
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
Flag Name Value Interpretation
ACC_PUBLIC 0x0001 公开的;可以从其外部进入
ACC_FINAL 0x0010 宣布结束;不允许有子类。
ACC_SUPER 0x0020 当被invokspecial指令调用时,对超类方法进行特殊处理
ACC_INTERFACE 0x0200 是接口,而不是类。
ACC_ABSTRACT 0x0400 声明抽象的;不能被实例化。
ACC_SYNTHETIC 0x1000 宣布合成;源代码中没有。
ACC_ANNOTATION 0x2000 声明为注释类型。
ACC_ENUM 0x4000 声明为enum类型。
表示成员变量数量,本类为 0
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
表示方法数量,本类为 2
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
参考文献
上一篇:【C++】非常重要的——多态
下一篇:ES6标准入门