JVM
JVM
MR.XSSJVM
定义:
Java Virture Machine -java的运行环境(java字节码的运行环境)
好处:
一次编写,到处运行
自动内存管理,垃圾回收功能
数组下标越界,越界检查
多态
结构图
相关参数
程序计数器:
作用:记住下一条jvm需要操作的执行地址
是线程私有的,每个线程都独有一个程序计数器
在多线程操作下,每个线程都独立拥有一个程序计数器,当时间片用完时,程序计数器会保存当前运行地址,再次抢占到cpu时间片时,会再次激活该线程
不会发生内存溢出
栈内存
栈(Java Virtual Machine Stacks):线程运行时需要的内存空间,称为虚拟机栈
栈帧:每个方法运行时需要的内存,对应着每次方法调用所需要的内存
活动栈帧:每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
注:
垃圾回收是否涉及栈内存
栈内存在方法执行结束后,会自动弹出栈,不需要GC支持
栈内存是不是分配越大越好
使用 -Xss size命令指定栈大小,不指定默认为1024kb,windows除外,
物理内存是固定的,一个栈对应一个线程,栈内存越大,线程数越小
判断变量是否是线程安全
- 若方法内部变量没有逃离方法的作用范围,他就是线程安全的
- 如果是局部变量引用了对象,并且逃离方法的作用范围,需要考虑线程安全
例子
判断线程是否安全
m1安全,m2不安全,m3不安全
栈内存溢出
Java.lang.StackOverflowError
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出(几乎不会出现)
线程运行诊断
cpu占用过多
定位
- 使用top定位哪个进程对cpu占用过高
- ps H -eo pid,tid,%cpu|grep进程id(用ps命令进一步定位是哪个线程引起cpu占用过高)
- jdk命令: jstack 进程 id
- 可以根据线程id找到问题,进一步定位到代码的行数
程序运行很长时间没有结果
程序可能发生了死锁
Jstack命令查看
死锁
本地方法栈
不是使用java代码编写的代码,调用系统资源
使用native关键字调用本地方法
堆
Heap 堆
通过new关键字,创建对象都会使用堆内存
特点
- 他是线程共享的,堆中对象都需要考虑线程安全问题
- 有垃圾回收机制
堆内存溢出
java.lang.OutOfMemoryError:java heap space
工具
- jps工具
- 查看当前系统有哪些java进程
- jmap工具
只是可以查看某一个时刻堆内存的占用情况
- 查看堆内存占用情况
jconsole工具
- 图形化界面,多功能的检测工具,可以连续监测
jvisualvm
- 可视化jvm内存工具
案例
- 垃圾回收后,内存占有率依然很高
方法区
1.8和其之前的结构
在 JDK 1.8 中,方法区被废弃了,取而代之的是元空间(Metaspace)。元空间是 JVM 中的一个内存区域,用于存储类信息、常量池、静态变量、即时编译器编译后的代码等信息。元空间的存储位置不是在 JVM 进程堆内存中,而是被移动到了本地内存中。在 JVM 启动时,可以用参数来指定元空间的大小,它的默认大小取决于平台的位数(32位或64位)和物理内存大小。可以通过以下命令查看元空间的大小:
1 | java -XX:+PrintFlagsFinal -version | grep MetaspaceSize |
需要注意的是,由于元空间不是在堆内存中,因此它不受默认堆内存限制的影响。如果你的应用程序使用了过多的类的话,可能会出现元空间溢出的情况,这时需要调整元空间的大小。可以使用以下参数调整元空间的大小:
1 | -XX:MetaspaceSize=<n> //设置元空间初始大小为n,默认2147483648B |
组成
运行时常量池(StringTable)
加载常量池中的常量信息,加载到串池里面(StringTable),底层使用hashtable结构,不能够扩容
StringTable特性
常量池中的字符串仅是符号,第一次使用到后才会编程对象
利用串池机制,避免重复创建字符串对象
字符串拼接原理是StringBuilder
字符串常量拼接的原理是编译期优化
可以使用intern()方法,主动将串池中还没有的字符串对象放进串池,如果串池有就放入,如果没有会放入串池,会把串池中的对象返回
常量池
为虚拟机提共查找所用的常量表,找到运行所需要的资源
在 Java 中,StringTable 是一个哈希表(Hash Table),用于加速字符串比较和访问,存储了字符串对象的引用、哈希值等数据。而常量池(Constant Pool)则是在 Java 代码编译期间生成的保存在 Class 文件中的常量表,用于存储各种常量的字面值、符号引用等信息,Java 字符串常量池就属于其中的一种。
需要注意的是,常量池和 StringTable 都是在 Java 内存模型中的一部分,是用于优化程序效率和节省内存空间的重要机制。但它们存储的内容不同,常量池存储各种字面值和符号引用,而 StringTable 则存储字符串对象的引用、哈希值等信息。
StringTable(串池)
结构
示例
示例一
1 | string s1 = "a"; |
示例2
在 Java 中,使用双引号定义的字符串被视为常量字符串,它们会在编译期间被优化并添加到字符串常量池中。而通过 new
运算符创建的字符串对象则会在堆内存中动态分配内存,每次都创建一个新的字符串对象,并且不会被添加到字符串常量池中。
因此通过 new
创建的字符串和双引号字符串是完全不同的两个对象,它们在内存中的地址也不同。例如:
1 | String str1 = "hello"; // 使用双引号定义的字符串 |
上述代码将输出 false
,因为 str1
和 str2
是两个不同的对象,它们在内存中的地址不同。
如果需要比较两个字符串的值是否相等,应该使用 equals()
方法,而不是使用 ==
进行比较。例如:
1 | String str1 = "hello"; |
上述代码将输出 true
,因为 equals()
方法比较的是两个字符串的值是否相等。
示例三
Java中的字符串常量池是通过String.intern()方法实现的。当调用字符串对象的intern()方法时,JVM会先检查常量池中是否已经存在该字符串,如果存在,则返回常量池中该字符串的引用;如果不存在,则在常量池中创建新的字符串,并返回该字符串的引用。
具体地说,String.intern()方法的执行过程如下:
如果调用intern()方法的字符串对象在常量池中已经存在,则直接返回常量池中的该字符串的引用。
如果调用intern()方法的字符串对象在常量池中不存在,则在常量池中创建该字符串的一个副本,并返回副本的引用。
需要注意的是,String.intern()方法在JDK 7之前的实现会将字符串本身存入常量池中,而JDK 7及之后的实现则会将字符串对象在堆内存中的引用存入常量池中。因此,在使用String.intern()方法时需要注意版本兼容性问题。
例如:
1 | String str1 = "test"; // 将字符串赋值给变量,此时该字符串对象已经存储在字符串常量池中 |
需要注意的是,在某些情况下,使用 intern()
方法可能会导致一些性能问题,尤其是当需要存储大量字符串时。因为在添加字符串到常量池时,需要进行字符串比较和哈希计算,这些操作可能比创建新字符串对象的开销更大。因此,应该仔细权衡在使用 intern()
方法时带来的影响。
StringTable垃圾回收机制
StingTable里面的数据在虚拟机堆内存不足时会发生回收,这个串池的回收类似于弱引用的回收,当没有指针指向该字符串时,就会触发GC垃圾回收
StringTable调优
当存在大量重复的字符串数据时,可以考虑将字符串入池intern()操作,减少重复字符串对堆内存的占用情况
直接内存
Direct Memory(操作系统内存)
- 常见于nio操作,用于数据缓冲区
- 分配回收成本比较高,读写性能高
- 不受JVM内存回收管理
文件拷贝操作,使用传统io操作
直接内存使用,java代码可以使用native方法直接访问
释放原理
Java中的直接内存(Direct Memory)是一种通过堆外内存分配的机制,它可以在堆外分配内存Buffer,缓解了GC对于内存回收的压力。直接内存的回收是由JVM自动进行管理的,当对象不可达时,会自动被回收。
通常来说,使用直接内存时需要显式地调用Buffer对象的release()方法以释放该对象所持有的内存资源。当调用release()方法时,JVM会使用一个内存管理器来释放该对象指向的堆外内存。这个内存管理器的具体实现,可能会使用一些操作系统提供的底层API来进行内存释放,例如Linux下的mmap和munmap系统调用。
需要注意的是,直接内存的使用需要谨慎,因为过多的使用直接内存可能会导致内存碎片等问题,导致应用程序出现性能瓶颈。如果使用不当,可能会对系统性能产生较大的影响。在使用直接内存时,需要合理估计内存使用量,以便有效地控制系统内存的使用情况。
垃圾回收(GC)
对象已死?
引用计数法
引用计数法是一种垃圾回收算法,它的基本思想是对堆中的每个对象维护一个引用计数器。当一个对象被引用时,其引用计数器值加一;当一个对象被取消引用时,其引用计数器值减一。当一个对象的引用计数器值为0时,表示该对象已经不再被使用,可以被回收。
引用计数法的优点是实现简单,能够及时回收无用对象。但它也存在一些缺点。首先,引用计数法无法处理循环引用情况,即若干个对象之间形成环状引用,则它们的引用计数器值永远不可能为0,因此这些对象会一直占用内存,从而导致内存泄露。其次,引用计数法需要每个对象维护一个引用计数器,如果程序中存在大量的对象,这种开销可能会比较大。
存在循环引用现象
可达性分析算法
可达性分析算法是现代垃圾回收器中使用的一种主流算法,其基本思想是通过一系列的根对象,即”GC Roots”作为起始点集,从这些节点开始向下搜索,标记所有可达的对象,然后将未标记的对象视为无用对象,进而回收它们所占用的内存。
在Java程序中,GC Roots包括以下几种类型的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
可达性分析算法从这些GC Roots对象开始,搜索它们所引用的对象,如果某个对象已经被标记,则递归搜索该对象所引用的对象,直到搜索完所有可达的对象为止。
在搜索时,对于不同层次的对象,可达性分析算法使用不同的标记方式。例如,在新生代的垃圾回收过程中,使用的是标记-复制算法,其中对象被标记后会被复制到另外一块内存中;而在老年代的垃圾回收过程中,使用的是标记-清除-整理算法,其中对象被标记后会被移动到内存的一端,然后释放其它一端的内存空间。
优点:可以有效地处理循环引用问题,并且能够避免引用计数法的开销。
缺点:在搜索时需要扫描整个堆内存,因此其效率在一定程度上受到堆内存大小的限制。
五种引用
#### 强引用
在Java中,强引用是最基本的引用类型之一,它通常是指通过一个变量来直接引用一个对象。如果一个对象存在强引用,它就不会被垃圾回收器回收,只有当所有的强引用都失效时,该对象的内存才会被回收。
例如,假设我们创建了一个对象 obj,并通过变量 ref 引用它,那么 ref 就是一个强引用。只要 ref 存在,垃圾回收器就不会回收 obj 对象。当 ref 超出作用域或显式将其赋值为 null 时,obj 对象才会成为垃圾回收的目标。
示例代码如下:
1 | Object obj = new Object(); |
需要注意的是,在使用强引用时,要尽量避免出现内存泄漏,即对象已经不再需要,但却因为存在强引用而无法被回收的情况。
软引用
在Java中,软引用是一种比强引用更为弱化的引用类型。如果一个对象只有软引用,那么当虚拟机内存紧张时,这个对象就会被回收。
软引用可以通过 java.lang.ref.SoftReference 类来创建,它可以用来实现缓存和优化内存使用。当 JVM 内存足够时,软引用对应的对象就像强引用一样,不会被回收;但当内存不足时,JVM 就会自动回收这个对象。因此,软引用适用于那些占用内存较大、但仍有一定可用性的对象。
示例代码如下:
1 | Object object = new Object(); |
需要注意的是,软引用对应的对象并不保证会被回收,这取决于 JVM 的内存占用情况。因此,软引用仅能用作缓存等场景,不能依赖软引用来进行精确控制对象生命周期。
弱引用
在Java中,弱引用是比软引用更弱化的引用类型。如果一个对象只有弱引用,那么当垃圾回收器扫描到该对象时,不论内存是否充足,都会将其回收。
弱引用可以通过 java.lang.ref.WeakReference 类来创建,它与软引用一样,可以用于实现缓存和优化内存使用。但与之不同的是,只要假设该对象的弱引用被清除,JVM 就会自动对该对象进行回收。因此,弱引用适用于那些占用内存较大、但不一定需要使用的对象。
示例代码如下:
1 | Object object = new Object(); |
需要注意的是,由于弱引用对应的对象可能已被回收,因此获取弱引用的对象时,需要进行判空处理。并且,使用弱引用应该避免被回收的对象被其他线程访问产生竞争问题。
虚引用(使用引用队列)
在Java中,虚引用是最弱化的引用类型。如果一个对象只有虚引用,那么这个对象在任何时候都可能被回收,甚至在 finalize() 方法被调用之前。
虚引用可以通过 java.lang.ref.PhantomReference 类来创建,不同于弱引用和软引用,虚引用并不影响对象的生命周期。虚引用主要用于跟踪对象是否已被垃圾回收器回收,以及在对象被回收时进行一些必要的清理工作。
示例代码如下:
1 | Object object = new Object(); |
需要注意的是,虚引用不能直接通过 get() 方法获取其对应的对象,只能通过 ReferenceQueue 进行操作。同时,在使用虚引用时,需要确保在虚引用对象被回收时,执行必要清理工作。
终结器引用
终结器引用也是 Java 中的一个比较特殊的引用类型,通常称为“对象的终结器”或“finalize”引用。终结器引用通过重写 Object 类中的 finalize() 方法来实现,在对象被回收之前可以执行一些清理工作。
当 GC 发现一个可回收对象的时候,会把这个对象放到一个队列里面,然后由一个专门的线程在适当的时间调用对象的 finalize() 方法。在 finalize() 方法内部,程序可以实现释放该对象所占用的资源等操作。完成操作后,该对象的内存才会被真正释放。
需要注意的是,finalize() 方法的调用是不可靠的,即不能保证该方法一定会被调用。另外,在 Java 9 版本中,finalize() 方法已经被弃用,推荐使用 cleaner 架构进行资源回收管理。
示例代码如下:
1 | public class MyObject { |
需要注意的是,finalize() 方法一定要在子类中重写,在该方法中调用超类的 finalize() 方法,以确保 finalize() 链得到正确的处理。同时,由于 finalize() 方法会影响垃圾收集性能,因此应该尽量避免在程序中过多地使用该方法。
垃圾回收算法
标记清除算法
速度快,产生内存碎片
会产生内存碎片,当新建对象时,新建对象空间大于内存碎片大小,这时总空间足够,但是由于碎片化,创建对象无法成功,导致内存泄漏
标记整理算法
整理会使对象移动,改变对象引用地址,导致速度较低
复制算法
缺点占用双倍空间
分代垃圾回收
简介
分代垃圾回收是一种在垃圾回收过程中将对象分为几个代(Generation)的机制。一般情况下,把Java Heap里面的内存分为初始代和老年代两个领域,再把新生代分为(伊甸园)Eden区和(幸村区)Survivor区(From Space和To Space)三个区,Survivor区From Space和To Space的角色是交替的。主要目的是根据对象的生命周期将内存划分为不同的区域,并针对不同区域采用不同的垃圾收集算法,以提高垃圾回收效率。
执行流程
分代垃圾回收的执行流程通常包括以下几个步骤:
对象的创建:当程序需要创建一个对象时,VM 将根据对象大小将其放入相应的内存区域中进行创建,一般会被放入 Eden 区域。
Eden 区的垃圾回收:当 Eden 区域填满时,VM 将启动一次 Minor GC(小型垃圾回收),对 Eden 区域中的对象进行垃圾回收。在回收过程中,未被回收的对象将被复制到另一个 Survivor 区域中,并从 Eden 区域中删除。
Survivor 区域的垃圾回收:在 Survivor 区域中,采用复制算法进行垃圾回收。当 Survivor 区域中的 From 区域和 To 区域被占满时,将触发一次 Minor GC,将一部分存活的对象复制到另一个 Survivor 区域。
老年代区域的垃圾回收:当老年代区域的内存空间被占满时,将启动 Major GC(大型垃圾回收),对老年代区域中的对象进行垃圾回收。在回收过程中,暂停应用程序,将未被清理的对象标记后,释放掉未标记的对象。
除此之外,还有一些其他的 GC(如永久代的回收等),不过它们都与分代垃圾回收算
模型图
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊园和 rom 存活的对象使用 copy 复制到 to 中,存活的对象年龄加1并目交换 from to
- minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行当对象寿命超过闻值时,会晋升至老年代,最大寿命是15 (4bit)
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STw的时间更长
垃圾回收器
串行
- 单线程
- 堆内存较小,适合个人电脑
吞吐量优先
- 多线程
- 堆内存较大场景,多核cpu支持
- 单位时间内,STW的时间最短
响应时间优先
- 多线程
- 堆内存较大场景,多核cpu支持
- 尽可能使STW的时间变短
串行
吞吐量优先
java8 默认使用此垃圾回收器
响应时间优先
类加载
类文件结构
Java类文件是Java程序编译后生成的二进制文件,它包含了Java代码编译后的字节码。Java类文件结构包括以下部分:
- 魔数:4个字节的无符号整数,标识该文件是否为Java类文件,其值为0xCAFEBABE。
- 版本信息:2个字节的无符号整数,分别表示该文件采用的Java虚拟机版本和编译器的版本。
- 常量池:包含了类、字段、方法等各种符号引用所需要的常量,它是一张表,表项数量不固定。
- 访问标志:2个字节的无符号整数,表示该类或接口的访问权限控制关键字,如public、private、final等。
- 类索引、父类索引和接口索引集合:3个字段共6个字节,分别表示该类的全限定名、父类的全限定名和该类实现的所有接口。
- 字段表集合和方法表集合:分别描述该类的所有字段和方法,每个字段和方法的表项数量不固定。
- 属性表集合:用于存储与该类相关的所有属性信息,其大小和数量不固定。
Java类文件结构的组合形式是类似于树形结构的,多个部分按照特定的顺序排列,它们共同构成了Java程序的基本结构。
执行步骤
Java虚拟机(JVM)在运行Java程序时会按照一定的顺序加载程序所需的类,类加载的步骤包括:
加载:从文件、网络或其他来源加载字节码文件,并将其转换成该类的运行时数据结构,即JVM能够识别的对象格式。
链接:
验证:保证该字节码文件符合JVM规范,比如是否包含不兼容的版本等错误。
准备:为类的静态变量分配存储空间,并设置默认初始化值。
解析:将类中的符号引用转换为直接引用,并将其与其他类进行连接,这是静态绑定的重要阶段。
初始化:对类的静态字段进行初始化,包括指定字段值或执行静态语句块中的代码。当初始化一个类时,其父类也会被初始化。
需要注意的是,类在运行期间可能会被多次加载,但只会初始化一次。其他的情况再次加载该类时,就会直接使用已加载的类,而不会再次初始化。
此外,类加载还有以下特点:
双亲委派机制:如果一个类加载器收到了加载请求,它会先将加载请求委派给其父类加载器。这样,除了在顶层和底层之外,每个类加载器都有机会处理加载请求。
缓存机制:一旦一个类加载器将某个类加载到了内存中,它就会将该类保存在缓存中。下次请求加载该类时,它就能够直接使用缓存中的类,而无需再次加载和初始化。
可见性:一个类加载器所加载的类可以被同一个类加载器所加载的其他类所访问到,但同一个类加载器所加载的类不能被其他类加载器所加载的类所访问到。这是为了保证类的隔离性和安全性。
javap工具
javap是Java开发工具包(JDK)自带的一个命令行工具,用于查看Java类文件的反汇编结果。
使用javap命令,你需要打开命令行窗口,然后输入以下命令:
1 | javap [-options] [classes] |
其中,[-options]
为可选项,用于设置反汇编的选项,比如 -verbose
选项可以显示更详细的反汇编信息;[classes]
为必需项,指定需要反汇编的Java类文件,可以是 .class 文件、jar 文件或者包含 .class 文件的目录。
下面是一个简单的示例,假设我们需要反汇编 Test.class 文件,我们可以使用以下命令:
1 | javap -verbose Test.class |
该命令可以显示 Test.class 文件的更详细的反汇编信息,包括类的访问修饰符、类名、父类、接口、字段、方法等等。
除了 -verbose
选项外,还可以使用其他选项,如 -c
选项可以显示字节码指令, -l
选项可以显示行号信息, -s
选项可以显示源文件以及调试符号信息等等,具体可查阅 javap 命令的文档。
示例
1 | public class StringDemo { |
javap -v StringDemo.class
1 | Classfile /D:/Java_Project/mybatis/target/classes/com/xu/jvm/StringDemo.class |