学习笔记-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 == i2false

    • 因为是new了两个新对象,两个新对象的地址不一样
  • 为什么i3 == i4true

    • 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);
      }
      
    • IntegerCacheInteger的静态内部类

      • 它通过static{}静态代码块,将-128~127的值全部缓存在了一个Integer数组中
  • 为什么i5 == i6false

    • 因为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 + i2true

    • 因为对象在进行+运算时是会进行拆箱的

    • 拆箱成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和系统都能够访问

热门相关:仙城纪   豪门重生盛世闲女   重生当学神,又又又考第一了!   特工重生:快穿全能女神   豪门重生盛世闲女