前言
1:JVM(Java Virtual Machine )Java 二进制字节码的运行环境
一、jvm的好处:
- 一次编译,处处执行(不再局限于windows、mac、linux等。只要有jvm)
- 自动的内存管理,垃圾回收机制(在c中需要程序员手动释放内存,导致稍有不慎就内存泄漏)
- 数组下标越界检查
二、jvm、jdk、jre三者关系
三、JVM概括图
- ClassLoader:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
- Method Area:类是放在方法区中。
- Heap:类的实例对象。
- 当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。
方法执行时每行代码由执行引擎中的解释器逐行执行,方法中的热点代码及频繁调用的方法由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口
程序计数器
概念:
- 程序计数器是一个记录着当前线程所执行的字节码的行号指示器。
JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。
从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。
首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。
https://www.cnblogs.com/manayi/p/9290490.html
特点
- 线程私有、隔离性,每个线程工作时都有属于自己的独立计数器。
- 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
- .程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域
虚拟机栈
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
定义
- 每个线程运行需要的内存空间,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
代码演示
可以看到:每次调用方法就是分配虚拟机栈空间。栈准守先进后出。调用入栈从main->mehtod1->method2 方法结束出栈:mehtod2->mehtod1->main
问题:
- 垃圾回收是否涉及栈内存?
不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。
- 栈内存分配越大越好吗?
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
- 方法呢的局部变量是否线程安全?
1 如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
2 如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题
代码演示:
- 栈内存溢出
线程运行诊断:
- 案例一:cpu 占用过多
解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程
- top 命令,查看是哪个进程占用 CPU 过高
- ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
- jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换
本地方法栈
定义
Heap堆
- 通过 new 关键字,创建对象都会使用堆内存
特点 - 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆内存溢出
堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程id - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测 - jvisualvm 工具
方法区
定义
保存在着被加载过的每一个类的信息;这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;static变量信息也保存在方法区中;
可以看做是将类(Class)的元数据,保存在方法区里;
方法区是线程共享的;当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待;
方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。jvm也可以允许用户和程序指定方法区的初始大小,最小和最大限制;
方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展Java程序,这样可能会导致一些类,不再被使用,变为垃圾。这时候需要进行垃圾清理。
方法区内存溢出
1.8 之前会导致永久代内存溢出
使用 -XX:MaxPermSize=8m 指定永久代内存大小
1.8 之后会导致元空间内存溢出
使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
stringTable
特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池(1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回)
示例代码
StringTable调优
- 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
- 考虑是否需要将字符串对象入池
可以通过 intern 方法减少重复入池
直接内存
定义
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高(直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。)
- 是本地内存不受JVM回收管理
直接内存的回收不是通过 JVM 的垃圾回收来释放的而是有一个守护线程去监控ByteBuffer对象。当ByteBuffer被GC时程序就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存,我们也可以通过unsafe.freeMemory 来手动释放。
public static int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
// method();
method1();
}
// 演示 直接内存 是被 unsafe 创建与回收
private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base,_1GB, (byte)0);
System.in.read();
unsafe.freeMemory(base);
System.in.read();
}
// 演示 直接内存被 释放
private static void method() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
System.gc(); // 手动 gc
System.in.read();
}
总结
- 使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
- ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存
注意
一般项目中会配置-XX:+DisableExplicitGC
意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。