JVM学习笔记

整数的表达

在计算机系统中,数值一律用补码来表示和存储。
原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理。
此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

  • 原码——第一位为符号位(0为整数,1为负数)
  • 反码——符号位不懂,原码取反
  • 负数补码——符号为不动,反码加1
  • 正数补码——和原码相同
    1
    2
    3
    4
    5
    6
    //打印整数的二进制表示
    int a = -6;
    for(int i = 0; i < 32; i++){
    int t = (a & 0x80000000>>>i)>>>(31-i);
    System.out.print(t);
    }




JVM需要对Java Library提供以下支持

  • 反射java.lang.reflect
  • ClassLoader
  • 初始化class和interface
  • 安全相关java.security
  • 多线程
  • 弱引用



JVM启动流程

Mark-Down




Java内存模型

Mark-Down

Java栈是线程私有的,Java堆是全局共享的。

PC寄存器

每个线程拥有一个PC寄存器
在线程创建时创建
指向下一条指令的地址
执行本地方法时,PC的值为undefined

方法区

保存装载的类信息

  • 类型的常量池
  • 字段,方法信息
  • 方法字节码
    通常和永久区(Perm)关联在一起

    Java堆

    和程序开发密切相关
    应用系统对象都保存在Java堆中
    所有线程共享Java堆
    对分代GC来说,堆也是分代的
    GC的主要工作区间
    Mark-Down

Java栈

线程私有
栈由一系列帧组成(因此Java栈也叫做帧栈)
帧保存一个方法的局部变量、操作数栈、常量池指针
每一个方法调用创建一个帧,并压栈

1
2
3
4
5
6
7
8
9
public class StackDemo{
public static int runStatic(int i, long l, float f, Object o, byte b){
return 0;
}

public int runInstance(char c, short s, boolean b){
return 0;
}
}

Mark-Down




Java栈上分配

小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
直接分配在栈上,可以自动回收,减轻GC压力
大对象或者逃逸对象无法栈上分配



栈、堆、方法区交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AppMain{ //运行时,jvm把appmain的信息都放入方法区
public static void main(String[] args){ //main方法本身放入方法区
Sample test1 = new Sample("测试1"); //test1引用,所以放到栈区,Sample是自定义对象应该放到堆里面
Sample test2 = new Sample("测试2");

test1.printName();
test2.printName();
}
}

public class Sample{ //运行时,jvm把appmain的信息都放入方法区
private String name; //new Sample实例后,name引用放入栈区里name对象放入堆里
public Sample(String name){
this.name = name;
}

//print方法本身放入方法区里
public void printName(){
System.out.println(name);
}

}

Mark-Down




工作内存和主存

当数据从主内存复制到工作存储时,必须出现两个动作

  • 由主内存执行的读(read)操作
  • 由工作内存执行的相应的load操作


    当数据从工作内存拷贝到主内存时,也出现两个操作
  • 由工作内存执行的存储(store)操作
  • 由主内存执行的相应的写(write)操作


    每一个操作都是原子的,即执行期间不会被中断
    对于普通变量,一个线程中更新的值,不能马上反应在其他变量中
    如果需要在其他线程中立即可见,需要使用volatile关键字
    Mark-Down

有序性

  • 在本线程内,操作都是有序的
  • 在线程外观察,操作都是无序的。(指令重排或主内存同步延时)

    指令重排的基本原则

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile变量的写,先发生于读
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行结束先于finalize()方法



    几种常见的GC算法

  • 引用计数法
  • 标记-清除法
  • 标记-压缩法
  • 复制算法

    引用计数法

    老牌垃圾回收算法
    通过引用计数来回收垃圾
    使用者:COM、ActionScript3、Python
    缺陷:引用和去引用伴随加法和减法,影响性能;很难处理循环引用问题

    标记-清除

    标记阶段:通过根节点,标记所有从根节点开始的可达对象,因此未被标记的对象就是未被引用的垃圾对象。
    清除阶段:清除所有未被标记的对象。
    Mark-Down


标记-压缩

它是标记-清除的优化,先标记,但之后并不简单的清理未被标记的对象,而是将所有的存活对象压缩到内存的一端。
之后,清理边界外所有空间。这样做,减少了内存碎片。
[标记-压缩] 算法适用于存活对象较多的场合,如老年代。
Mark-Down


复制算法

与标记-清除算法相比,复制算法是一种相对高效的回收方法。
不适用于存活对象较多的场合,如老年代。
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,
之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
Mark-Down

复制算法的最大问题是:空间浪费 整合标记清理思想
Mark-Down


分代思想

  • 依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。
  • 根据不同代的特点,选取合适的收集算法:少量对象存活,适合复制算法;大量对象存活,适合标记清理或标记压缩算法。

    可触及性

  • 可触及的:从根节点可以触及到这个对象
  • 可复活的:一旦所有引用被释放,就是可复活状态,因为finalize()中可能复活该对象
  • 不可触及的:在finalize()后,可能会进入不可触及状态,不可触及的对象不可能复活,可以回收
    避免使用finalize(),操作不慎可能导致错误。可以使用try-catch-finally来替代它。

STOP_THE_WORLD

Java中一种全局暂停的现象。
全局停顿,所有Java代码停止,native可以执行,但不能和JVM交互。
多半由于GC引起。

  • Dump线程
  • 死锁检查
  • 堆Dump
    危害:长时间服务停止,没有响应;遇到HA系统,可能引起主备切换,严重危害生产环境。

类装载器

class装载验证流程

加载——>链接[验证、准备、解析]——>初始化

加载

装载类的第一个阶段
取得类的二进制流
转为方法区数据结构
在Java堆中生成对应的java.lang.Class对象

验证

目的:保证Class流的格式是正确的

  • 文件格式的验证
  • 元数据验证
  • 字节码验证(很复杂)
  • 符号引用验证

    准备

    分配内存,并为类设置初始值(方法区中)
    1
    2
    public static int v = 1; //在准备阶段找那个,v会被设置为0,在初始化的<clinit>中才会被设置为1
    public static final int v = 1; //对于static final类型,在准备阶段就会被赋上正确的值

解析

符号引用替换为直接引用。

初始化

执行类构造器,static变量赋值语句,static{}语句。
子类的调用前保证父类的被调用。

是线程安全的。

什么是ClassLoader

ClassLoader是一个抽象
ClassLoader的实例将读入Java字节码将类装载到JVM中
ClassLoader可以定制,满足不同的字节码流获取方式
ClassLoader负责类装载过程中的加载阶段
ClassLoad的重要方法

  • public Class<?> loadClass(String name)throws ClassNotFoundException 载入并返回一个Class
  • protected final Class<?> defineClass(byte[] b, int off, int len) 定义一个类,不公开调用
  • protected Class<?> findClass(String name) throws ClassNotFoundException loadClass回调方法,自定义ClassLoader的推荐做法
  • protected final Class<?> findLoadedClass(String name) 寻找已经加载的类

JDK中ClassLoader默认设计模式

BootStrap ClassLoader(启动ClassLoader)
Extension ClassLoader(扩展ClassLoader)
App ClassLoader(应用ClassLoader/系统ClassLoader)
Custom ClassLoader(自定义ClassLoader)
每个ClassLoader都有一个Parent作为父亲。
Mark-Down

双亲模式的问题:顶层ClassLoader,无法加载底层ClassLoader的类。
解决办法:Thread.setContextClassLoader(),上下文加载器,基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例。
双亲模式是默认的模式,但不是必须这么做,Tomcat的WebappClassLoader就会先加载自己的Class,找不到再委托parent;
OSGi的ClassLoader形成网状结构,根据需要自由加载Class。