jvm介绍
什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,顾名思义它是一个虚拟计算机,也是 Java 程序能够实现跨平台的基础。它的作用是加载 Java 程序,把字节码翻译成机器码再交由 CPU 执行的一个虚拟计算器。
JVM是如何工作的
首先程序在执行之前先要把 Java 代码(.java)转换成字节码(.class),JVM 通过类加载器(ClassLoader)把字节码加载到内存中,但字节码文件是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine) 将字节码翻译成底层机器码,再交由 CPU 去执行,CPU 执行的过程中需要调用本地库接口(Native Interface)来完成整个程序的运行。
说一下 JVM 的主要组成部分?及其作用?
-
class loader 类加载器:加载类文件到内存。Class loader只管加载,只要符合文件结构就加载,至于能否运行,它不负责,那是有Exectution Engine 负责的。
-
Exectution engine :执行引擎也叫解释器,负责解释命令,交由操作系统执行。
-
native interface:本地库接口。本地接口的作用是融合不同的语言为java所用。它使得在 Java 虚拟机(VM) 内部运行的Java 代码能够与用其它编程语言(如 C、C++ 和汇编语言)编写的应用程序和库进行互操作
-
Runtimedata area 运行数据区:运行数据区是jvm的重点,存放着程序运行时所需的数据和信息,我们所有所写的程序都被加载到这里,之后才开始运行。
java内存模型 (内存区域,jvm运行时数据区)
为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。
java源文件.java会被编译成.class文件,.class文件会被类装载器装载到jvm里,所有的数据都在运行时数据区(我们优化的大部分都在运行时数据区,更多的是堆的优化),就由我们jvm的执行引擎负责执行,执行方法就会在虚拟机栈里边进行方法的依次调用,入栈出栈,这些操作,程序走到哪儿,走到哪一行了,程序计数器在记录
所谓的jvm调优实际上99%的情况都是调堆,方法区是一个特殊的堆
JVM的内存被划分5个区域
- 堆区、方法区——这两个区域的数据共享
- 虚拟机栈、本地方法栈、程序计数器——这三个区域的数据私有隔离,不可共享
1.1 堆区
Heap,一个JVM只有一个堆内存,堆内存的大小是可以扩展
的。
堆是JVM内存占用最大,是被所有线程共享的,其唯一的用途就是存放对象实例
:所有的对象实例及数组都在对上进行分配。在实现上也可以是固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是可扩展的,通过 -Xmx 和 -Xms
控制。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError(内存溢出)异常。
堆空间内存分配(默认情况下)
- 老年代 : 三分之二的堆空间
- 年轻代(新生代) : 三分之一的堆空间
- eden区: 8/10 的年轻代空间
- survivor0(幸存者区) : 1/10 的年轻代空间
- survivor1 : 1/10 的年轻代空间
新创建的对象的内存分配策略
- 先会去新生代进行分配,先看eden区内存够不够,够的话直接分配内存,如果不够进行一次YGC(简单GC),主要清理新生代的空间(比如eden区存了10个对象,其中一个对象还在用,其他9个都没用,就会把这9个清除,剩下的一个会放到幸存者区)
一次YGC总是要把eden区清理干净,能到幸存者区到幸存者区,不能到的再放到老年代。 - 如果一个对象经过多次YGC依然存活,就会被移到老年代,
- 如果新生代经过GC后还是放不下,对象就会被放到老年代,如果空间不够,就会进行FGC(全面GC,非常慢,比YGC慢10倍左右),如果还不够,就会报内存溢出异常。
老年代存的都是大对象或者生命力持久的对象。
性能监控的时候一定要避免我们应用经常性发生fgc的问题
-
对象优先在 Eden 区分配。如果 Eden 区没有足够的空间,JVM 会尝试进行一次 Minor GC(轻量级GC),清理 Eden 区和 Survivor 区中的垃圾对象,然后将存活的对象复制到 Survivor 区,并为新对象腾出空间。
-
大对象直接进入老年代。大对象是指需要大量连续内存空间的对象,JVM 可以通过参数来设定大对象的阈值。这样可以避免在年轻代中频繁地进行垃圾回收,提高垃圾回收效率。
-
长期存活的对象进入老年代。对象在年轻代中每经过一次 Minor GC 仍然存活,它的年龄就会增加。当对象的年龄达到一定阈值(可以通过参数设置)时,会被晋升到老年代。
对象晋升老年代的判断
-
随着对象在 Survivor 区中经历多次 Minor GC,当对象年龄达到一定阈值(默认 15)时,会被晋升到老年代。
-
如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象也会直接进入老年代。
-
另外,如果创建的对象占用空间较大,超出了一定的大小限制(大对象),会直接在老年代分配内存。
新生区
- 类:诞生和成长的地方,甚至死亡
- 伊甸园:所有对象都是在 伊甸园 区new出来的
- 幸存者区(0,1)
真理:经过研究,99%的对象都是临时对象!
方法区里面那个小方块儿是常量池。
1.2 方法区
这个区域常驻内存的。这个区域不存在垃圾回收!关闭vm虚拟机就会释放这个区域的内存
方法区也是所有线程共享。方法区(Methed Area)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的字节码等数据。
方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
Jdk1.8之后方法区被元空间取代,元空间使用直接内存
在 JDK 1.8 中,永久代(PermGen)被元空间取代。元空间使用的是本地内存,而不是堆内存。它主要存储类的元数据信息,如类名、方法名、字段信息等。元空间的大小可以通过参数进行调整,但一般情况下,如果不是加载了大量的类,不太容易出现元空间内存不足的情况。
极端情况,一个启动类加载了大量的第三方jar包。或者tomcat部署了太多的应用。或者大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM。
1.3 虚拟机栈
栈:栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存就会被释放。对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就Over了。
我们通常所说的“方法入栈”、“栈区”其实指代的就是虚拟机栈。
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈里面存放的内容:8大基本类型 + 对象引用 + 实例的方法
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
栈运行原理:栈帧
一个栈帧就相当于一个方法,程序正在执行的方法,一定在栈的顶部,执行完之后就会弹出去,直到所有的方法都弹出去
子帧和父帧的意思就是该栈帧的前一个方法和后一个方法。
1.3.1 Java 虚拟机栈会出现两种异常:StackOverFlowError (栈溢出)和OutOfMemoryError(内存不足)。
准确的说,不应该是异常,而是错误(Error)。
- StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
- OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
1.3.2 说一下堆栈的区别
-
功能方面:堆是用来存放对象的,栈是用来执行程序的。
-
共享性:堆是线程共享的,栈是线程私有的。
-
空间大小:堆大小远远大于栈。
1.4 本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
// 凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库
// 会进入本地方法栈
// 然后本地方法栈会调用本地方法接口(JNI)JAVA NATIVE INTERFACE
// 本地方法接口又调用本地方法库,
// 这样做的目的是为了扩展java的使用(JNI的作用),比如java要去调用python、c++等,都可以用这样的方式去实现
private native void start0();
1.5 程序计数器
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,也就是任何时刻,一个处理器(或者说一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器。
如果线程正在执行 Java 中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是 Native 方法,这个计数器就为空(undefined),因此该内存区域是唯一一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 的区域。
什么是符号引用
好如有一个类 A,其中调用了另一个类 B 的方法。在编译阶段,类 A 对类 B 方法的引用只是一个符号引用,可能只是用类 B 的全限定名加上方法名和参数类型等信息来表示这个引用。
假设类 B 的全限定名为“com.example.B”,方法名为“doSomething”,参数为空。在编译后的类 A 的字节码中,对类 B 的这个方法的引用就会以符号引用的形式存在,比如“ com.example.B.doSomething()” 这样的描述。
但是在程序运行时,虚拟机需要确切地知道这个方法在内存中的位置才能执行它。在类加载的解析阶段,虚拟机会将这个符号引用转换为直接引用。如果类 B 的这个方法在内存中的起始地址是 0x12345678,那么转换后的直接引用就会指向这个地址。这样,当类 A 要调用类 B 的这个方法时,就可以通过这个直接引用快速准确地找到方法的实际位置并执行它。
2. Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)
下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
2.1 类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2.2 分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:(补充内容,需要掌握)
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
2.3 初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
2.4 设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置
,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中
。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
2.5 执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
3. 对象的访问定位的两种方式(句柄和直接指针两种方式)
建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针
两种:
句柄
: 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
直接指针
: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
第二部分–JVM垃圾回收
简述 java 垃圾回收机制?
在 java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。
在JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
from和to是会来回变化的,就好像是结合算法那样,
老年代默认是在新生代经过了15次GC都还没有死的对象
每次GC后都Eden区和to区都是空的了,
1. 如何判断对象是否死亡(是否可以被回收?GC判定的两种方法)
引用计数法
引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
可达性分析算法 通过一种GC ROOT的对象,可以作为GC Root根节点的对象有:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(即一般说的Native方法)引用的对象)来判断,如果有一条链能够到达GC ROOT就说明,对象还在被引用,不能到达GC ROOT就说明对象已经不再被引用,可以回收
2. 引用的分类
- 强引用
发生 gc 的时候不会被回收
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。- 软引用(SoftReference)
有用但不是必须的对象,在发生内存溢出之前会被回收
如果一个对象只具有软引用。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。(例如mybatis)- 弱引用(WeakReference)
有用但不是必须的对象,在下一次GC时会被回收
如果一个对象只具有弱引用。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。- 虚引用(PhantomReference) "虚引用"顾名思义,就是形同虚设。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
为对象设置虚引用的目的只有一个,就是当着个对象被收集器回收时收到一条系统通知。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
3. 如何判断一个常量是废弃常量?
假如在常量池中存在字符串 “abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。
4. 如何判断一个类是无用的类?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
5. 垃圾收集有哪些算法?
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
5.1 标记-清除算法
标记-清除算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,
但是会带来两个明显的问题:效率不高,扫描两次,严重浪费时间
空间问题(标记清除后会产生大量不连续的碎片)
优点是:不需要额外的空间!
5.2 复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
复制算法最佳使用场景:对象存活度较低的时候,新生区
5.3 标记-整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。可以防止内存碎片的产生
5.4 分代收集算法
根据对象存活周期的不同将内存划分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,所以我们选择“标记-清除”或“标记-整理”算法进行垃圾收集
总结
内存效率:复制算法(1次)、标记清除算法(2次)、标记压缩算法(更多),其实是个时间复杂度的问题
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
6. HotSpot为什么要分为新生代和老年代?
将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集
7. 垃圾回收器的分类
这里介绍的是最简单的两种吧,有时间再看其他的
CMS 一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统。
G1:一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项
8. 介绍一下CMS,G1收集器
G1 收集器(Garbage-First)(xhs上有说公司用这个垃圾回收器的)
-
优势:
-
可以同时注重低延迟和高吞吐量。它能够在不牺牲大量吞吐量的情况下,尽可能地减少垃圾回收导致的停顿时间,这对于很多对响应时间有要求的应用非常关键。
-
适用于大内存应用。随着内存的不断增大,传统的垃圾回收器在处理大堆内存时可能会面临较长的停顿时间,而 G1 收集器通过将堆划分为多个区域,能够更好地管理和回收大内存空间。
-
预测停顿时间。G1 可以根据用户设定的目标停顿时间来制定回收计划,尽量在满足停顿时间要求的同时,提高垃圾回收的效率。
适用场景:
-
大型企业级应用,特别是那些需要同时满足低延迟和高吞吐量要求的系统。例如,金融交易系统、大型电商平台等,这些应用对响应时间敏感,同时又需要处理大量的数据和高并发的请求。
具有大堆内存的应用,如内存占用几十 GB 甚至上百 GB 的数据分析处理系统、大规模分布式缓存等。
CMS 收集器(Concurrent Mark Sweep)
-
优势:
-
以获取最短回收停顿时间为目标,在垃圾回收过程中,大部分阶段都可以与用户线程并发执行,从而减少了垃圾回收对应用程序的影响,提高了系统的响应速度。
-
对于老年代的垃圾回收比较高效,采用标记 - 清除算法,避免了在回收过程中进行大量的对象移动,减少了内存碎片的产生。
-
-
适用场景:
-
对响应时间要求较高的应用,如互联网 Web 应用、实时交互系统等。这些应用需要快速响应用户请求,不能因为垃圾回收导致长时间的停顿。
-
适合在老年代对象增长相对缓慢的场景下使用,因为 CMS 收集器在老年代空间占用达到一定比例时才会触发回收,如果老年代对象增长过快,可能会导致频繁的回收,反而影响系统性能。
-
综上所述,G1 收集器和 CMS 收集器在不同的应用场景下都有广泛的应用,开发人员可以根据具体的业务需求和系统特点选择合适的垃圾回收器。
9. Minor Gc和Full GC 有什么不同呢?
新生代内存不够用时候发生 MGC,JVM 内存不够的时候发生 FGC
Minor GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC:指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor
GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。
Full GC清理整个heap区,包括新生代和老年代。
第三部分–类加载机制
简述类加载机制
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的 java 类型。
1. 简单介绍一下Class类文件结构(常量池主要存放的是那两大常量?Class文件的继承关系是如何确定的?字段表、方法表、属性表主要包含那些信息?)
一、魔数与版本号
- 魔数(Magic Number):
Java 的.class文件开头的 4 个字节为魔数,固定值是 0xCAFEBABE。这个魔数主要用于标识文件是有效的 Java 类文件,在读取文件时,可通过检查魔数快速判断文件类型是否正确。
- 版本号:
紧挨着魔数的 4 个字节是次版本号和主版本号,各占 2 个字节。版本号能标识该类文件是由哪个版本的 Java 编译器编译生成的,不同版本的 Java 虚拟机对类文件的版本有不同的支持范围。
二、常量池
- 概述:
常量池在.class文件中占据重要地位,它存储了各种字面量(如字符串常量、整数常量等)和符号引用(如类名、方法名、字段名等)。
常量池的数量不固定,第一个常量的索引从 1 开始。
- 常量类型:
常量池中包含多种不同类型的常量,例如 CONSTANT_Utf8_info(表示字符串常量)、CONSTANT_Integer_info(表示整数常量)、CONSTANT_Class_info(表示类或接口的符号引用)等。
三、访问标志
- 作用:
占用 2 个字节,用于标识类或接口的访问信息,例如是否是 public、是否是 abstract、是否是 final 等。
- 标志含义:
比如,ACC_PUBLIC 表示类是 public 的,ACC_FINAL 表示类是 final 的,ACC_INTERFACE 表示这是一个接口等。
四、类信息
- 类名、父类名和接口信息:
- 分别用 CONSTANT_Class_info 类型的常量来表示当前类的全限定名、父类的全限定名以及实现的接口列表。
- 字段表:
- 用于描述类或接口中声明的变量,涵盖字段的名称、类型、修饰符等信息。
- 方法表:
- 描述类或接口中声明的方法,包括方法的名称、参数列表、返回值类型、修饰符、字节码指令等信息。
五、属性表
- 概述:
属性表是.class文件中的可选部分,用于存储额外的类相关信息,比如源文件名称、编译器版本等。
- 常见属性:
SourceFile 属性:用于记录生成该类的源文件名称。
InnerClasses 属性:用于描述内部类和宿主类之间的关系。
Deprecated 属性:表示该类、方法或字段已被弃用。
2. 简单说说类加载过程
类加载分为以下 5 个步骤:
- 加载:通过全限定名来加载生成 class 对象到内存中;
- 验证:验证这个 class 文件,包括文件格式校验、元数据验证,字节码校验等,确保文件中的信息符合当前虚拟机的要求。
- 准备: 给类中的静态变量分配内存空间;
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
- 初始化:对静态变量和静态代码块执行初始化工作
3. 类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。
主要有以下三种类加载器(还有一种自定义类加载器,暂时先不了解):
-
启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接引用。
-
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
-
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
4. 什么是类加载器双亲委派模型?
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
第四部分–虚拟机性能监控和故障处理工具(工具挑一两个看看,主要看看参数吧)
1. 垃圾回收的调优参数有哪些?
垃圾回收的常用调优如下:
- -Xmx:512 设置最大堆内存为 512 M;
- -Xms:215 初始堆内存为 215 M;
- -XX:MaxNewSize 设置最大年轻区内存;
- -XX:MaxTenuringThreshold=5 设置新生代对象经过 5 次 GC 晋升到老年代;
- -XX:PretrnureSizeThreshold 设置大对象的值,超过这个值的大对象直接进入老生代;
- -XX:NewRatio 设置分代垃圾回收器新生代和老生代内存占比;
- -XX:SurvivorRatio 设置新生代 Eden、Form Survivor、To Survivor 占比。
堆内存调优
package demo;
public class Demo {
public static void main(String[] args) {
// 返回虚拟机试图使用的最大内存
long maxMemory = Runtime.getRuntime().maxMemory(); // 字节
// 返回jvm的总内存
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("maxMemory=" + maxMemory + "字节\t" + (maxMemory/1024/1024) + "MB");
System.out.println("totalMemory=" + totalMemory + "字节\t" + (totalMemory/1024/1024) + "MB");
// maxMemory=3737649152字节 3564MB
// totalMemory=253231104字节 241MB
// 电脑内存是16G,最大内存约占1/4,总内存约占1/80
// 那么最大内存和总内存相差约20倍,241*20 = 4820,大概差不多
// 默认情况下,分配的总内存 是电脑内存的 1/4,而初始化的内存,是1/64
// -Xms 1024m -Xmx 1024m -XX:+PrintGCDetails
// 我们将内存的最大和最小都设置为1024m,后面的是打印GC的信息,固定格式
// Heap
// PSYoungGen total 305664K, used 15729K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
// eden space 262144K, 6% used [0x00000000eab00000,0x00000000eba5c420,0x00000000fab00000)
// from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
// to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
// ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
// object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
// Metaspace used 3448K, capacity 4496K, committed 4864K, reserved 1056768K
// class space used 376K, capacity 388K, committed 512K, reserved 1048576K
// OOM
// 1. 尝试扩大堆内存
// 2. 分析内存,看一下哪个地方出现了问题(专业工具)
}
}
package demo;
import java.util.Random;
public class Demo02 {
public static void main(String[] args) {
String str = "sadvf";
while (true){
str += str + new Random().nextInt(88888888) + new Random().nextInt(88888888);
}
// [GC (Allocation Failure) [PSYoungGen: 221532K->43495K(305664K)] 221532K->65159K(1005056K), 0.0430900 secs] [Times: user=0.03 sys=0.02, real=0.04 secs]
//[GC (Allocation Failure) [PSYoungGen: 263011K->43351K(305664K)] 284675K->150774K(1005056K), 0.0384791 secs] [Times: user=0.03 sys=0.06, real=0.04 secs]
//[GC (Allocation Failure) [PSYoungGen: 305071K->568K(305664K)] 412494K->279508K(1005056K), 0.0702580 secs] [Times: user=0.30 sys=0.05, real=0.07 secs]
//[GC (Allocation Failure) [PSYoungGen: 176176K->520K(305664K)] 626635K->536737K(1005056K), 0.0298909 secs] [Times: user=0.05 sys=0.03, real=0.03 secs]
//[Full GC (Ergonomics) [PSYoungGen: 520K->0K(305664K)] [ParOldGen: 536217K->257929K(699392K)] 536737K->257929K(1005056K), [Metaspace: 3453K->3453K(1056768K)], 0.0179227 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
//[Full GC (Ergonomics) [PSYoungGen: 181254K->0K(305664K)] [ParOldGen: 600965K->172474K(699392K)] 782220K->172474K(1005056K), [Metaspace: 3454K->3454K(1056768K)], 0.0136565 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
//[GC (Allocation Failure) [PSYoungGen: 171517K->0K(262656K)] 687027K->515509K(962048K), 0.0008897 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[GC (Allocation Failure) [PSYoungGen: 0K->0K(273408K)] 515509K->515509K(972800K), 0.0011893 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
//[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(273408K)] [ParOldGen: 515509K->515206K(699392K)] 515509K->515206K(972800K), [Metaspace: 3454K->3454K(1056768K)], 0.0765700 secs] [Times: user=0.13 sys=0.00, real=0.08 secs]
//[GC (Allocation Failure) [PSYoungGen: 0K->0K(269312K)] 515206K->515206K(968704K), 0.0011450 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(269312K)] [ParOldGen: 515206K->515186K(699392K)] 515206K->515186K(968704K), [Metaspace: 3454K->3454K(1056768K)], 0.0778267 secs] [Times: user=0.25 sys=0.00, real=0.08 secs]
// Heap
// PSYoungGen total 269312K, used 7382K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
// eden space 193536K, 3% used [0x00000000eab00000,0x00000000eb235a28,0x00000000f6800000)
// from space 75776K, 0% used [0x00000000f6800000,0x00000000f6800000,0x00000000fb200000)
// to space 71680K, 0% used [0x00000000fba00000,0x00000000fba00000,0x0000000100000000)
// ParOldGen total 699392K, used 515186K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
// object space 699392K, 73% used [0x00000000c0000000,0x00000000df71ca48,0x00000000eab00000)
// Metaspace used 3485K, capacity 4496K, committed 4864K, reserved 1056768K
// class space used 379K, capacity 388K, committed 512K, reserved 1048576K
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
// at java.util.Arrays.copyOf(Arrays.java:3332)
// at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
// at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
// at java.lang.StringBuilder.append(StringBuilder.java:208)
// at demo.Demo02.main(Demo02.java:11)
}
}
Jprofiler
在项目中突然出现了OOM故障,那么该如何排除~ 研究为什么出错
- 能够看到代码第几行出错:内存快照分析工具,MAT(Eclipse集成),Jprofiler
- Debug,一行行分析代码!线上的做不到
举例子的话
就说电商秒杀服务的订单创建过多,导致OOM,我们可以对秒杀进行限流来解决这个问题
MAT(Eclipse集成),Jprofiler(idea集成)作用
- 分析Dump内存文件,快速定位内存泄漏;
- 获得堆中的数据
- 获得大的对象
Jprofiler还需要一个windows客户端,百度下载即可
package demo;
import java.util.ArrayList;
// Dump内存快照
// 如果需要dump其他异常或错误的信息 -XX:+HeapDumpOn 后面加错误/异常 就行了
// -Xmx8m -Xms1m -XX:+HeapDumpOnOutOfMemoryError
// java.lang.OutOfMemoryError: Java heap space
// Dumping heap to java_pid14664.hprof ...
// Heap dump file created [6752157 bytes in 0.008 secs]
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
// at demo.Demo03.<init>(Demo03.java:9)
// at demo.Demo03.main(Demo03.java:17)
public class Demo03 {
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new Demo03());
count+=1;
}
} catch (Error e) { // OOM是error,不是Exception
System.out.println("count:" + count);
e.printStackTrace();
}
}
}
运行代码的时候-Xmx8m -Xms1m -XX:+HeapDumpOnOutOfMemoryError
,就会在项目跟路径下生成一个.hprof
文件,用Jprofile打开即可
很显然,这里的list集合放了太多的对象了
这里可以看具体的线程信息,我们可以根据线程信息进而定位到错误
线上内存快照怎么生成
#导出存活对象
jmap -dump:live,format=b,file=/usr/local/log/dump.hprof pid
#导出所有对象
jmap -dump:format=b,file=/usr/local/log/dump.hprof pid
jmap和jstack都是 Java 提供的用于诊断 Java 进程的工具,它们的主要区别如下:
一、功能不同
- jmap:
主要用于生成 Java 进程的内存映射信息或堆转储文件(heap dump)。
可以查看 Java 进程的内存使用情况,包括堆内存中各个区域(如新生代、老年代)的使用情况,以及对象在内存中的分布情况。
通过生成堆转储文件,可以使用内存分析工具(如 Eclipse Memory Analyzer Tool,MAT)来分析内存泄漏、大对象占用等问题。
- jstack:
用于生成 Java 进程中所有线程的堆栈跟踪信息。
可以查看线程的状态,如运行、等待、阻塞等,帮助诊断线程死锁、死循环、长时间等待资源等问题。
对于分析多线程应用程序中的线程问题非常有用。
二、输出内容不同
- jmap的输出:
当使用jmap -heap 命令时,会输出 Java 进程的堆内存使用情况,包括堆的大小、使用的垃圾收集器、新生代和老年代的大小等信息。
当使用jmap -dump:format=b,file= 命令时,会生成一个堆转储文件,该文件可以被内存分析工具读取,以进行详细的内存分析。
- jstack的输出:
输出包含每个线程的堆栈跟踪信息,包括线程的 ID、名称、状态以及当前执行的方法栈。
可以帮助开发者快速定位线程问题,例如死锁时会显示出互相等待的线程以及它们持有的锁信息。
三、使用场景不同
- jmap的使用场景:
当怀疑应用程序存在内存泄漏问题时,可以使用jmap生成堆转储文件,然后使用内存分析工具进行分析,找出占用大量内存的对象以及它们的引用关系。
想要了解 Java 进程的内存使用情况,以便进行内存调优时,可以使用jmap -heap命令查看堆内存的详细信息。
- jstack的使用场景:
当应用程序出现卡顿、响应缓慢或者怀疑存在线程死锁、死循环等问题时,可以使用jstack生成线程堆栈跟踪信息,分析线程的状态和执行情况,找出问题所在。
在多线程应用程序的开发和调试过程中,可以使用jstack来查看线程的运行情况,帮助理解程序的执行流程。
部分知识引用自:
https://blog.youkuaiyun.com/u011552404/article/details/80306316
https://blog.youkuaiyun.com/weixin_38896998/article/details/86499993
https://www.cnblogs.com/dailyprogrammer/p/12272769.html
https://www.jianshu.com/p/20ddf72122af
https://zhuanlan.zhihu.com/p/96756501
https://www.imooc.com/article/42827
https://www.bilibili.com/video/BV1iJ411d7jS/?p=4&spm_id_from=pageDriver&vd_source=64c73c596c59837e620fed47fa27ada7
人是有差距的,要承认差距存在,一个人对自己所处的环境,要有满足感,不要不断的攀比。例如:有人少壮不努力,有人十年寒窗苦;有人读书万卷活学活用,有人死记硬背,一部活字典;有人清晨起早锻炼,身体好,有人老睡懒觉,体质差;有人把精力集中在工作上,脑子无论何时何地都像车轱辘一样的转,而有人没有做到这样。……。待遇和处境能一样吗?你们没有对自己付出的努力有一种满足感,就会不断地折磨自己,和痛苦着,真是生在福中不知福。这不是宿命,宿命是人知道差距后,而不努力去改变。
任正非:要快乐地度过充满困难的一生