Java Virtual Machine
Posted 364.99°
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Virtual Machine相关的知识,希望对你有一定的参考价值。
目录
1. 概述
JVM: java程序运行环境(字节码运行环境)。
- 好处:
- 一处编译、到处运行
- 自动内存管理,垃圾回收机制(GC)
- …
2. 内存结构
1. 程序计数器
先看一段简单程序及其字节码:javap -c Demo1.class
java代码执行流程:
程序计数器:
- 作用: 记住下一条jvm指令的地址。
- 二进制字节码前面的数字是下一条指令的地址
- 物理实现:寄存器(速度很快)
- 特点:
- 线程私有
- 不会存在内存溢出
2. 虚拟机栈
1. 概述
栈: 先进后出的数据结构。
虚拟机栈: 线程运行时的内存空间。
- 一个栈由多个栈帧组成,一个栈帧就对应一个方法的调用所占用的内存
- 每个线程只有一个活动栈帧,对应着当前执行的那个方法
- 栈帧内存在每一次方法执行完之后都会弹出栈内存
栈内存溢出原因(StackOverflowError
):
- 栈帧过多(如,不断递归调用)
- 栈内存过大
VM options设置栈内存大小:
-Xss256k
设置每个线程的栈大小。
- jdk5之前,每个栈的大小是 256k,之后是 1M
- 相同物理内存下,减小此值可生成更多线程,但操作系统对于一个进程的线程数是由限制的
- 超出线程数限制,就会报错
StackOverflowError
2. 线程诊断
Linux查看线程占用:
top
查看进程内存、CPU占用(定位进程)ps H -eo pid,tid,%cpu | grep xxxxx
查看线程对CPU的占用(定位线程)- H 打印所有进程和线程
- -eo 规定要输出的内容
- pid,tid,%cpu pid,tid和CPU占用
- grep 过滤进程的条件
jstack 进程id
列出进程中的所有线程(根据线程id—tid的十六进制数去找到目标线程)
排除线程死锁: 迟迟得不到结果。
jstack 进程id
会打印出死锁死锁所在范围
Find one Java-level deadlock
Java stack information for the threads listed above
3. 本地方法栈
本地方法栈: JVM在调用本地方法的时候需要的内存空间。
- 本地方法:不是由Java代码编写的,可以直接与操作系统底层打交道的API(如,Object的clone()、hashCode()、notify()、wait())
4. 堆
1. 概述
堆: 通过 new
,创建对象都会使用堆内存。
- 特点:
- 线程共享,线程安全问题
- 垃圾回收机制
VM options设置堆内存大小:
-Xmx8m
设置JVM最大堆内存为8M
2. 堆内存诊断
jps
查看有哪些java进程(显示:进程id 进程名)
jmap -heap pid
查看堆内存占用情况
jconsole
图形界面检查内存、线程、类…
jvisualvm
5. 方法区
方法区:
- 线程共享区域
- 存储与类结构相关的信息:run-time constant pool、field、method data、methods、constructors
- 虚拟机启动时创建
- 逻辑上是堆的组成部分
方法区内存溢出: 1.8之前是永久代内存不足导致内存溢出、1.8之后是元空间内存不足导致内存溢出。
VM options设置永久代最大保留区域(了解即可):
-XX:MaxPermSize=2048m
在1.8已经弃用。
VM options设置元空间:
-XX:MetaspaceSize=100m
设置元空间初始大小为100M-XX:MaxMetaspaceSize=100m
设置元空间最大可分配大小为100M
通过字节码动态生成类的包:CGLIB
1. 运行时常量池
常量池: 就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池: 常量池是 *.class
文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
2. String Table
- 字符串的创建过程:
请点击跳转我的文章: 从字节码分析字符串是否相等
- 字符串延迟加载(实例化):
通过debug可以发现,只有当使用到某个字符串的时候,才会放入到串池(String Table)中,而且,不会重复放置相同的字符串。
这就是延迟加载机制。
串池的位置:
- 1.6 存在永久代中,内存溢出的时候会报错:
java.lang.OutOfMemoryError: PermGen space
- 1.8 存在堆中,内存溢出会报错:
java.lang.OutOfMemoryError: Java heap space
,java.lang.OutOfMemoryError: GC overhead limit exceeded
-
设置堆最大内存为4M:
-Xmx4m
-
代码:
public class TestStringTableLocation public static void main(String[] args) List<String> list = new ArrayList<>(); int i = 0; try for (int j = 0; j < 10000000; j++) list.add(String.valueOf(j).intern()); i++; catch (Throwable e) e.printStackTrace(); finally System.out.println(i);
上述报错的原因:当98%的时间花在了垃圾回收上面,但是只有2%的堆空间被回收了,JVM就会放弃垃圾回收,直接报错(并不会报堆空间不足的错)
如果想要报堆空间不足的错,就需要将上述的机制关掉:完整的虚拟机参数:-Xmx4m -XX:-UseGCOverheadLimit
-
演示串池的垃圾回收:
虚拟机参数:-Xmx4m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
-XX:+PrintStringTableStatistics
:打印有关StringTable和SymbolTable的统计信息(+是开启、-是关闭)-XX:+PrintGCDetails
: 打印输出详细的GC收集日志的信息-verbose:gc
:在控制台输出GC情况
public class TestStringTableGC
public static void main(String[] args)
int i = 0;
try
catch (Throwable e)
e.printStackTrace();
finally
System.out.println(i);
public class TestStringTableGC
public static void main(String[] args)
int i = 0;
try
for (int j = 0; j < 100; j++)
String.valueOf(j).intern();
i++;
catch (Throwable e)
e.printStackTrace();
finally
System.out.println(i);
通过不断修改循环的上限值,可以从控制台看到GC回收被触发:
性能调优:
通过上述案例我们可以知道,String Table采用的是桶机制,所以:
- 当桶足够多的时候,桶元素发生hash碰撞的几率就更小,查找速度就会增快
- 当桶的数量较少的时候,桶元素发生hash碰撞的几率就更大,导致链表更长,从而降低查找速度
所以String Table的性能调优就是修改桶的个数。
虚拟机参数:
-Xms500m
设置堆内存初始值为500m-Xmx500m
设置堆最大内存为500M-XX:+PrintStringTableStatistics
打印有关StringTable和SymbolTable的统计信息-XX:StringTableSize=20000
设置串池的桶个数为20000
public class TestStringTableOptimization
public static void main(String[] args) throws IOException
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\\\YH\\\\examtest20210723.sql"))))
String line = null;
long start = System.nanoTime();
while (true)
line = reader.readLine();
if (line == null)
break;
line.intern();
System.out.println("const:" + (System.nanoTime() - start) / 100000);
当桶的个数为2000时: -XX:StringTableSize=20000
当桶的个数为10000时: -XX:StringTableSize=10000
当桶的个数为1009时: -XX:StringTableSize=1009
字符串入串池的优点: 极大节约了内存占用(重复的字符串只会在串池中存储一个)
-
不存入串池
list.add(line);
public class TestStringTableOptimization public static void main(String[] args) throws IOException List<String> list = new ArrayList<>(); System.in.read(); for (int i = 0; i < 10; i++) try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\\\YH\\\\examtest20210723.sql")))) String line = null; long start = System.nanoTime(); while (true) line = reader.readLine(); if (line == null) break; /*line.intern();*/ list.add(line); System.out.println("const:" + (System.nanoTime() - start) / 100000); System.in.read();
-
存入串池:
list.add(line.intern());
3. 直接内存
直接内存:
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
不使用直接内存:
使用直接内存:
直接内存使得java代码能直接读取到系统内存的数据,极大缩减了代码执行时间。
分配直接内存缓冲区的方法:
public static ByteBuffer allocateDirect(int capacity)
return new DirectByteBuffer(capacity);
直接内存溢出演示:
public class TestDirectOut
static int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args)
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try
while (true)
// allocateDirect分配多少内存,就会占用本地多少内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
finally
System.out.println(i);
分配和回收原理:
- 使用了
Unsafe
对象完成直接内存的分配回收,并且回收需要主动调用freeMemory
方法 ByteBuffer
的实现类内部,使用了Cleaner
(虚引用)来监测ByteBuffer
对象,一旦ByteBuffer
对象被垃圾回收,那么就会由ReferenceHandler
线程通过Cleaner
的clean
方法调用freeMemory
来释放直接内存
JVM调优常用参数: -XX:+DisableExplicitGC
- 让显示的垃圾回收无效(即直接手动敲代码回收,如
System.gc()
) - 因为显示的垃圾回收是一种 Full gc 即,要回收新生代,还要回收老年代,会造成较长的代码停留时间
但是,禁用掉显示的垃圾回收之后,直接内存的回收就只能依靠 Cleaner
来检测回收了,这样就会导致直接内存长时间得不到释放。
public static void main(String[] args) throws IOException
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
这时候就需要使用Unsafe
来手动回收内存了:
public static void main(String[] args) throws IOException
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
public static Unsafe getUnsafe()
try
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
catch (NoSuchFieldException | IllegalAccessException e)
throw new RuntimeException(e);
3. 垃圾回收
1. 判断对象可以被回收的算法
1. 引用计数法
引用计数法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器就减1。任何时刻计数器为0的对象就是不再被使用的。
巨大缺陷: 很难解决对象之间相互循环引用的问题,因此JVM并未采用这种方法。
2. 可达性分析算法
可达性分析算法:
- 用过一系列的
gc root
来判断对象是否被引用 - 如果
gc root
可以直接或间接引用到某个对象,就表明该对象被引用,反之则说明该对象不可用 - 在下次垃圾回收到达的时候,不可用的对象就会被回收
如图,下次垃圾回收的时候,obj9、obj10、obj11就会被回收。
GC Roots对象取用范围:
System Class
系统类,启动类加载器加载的类(核心类)- 如Object、String、HashMap等
Native Stack
本地方法栈的操作系统方法Busy Monitor
被synchronized
或者lock
加锁的对象Thread
活动线程用到的对象(一个线程对应一个栈,栈帧内的对象)
宣告对象死亡:
宣告对象死亡至少需要经历两次标记过程:
- 第一次标记:
- 在对对象进行可达性分析发现对象没有被 gc root 引用,则会对其进行标记并进行第一次筛选
- 第一次筛选主要是为了判断改对象是否需要执行
finalize()
利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期
这是由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间这段时间是任意长的- 当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行
- 如果对象被判定为有必要执行,则会被放到一个F-Queue队列
- 第二次标记:
- gc将F-Queue中的对象进行第二次标记
- 如果这时候,对象通过调用
finalize()
与 gc root 引用链上的任何一个对象建立关联,那么此对象就会被移出即将被回收的队列
2. 五种常见引用类型
1. 简介及其回收机制
强引用:
- 如:
Object obj = new Object()
变量obj强引用了实例出的对象 - 只要引用链能找到此对象,就不会被回收
软/弱引用:
-
只要没有被强引用直接地引用,都有可能被垃圾回收,如下图:
软引用被回收: 因为obj2在引用cg root引用链中没有被强引用直接引用,所以在下一次垃圾回收且内存不够的时候,有可能被回收
弱引用被回收: obj3被强引用直接引用了,所以就不会被垃圾回收。可如果obj3也没有强引用直接引用,就会在下一次垃圾回收的时候被回收 -
当软/弱引用的对象被回收之后,如果在创建软/弱引用的时候,被分配了一个引用队列,那么软/弱引用就会进入引用队列(这两者也会占用内存,也可以释放掉)
虚/终结引用:
- 虚/终结引用 必须配合引用队列来使用
- 当 虚/终结引用对象 被创建的时候,就会创建一个引用队列
- 虚引用回收:
在创建ByteBuffer对象的时候,就会使用Cleaner
来监测,而一旦没有强引用引用ByteBuffer的时候,ByteBuffer自己就会被垃圾回收掉,如下:
但是这时候,直接内存还没有被回收,所以这时候,虚引用对象就会进入引用队列,由Reference Handler
线程调用虚引用相关方法(Unsafe.freeMemory
)释放直接内存
- 终结引用回收:
当没有强引用去引用对象(重写了finallize()
的对象)的时候,JVM就会给此对象创建一个终结器引用
当对象被垃圾回收器回收的时候,终结器引用也会进入引用队列,但这时候对象还没有被回收
然后Finalizer
线程(此线程优先级很低,被执行的机会很少)通过终结器引用找到被引用对象并调用它的finalize
方法,第二次 GC 时才能回收被引用对象
2. 代码演示
在JDK1.2之前,只有引用和没引用两种状态。
SoftReference
实现软引用的类,WeakReference
实现弱引用的类、PhantomReference
实现虚引用的类。
软引用演示:
-
虚拟机参数:
-Xmx20m -XX:+PrintGCDetails -verbose:gc
最大堆内存20M,打印GC细节,控制台输出gc情况 -
代码(软引用所引用对象的回收):
- 不使用软引用情况:
会造成内存溢出public class TestSoft private static final int _4MB = 4 * 1024 * 1024; public static void main(String[] args) throws IOException List<byte[]> list = new ArrayList<>(); for (int i = 0; i < 5; i++) list.add(new byte[_4MB]); System.in.read();
- 使用软引用:
不会造成内存溢出:public class TestSoft private static final int _4MB = 4 * 1024 * 1024; public static void main(String[] args) throws IOException soft(); public static void soft() // list --> SoftReference --> byte[] List<SoftReference<byte[]>> list = new ArrayList<>(); for (int i = 0; i < 5; i++) SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]); System.out.println(ref.get()); list.add(ref); System.out.println(list.size()); System.out.println("循环结束:" + list.size()); for (SoftReference<byte[]> ref : list) System.out.println(ref.get());
软引用特点: 一次垃圾回收之后,内存仍然不足,就会把软引用所引用的对象回收。
- 不使用软引用情况:
-
代码(软引用对象本身的回收)
- 使用引用队列
ReferenceQueue
// 引用队列 ReferenceQueue<byte[以上是关于Java Virtual Machine的主要内容,如果未能解决你的问题,请参考以下文章
Cordova使用build命令出错: Could not create the Java Virtual Machine.
jstat命令 Java Virtual Machine Statistics Monitoring Tool
Java Language and Virtual Machine Specifications
Java Language and Virtual Machine Specifications
- 使用引用队列