学习笔记-JVM
JVM的位置
JVM是运行在操作系统上的虚拟机,存在于JRE当中
JVM的类型
-
HotSpot
-
Sun公司
-
用的基本都是这个
-
-
JRockit
- BEA
-
J9VM
- IBM
JVM的体系结构
本地方法接口JNI
-
JNI的作用
-
拓展java的使用,融合不同的编程语言为java所用
- 最初是C/C++
-
因为最初java诞生的时候,市面上全是C/C++,java要想立足,必须有能调用C/C++的方法
-
于是在内存中设置了本地方法栈,专门用来登记native方法
-
然后由JNI去调用本地方法库
-
-
-
凡是带了native关键字的方法
-
说明java的作用范围达不到了
-
会进入本地方法栈
-
执行引擎会调用本地方法接口JNI
-
去调用底层c语言的库
-
-
常见的本地方法
-
线程
-
打印机
-
管理系统
-
-
现在除了通过JNI,也有其他方法去调用其他语言的方法,比如说Socket
类加载器
ClassLoader
用于加载类
分类
-
虚拟机自带的加载器
-
java调用不到这个类
-
是用C/C++写的
-
-
启动类(根)加载器
- 加载java核心类库
-
扩展类加载器
- 加载ext目录中的jar包
-
应用程序加载器
- 加载当前classpath下的所有类
除此之外,用户也能自定义类加载器,用来加载指定路径的class类
双亲委派机制
-
类加载器收到类加载的请求
-
将这个请求向上委托给父类加载器去完成
-
一直向上委托
-
直到启动类加载器
-
-
当前加载器检查是否能够加载当前这个类
-
能加载就结束,使用当前的加载器
-
否则通知子加载器进行加载
-
-
重复步骤3
-
若没有任何类加载器可以加载
- Class Not Found
双亲委派机制的作用:
-
沙箱隔离机制,安全,防止Java的核心API类被篡改
- 恶意代码无法通过同名类的方法获得高级权限
-
避免重复加载
类加载的过程
-
验证
-
准备
-
解析
-
初始化
程序计数器
-
可以看作
-
当前线程所执行的字节码的行号指示器
-
指向下一个将要执行的指令代码的地址
-
如果是Java方法,记录的是虚拟机字节码指令的地址
-
如果是native方法,记录的是Undefined
-
-
由执行引擎来读取下一条指令
-
-
更确切地说
-
一个线程的执行
-
是通过字节码解释器改变当前线程的计数器的值
-
来获取下一条需要执行的字节码指令
-
-
在物理上是通过寄存器来使用的
-
不存在OOM
栈
-
线程运行需要的内存空间
-
虚拟机栈为java方法服务
-
本地方法栈为native方法服务
-
两者在作用上是非常相似的
-
下面主要描述虚拟机栈
栈中存储的是什么
-
栈帧(stack frame)是栈的元素
- 每个方法在执行时都会创建一个栈帧
-
栈帧主要包含四个部分
-
局部变量表(local variable)
-
操作数栈(operand stack)
-
动态连接(dynamic linking)
-
方法出口
-
局部变量表
-
用于存储数据
-
存储的类型有两种
-
基本数据类型的局部变量
- 包括方法参数
-
对象的引用
- 但是不存储对象的内容
-
-
所需的内存空间在编译期间完成分配
- 方法运行期间不会改变局部变量表的大小
-
变量槽(Variable Slot)
-
局部变量表的容量的最小单位
-
一个slot最大32位
- 对于64位的数据类型(long和double)会分配两个连续的slot
-
java通过索引定位的方法使用局部变量表
-
从0开始
-
一个Slot占1位
-
非static方法第0个槽存储方法所属对象实例的引用
-
-
slot复用
-
为了节省栈帧空间,slot是可以复用的
-
如果某个变量失效了
- 即超出了某个变量的作用域
-
那么这个变量的slot就会交给其他变量使用
-
副作用(这一段存疑):
-
会影响系统的垃圾收集行为
-
当某个变量失效后,因为它的slot可能还会交给其他变量复用,所以它占用的slot就不会被回收
-
-
-
-
线程安全
-
当局部变量表中的引用逃离了线程的范围
-
也就是当一个引用可以被另一个线程拿到的时候
-
就变成线程不安全的了
-
操作数栈
-
一个栈
-
元素可以是任意的java数据类型
-
主要作用
-
用于算数运算
-
用于参数传递
-
-
栈帧中用于计算的临时数据存储区
-
举例
-
public class OperandStack{ public static int add(int a, int b){ int c = a + b; return c; } public static void main(String[] args){ add(100, 98); } }
-
动态连接
指向运行时常量池中该栈帧所属方法的引用
返回地址
-
存放调用该方法的pc寄存器的值
-
正常退出时会使用
-
异常退出时会通过异常表来确认
可能出现的异常
-
StackOverflowError
-
栈溢出错误
-
如果一个线程在计算时所需的栈大小>配置允许最大的栈大小
-
那么jvm将抛出该错误
-
-
OutOfMemoryError
-
内存不足
-
栈进行动态扩展时如果无法申请到足够的内存
-
会抛出该错误
-
设置栈参数
-
-Xss
-
设置栈大小
-
通常几百K
-
jstack命令
-
jstack是JVM自带的JAVA栈追踪工具
-
它用于打印出给定的java进程ID、core file、远程调试Java栈信息
-
常用命令:
-
jstack [option] pid
- 打印某个进程的堆栈信息
-
选项
-
-F
强制输出 -
-m
显示本地方法的堆栈 -
-l
显示锁信息
-
-
-
使用案例
-
查看进程死锁情况
-
查看高cpu占用情况
- 还需要用到top命令
-
堆
被所有线程共享
主要存储
-
new关键字创建的对象实例
- 数组
-
静态变量
-
string池(1.8之后)
GC就是在堆上收集对象所占用的内存空间
堆的空间结构
-
新创建的对象会存储在生成区
-
年轻代内存满之后,会触发Minor CG,清理年轻代内存
-
长期存活的对象和大对象会存储在老年代
-
当老年代内存满之后,会触发Full CG,清理全部内存
-
如果清理后仍然无法存储进新的对象
-
会抛出OutOfMemoryError
-
堆内存诊断
- jps工具
- 查看当前系统中有哪些java进程
- jmap工具
- 查看堆内存占用情况
jmap -heap pid
jmap -dump:format=b,live,file=1.bin pid
- 将堆内存占用情况转储
format=b
:以二进制的形式live
:抓取之前调用一次垃圾回收file=1.bin
:将文件导出为1.bin
- jconsole工具
- 图形界面的,多功能的监测工具
- jvisualvm
- 可视化虚拟机
- 案例:调用垃圾回收后,占用的内存依然非常大
- 使用jvisualvm
- 查看对象个数
- 使用堆转储dump
方法区
被所有线程共享
主要存储
-
类信息
-
版本
-
字段
-
方法
-
接口
-
-
运行时的常量池
-
字面量
-
final修饰的常量
-
基本数据类型的值
-
字符串(1.8之前)
-
-
符号引用
-
类和接口的全类型
-
方法名和描述符
-
字段名和描述符
-
-
当类被加载时,
.class
中的常量池会被放进运行时常量池中
-
永久区
JDK1.7及之前,方法区的具体实现是PermSpace永久区
MetaSpace
JDK1.8后,使用MetaSpace元空间替代PermSpace
元空间不在JVM中,而是使用本地内存
有两个参数:
-
MetaSpaceSize
-
初始化元空间大小
-
控制发生GC的阈值
-
-
MaxMetaSpaceSize
-
限制元空间大小上限
-
防止异常占用过多的物理内存
-
使用常量池的优点
-
避免了频繁的创建和销毁对象而影响系统性能
-
实现了对象的共享
Integer常量池
public void TestIntegerCache()
{
public static void main(String[] args)
{
Integer i1 = new Integer(66);
Integer i2 = new Integer(66);
Integer i3 = 66;
Integer i4 = 66;
Integer i5 = 150;
Integer i6 = 150;
System.out.println(i1 == i2);//false
System.out.println(i3 == i4);//true
System.out.println(i5 == i6);//false
}
}
-
为什么
i1 == i2
为false
- 因为是new了两个新对象,两个新对象的地址不一样
-
为什么
i3 == i4
为true
-
当
Integer i3 = 66
时,其实进行了一步装箱操作 -
通过
Integer.valueOf()
将66
装箱成Integer
-
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
-
而
IntegerCache
是Integer
的静态内部类- 它通过
static{}
静态代码块,将-128~127的值全部缓存在了一个Integer数组中
- 它通过
-
-
为什么
i5 == i6
为false
-
因为150超出了缓存的范围
-
重新new了一个对象
public static void main(String[] args){
Integer i1 = new Integer(4);
Integer i2 = new Integer(6);
Integer i3 = new Integer(10);
System.out.print(i3 == i1+i2);//true
}
-
-
为什么
i3 == i1 + i2
为true
-
因为对象在进行
+
运算时是会进行拆箱的 -
拆箱成int再进行数值比较
-
String常量池
在1.6之后在堆中,在1.6及之前在永久代中
目的是为了减少字符串对内存的占用,提高效率
String是由final修饰的类,不可被继承
-
String str = new String("abcd");
- 每次都会创建一个新对象
-
String str = "abcd"
-
先在栈上创建一个引用
-
然后去String常量池找是否有"abcd"
-
若有,直接让引用指向它
-
没有,向常量池添加一个"abcd",再指向它
-
-
字符串+连接问题
String a = "a1";
String b = "a" + 1;
System.out.println((a == b)); //result = true
String a = "atrue";
String b = "a" + "true";
System.out.println((a == b)); //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b)); //result = true
JVM在编译时就会优化成+号连接后的值
字符串引用+连接问题
public static void main(String[] args){
String str1 = "a";
String str2 = "ab";
String str3 = str1 + "b";
System.out.print(str2 == str3);//false
}
因为是变量,在编译时无法确定结果
JVM会将+连接优化成StringBuilder的append方法
反编译后的内容
public class TestDemo
{
public TestDemo()
{
}
public static void main(String args[])
{
String s = "a";
String s1 = "ab";
String s2 = (new StringBuilder()).append(s).append("b").toString();
System.out.print(s1 = s2);
}
}
但要注意的是,用final修饰过的字符串引用,会被视为常量,而非变量
intern()
s.intern()
-
将字符串对象尝试放入串池中
-
如果有,则不放入
-
如果没有,则放入
-
但是在1.6版本时
-
会复制一份,然后把复制品放入串池中
-
-
-
返回串池中的对象
调优
一些参数
-
-XX:+PrinStringTableStatistic
- 打印串池的统计信息
-
-XX:+PrintGCDetails -verbose:gc
- 打印GC信息
-
-XX:StringTableSize=<数值>
- 调整StringTable底层hash表的长度
调优思路
-
调整桶个数
-
使用
-XX:StringTableSize=<数值>
-
因为StringTable底层是一个hashtable
-
所以我们可以通过调整长度来减少发生碰撞的次数
-
从而减少链表的长度
-
最终提高速度
-
-
-
考虑将字符串对象是否入池
-
使用
intern()
方法 -
将字符串入池
-
.class文件中的内容
反编译指令javap -v <.class文件>
-
类的基本信息
-
更改时间
-
MD5
-
类全名
-
版本信息
-
父类信息
-
接口信息
-
-
常量池
-
一张表
-
虚拟机根据这张常量表找到要执行的
- 类型、方法名、参数类型、字面量等信息
-
-
类的方法定义
-
构造方法
-
成员方法
-
直接内存
-
Direct Memory
-
常见于NIO操作时,用于数据缓冲区
-
分配回收成本较高,但读写性能高
-
不受JVM内存回收管理
-
会出现内存溢出OOM
-
-
Unsafe
-
在底层是通过Unsafe对象分配的空间
-
于是也需要手动调用Unsafe对象的freeMemory方法释放空间
-
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象
-
一旦ByteBuffer对象被垃圾回收
-
就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
-
-
禁用显示垃圾回收对直接内存的影响
-
-XX:+DisableExplicitGC
禁止显示的垃圾回收 -
无效掉
System.gc()
-
所以使用直接内存的时候,应该手动使用Unsafe对象
-
-
原始的IO操作
-
由于java不能直接访问系统内存
-
所以数据在被读入到系统缓冲区后,
-
要再读进java缓冲区
-
然后才能访问
-
直接内存
-
存在于系统内存中
-
但是java和系统都能够访问