学习笔记-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和系统都能够访问 

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号