JVM笔记

JVM内存区域

  1. 虚拟机栈:线程私有,每次方法的调用都会创建一个栈帧,压入栈.栈帧中包括:局部变量表,操作数栈,出口信息,动态链接

  2. 程序计数器:线程私有,记录线程当前执行到的指令的地址.

  3. 本地方法栈:为本地方法的执行提供内存区域

  4. 堆:线程共享的数据区,几乎所有的对象实例都存放在堆中,少部分由于逃逸分析的优化,可能在栈中分配,JDK7时堆分为新生代,老年代,永久代.到JDK8方法区的实现变成了元空间

  5. 方法区:线程共享,存放了类的字节码信息以及运行时常量池.在JDK6的时候,静态变量和字符串常量池也在方法区内.到了JDK7,静态变量和字符串常量池存放在了堆中

垃圾收集机制

判断对象是否为垃圾
  1. 引用计数法,缺点,无法解决循环引用的现象

  2. 可达性分析法,GC Roots的引用链中的对象都存活

  3. 固定可作为gcroots的对象包括以下几种

    1. 各线程的虚拟机栈中各栈帧内局部变量表中引用的对象

    2. 类静态属性,和常量引用的对象

    3. 本地方法栈中引用的对象

    4. java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如

      NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器

    5. 所有被同步锁(synchronized关键字)持有的对象

    6. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

四种引用类型
  1. 强引用:代码中普遍存在的引用赋值,强引用关系在的对象永远不会被垃圾回收

  2. 软引用:描述一些有用,但不是必须的对象,在即将发生OutOfMemoryError之前,会对这些对象进行回收

  3. 弱引用:描述非必须的对象,但是发生GC时,收集器随时可以对这些对象进行回收

  4. 虚引用:对象是否被虚引用完全不会对其生存时间构成影响,唯一目的是对象被当做垃圾回收之后收到一个系统通知.比如,当创建一个ByteBuffer对象,会被一个Cleaner的虚引用对象所引用,当bytebuffer被回收时,Cleaner对象就会进入与之关联的引用队列,被一个ReferenceHandler的线程执行其中的clean方法,释放直接内存.

垃圾回收算法
  1. 标记清除算法(Mark-Sweep)

    分标记和清除阶段,缺点是会造成内存空间的碎片化,在新生代一般不会使用该算法,因为新生代的对象大部分都是朝生夕灭的,极容易造成内部碎片

  2. 标记复制算法

    解决的内存空间的碎片化问题.将新生代分成eden,from和to三个区域,将eden和from作为对象分配的空间,在垃圾收集时,将存活的对象放入to中,并将to变为from.若to中无法容纳幸存下来的对象,则由老年代做分配担保.

    缺点是如果存活的对象过多会导致复制的成本过高,一般用于新生代,不适用于老年代

  3. 标记整理算法(Mark-Compact)

    针对老年代的存亡特征(存活对象更多),在垃圾收集后,将存活的对象向内存的一端移动,以达到清除小碎片的目的,这样做优缺点并存

    优点是减少了内存碎片,缺点是需要stw,开销大

典型垃圾收集器
  1. serial/serialold

    1. 新生代使用标记复制算法,老年代采用标记整理算法

    2. serial收集器是运行在客户端模式下的虚拟机很好的选择

    3. serial虽然是串行工作的,但是在堆内存不是很大的时候停顿不会很久,这对用户程序来说是可接受的

  2. ParNew/CMS

    1. 标记复制+标记清除 ,标记整理兜底

    2. ParNew支持多线程并行收集,默认开启核心数与处理器核数相同

    3. CMS实现了用户线程与垃圾回收线程并发的工作,减少了用户线程的停顿时间,即低延迟

    4. CMS适用于服务端,因为关注停顿时间,所以保证了服务的响应时间.

    5. 若老年代内部碎片过多可能会出现并发失败的问题,这时候CMS会退化为SerialOld,使用标记整理算法清除老年代的内存碎片

    6. CMS的缺点:由于占用了一部分处理器导致吞吐量下降,无法处理浮动垃圾,基于标记清除,会造成内部碎片最终有可能导致fullGC

  3. G1

    1. G1收集器运作过程大致分以下四个阶段

      1. 初始标记:标记一下GCRoots能直接关联到的对象,需要stw

      2. 并发标记:进行可达性分析,找出要回收的对象,不需要stw

      3. 最终标记:处理并发结束后的有变动的对象,需要stw

      4. 筛选回收:根据用户期望停顿时间筛选排好序的region作为回收集,将回收集中存活的对象放入空的region中,由多条线程并行完成.涉及存活对象的移动,必须stw

    2. 因此,G1除了并发标记阶段,其他阶段也要暂停用户线程,它并不纯粹追求低延迟,而是在延迟可控的情况下获得尽可能高的吞吐量

    3. G1的优势:将堆空间划分为一个个的region,不追求一次将整个堆清理干净,而是在用户可接受的延迟范围内,清理收益更高的region区域

    4. G1的缺点:G1的内存占用非常高,它需要为每个region都维护一张卡表

    5. 当一个对象大于region的一半时被称为巨型对象,G1不会对巨型对象进行拷贝,回收时被优先考虑

GC调优

个人理解:首先是尽量的增大一些堆的空间,并且使得新生代和老年代占比合理,这样就可以尽量减少gc的次数.然后根据应用程序的类型选择合适的垃圾收集器,如果是服务端注重响应时间的,应考虑g1或者cms这种低延迟的垃圾收集器,如果是科学计算类的,应选用parallelgc,提高吞吐量

Java类结构和字节码技术

类的字节码文件结构

概括:字节码文件的基本信息(魔数,版本,修饰符,父类,接口等),常量池信息,字段信息,方法信息

ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的访问标记
    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口数量
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//字段数量
    field_info     fields[fields_count];//一个类可以有多个字段
    u2             methods_count;//方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}
​

常见字节码指令

iconst_1:把常量1压入操作数栈,-1~5使用iconst

bipush 10 :把10压入操作数栈,-127~128使用bipush

putstatic #2:把值赋给#2引用的静态变量

iload_i :把槽位i的int局部变量值压入操作数栈

aload_i:把i位置的对象引用压入操作数栈,0表示压入this

iinc x 1:把x自增1

astore_1:把操作数栈顶的引用赋值给局部变量表槽位为1的变量

多态原理

当执行到invokevirtual指令时,通过对象引用找到对象,然后在对象头找到该对象实际的Class,在字节码文件中有虚方法表即vtable,它在类加载的链接阶段就已经生成

根据虚方法表得到方法的具体地址,然后执行方法

异常处理

字节码中的方法内部有个异常表,异常表中有from和to标记要监测的指令行,若范围内发生异常,会和表中的type即异常的类型比较,若是type类型或子类,则跳转到target所在的行号

Finally原理

finally块中的指令会被复制多份,在try后,catch后都有finally块中的指令.

如果发生了catch捕捉不到的异常也是有可能的.因此异常表中会记录catch以外的异常,如果发生,则跳转到target所在指令执行finally块

synchronized原理

通过monitorenter 和monitorexit两条字节码指令实现,加锁时,先将对象引用压入操作数栈,然后执行monitorenter给对象头加锁,执行完代码,同理给对象头解锁

如果过程中出现异常,由于exception table中会有对同步代码块中代码的监测,因此即使发生异常也能够解锁

语法糖

默认构造

当没有对类显式的进行构造方法的声明,编译器会在编译时为类加上默认的空参构造

自动拆装箱

装箱:integer x = integer.valueof(1); 拆箱: int y = x.intValue();

jdk5引入自动拆装箱,上述代码在jdk5以后可以写成integer x = 1; int y = x;

Integer有缓存的机制,在-128~127内的integer对象是共用的,即多次调用valueof得到的是同一个对象

而在拆箱时,可能会引发空指针异常,需要特别注意

泛型擦除

jdk5引入,java代码在编译时会执行一个泛型擦除的操作,即泛型信息在编译成字节码后就丢失了,实际的类型会被当做object处理.

而在取值时,取出的也是object类型,只是编译器会额外做一个类型转换操作checkcast

需要注意的是,泛型的擦除实际上擦除的是方法体,即方法内code中的泛型信息.但是方法中有个叫做局部变量类型表LocalVariavleTypeTable中会记录局部变量的类型的泛型信息:

局部变量表:start length slot name signature name即变量名,signature即变量类型

局部变量表中会有 名为list 类型为java/util/List

而局部变量类型表中会记录list 的类型为java/util/List<java/lang/Integer>

可变参数

jdk5引入,在定义方法时,在最后一个形参中加...,就表示该形参可以接受多个参数值

可变参数只能作为函数最后一个参数,且在编译后的字节码文件中,可变参数实际上就是数组类型,因此数组与可变参数不能作为方法的重载

增强for

对于数组的增强for,在编译后实际就是普通for,因此,数组的增强for底层可以看成普通for

对于集合的增强for,在编译后实际上是迭代器的遍历,即集合增强for底层是迭代器

枚举类

jdk7引入,枚举是一种特殊的类,代码中定义了枚举,编译器在编译时会定义一个枚举类,该类中会为代码中的每个枚举都定义一个静态常量的枚举类实例,由于构造方法是私有的,因此枚举类无法被外部创建对象

twr

jdk7引入,在try的小括号中定义需要关闭资源的对象,这样就不需自己在finally块中对资源关闭,编译器在编译阶段会为其补上关闭资源的相关代码

匿名内部类

当代码中new了一个匿名内部类,编译器在编译时会创建一个额外的类 :外部类名$1.真正的代码则是new了一个该类的对象.

类加载过程

加载

将类的字节码载入方法区,内部采用cpp的instanceKlass描述java类,有以下重要的field:

_java_mirror , _super, _fields , _methods , _constants , _class_loader, _vtable , _itable

如果这个类的父类还没有加载,先加载父类

加载和链接可能是交替运行的

链接
  1. 验证:检查字节码文件的格式内容是否符合JVM规范,以及一些安全性检查

  2. 准备:为static变量分配空间,设置默认值

    1. static变量jdk7以后存储在class对象中,即堆中

    2. static变量分配空间和赋值(cinit)是两个步骤,分配空间在准备阶段,而赋值在初始化阶段

    3. 如果是static final基本类型,或者是字符串常量,则编译阶段值就已经确定,因此赋值在准备阶段完成

    4. 如果是static final类型,但属于引用类型,那么赋值也在初始化阶段

  3. 解析:将常量池的符号引用解析为直接引用.未解析则常量池中只有这个类的符号,并没有实际的地址,解析后常量池中就会记录该类的内存地址

初始化

初始化即调用<cinit>()V,虚拟机会保证类构造方法的线程安全

类的初始化是懒惰式的,即只有在需要初始化的时候才会执行cinit

class.forname默认会对类进行加载链接初始化

类加载器

种类
  1. Boostrap ClassLoader 启动类加载器

  2. Extension ClassLoader 扩展类加载器

  3. Application ClassLoader 应用类加载器

  4. 自定义类加载器

双亲委派模式
概念

ClassLoader类中有loadClass方法,在将类加载进内存时,类加载器会判断,若类已经加载过则无需加载,

若没有加载过,会交由上级加载,若不在上级加载的范围内,则调用自己的findclass方法进行查找

优点

双亲委派模式保证了核心类库的安全,防止了用户有意或者无意的修改核心类,也防止了类的重复加载

如何打破双亲委派

因为双亲委派是通过loadclass方法实现的,因此可以自定义类加载器重写loadclass方法

JDBC中就打破了双亲委派机制,DriverManger是java的启动类,由bootstrap加载,但是在DriverManger会初始化驱动类,而驱动类并不在核心类库中,因此需要委托线程上下文加载器(默认是系统类加载器)加载

自定义类加载器

什么时候需要用到自定义类加载器

  1. 想要加载非classpath路径中的类文件

  2. 通过接口实现,希望解耦时,常用在框架设计中

  3. 希望将某些类隔离,不同应用的同名类都可以加载,而不会产生冲突,常见于tomcat容器

步骤

  1. 自定义加载器类继承父类ClassLoader

  2. 重写findclass方法,读取自定义路径下的字节码文件放入bytes数组,调用defineclass方法将字节码文件加载到内存中

运行期优化

JVM将字节码指令的执行状态分成了五个层次:0~4层,第0层是解释执行,1~4层由即时编译器编译执行

分层的原因

为了在启动速度和执行效率间达到平衡.程序刚开始执行不会进行编译,因为这会占用程序运行时间并且消耗内存,而在执行的过程中才会发现某些热点代码,这个时候进行即时编译,提高运行效率.

即时编译器JIT与解释器的区别
  1. 解释器将字节码解释为机器码,下次遇到相同的字节码仍会重复解释执行

  2. JIT将字节码编译为机器指令,并存入Code Cache,下次遇到相同代码,直接执行已经编译好的指令

常见的运行期优化
  1. 逃逸分析:分析对象是否逃出了某些作用域的范围,比如逃出了方法,若没有逃逸,则可以做出一定优化,例如堆分配改为栈分配等,从而节省时间开销

  2. 方法内联:若方法不会太大,可以将方法代码粘贴到调用者的位置,从而节省时间开销

  3. 字段优化:成员变量在堆中,当需要频繁对成员变量操作时,可以复制一份引用在局部变量

  4. 反射优化:如果持续反射调用方法达到阈值(默认15),JVM就会在运行期间生成一个类,当通过反射调用invoke方法时,实际就调用了运行时生成类的方法,它将反射调用优化成了正常调用方法

JMM

原子性问题

对共享变量,例如静态变量的自增自减并不是原子操作,实际jvm的指令为:

  1. getstatic i

  2. iconst_1

  3. iadd

  4. putstatic i

通过对希望是原子操作的代码使用synchronized关键字可以保障代码的原子性,从字节码指令角度来说,是由monitorenter和monitorexit指令保证.

可见性问题

当线程对主存中某个变量不断进行操作时,可能会将主存中的变量复制到工作缓存中,从而导致主存变量的修改对线程不可见

volatile :修饰成员变量和静态变量,可以避免线程从自己工作缓存中查找变量的值,必须到主存中获取它的值

通过volatile关键字,保证了在多个线程之间,一个线程对volatile修饰的变量的修改对其它线程可见

synchronized既可以保证可见性,也可以保证原子性,但是它是重量级的操作

有序性问题

由于JIT的优化,某些时候会出现指令重排的情况,这样可能会造成意料之外的结果

通过使用volatile关键字修饰变量,使得其不受指令重排的影响

指令重排:在同一个线程内,JVM会在不影响正确性的前提下,调整语句的执行顺序.但在多线程环境下是可能会出现问题的

问题实际场景:设计单例类的时候,有一种叫双重检查锁的方式(double-checked locking):

这种方式在单线程下没问题,但是在多线程环境下是有问题的.

new创建一个对象的过程:new开辟空间并将引用压栈,dup复制引用,invokespecial初始化,putstatic将引用赋给变量.

但是由于指令重排序,putstatic可能在初始化之前,这样就会造成对象还未初始化,但是变量的引用已经不为null.其它线程就有可能拿到一个尚未初始化的对象.

public final class Singleton {
    //单例类的实现
    private Singleton(){}
    //多线程环境下可能会出现问题(极低概率),因此应用volatile修饰
    private static Singleton INSTANCE;
    public static Singleton getSingleton() {
        //如果有实例就直接返回,这样就防止了每次get时都加锁
        if (INSTANCE == null){
            synchronized (Singleton.class){
                if (INSTANCE == null){
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

CAS(compare and swap)与原子类

概念
//共享变量
public static int a = 10;
while(true){
    //获取共享变量原来的值
    int x = a;
    //修改后的结果
    int result = x + 10;
    //如果a的值与获取时一样,即并未改变,则修改a的值为result,并退出循环,如果a发生变化,继续循环
    if(cas(x,result)){
        break;
    }
}

CAS体现了一种乐观锁的思想,即无锁并发

优点:线程不会阻塞,没有线程切换的开销,效率较高

缺点:如果竞争过于激烈,会频繁的发生重试,效率反而降低

CAS底层实现

cas的底层依赖于Unsafe类来直接调用操作系统提供的CAS指令.

原子操作类

juc中提供了原子操作类,可以提供线程安全的操作,例如AutomicInteger,AutomicBoolean等,它们底层就是采用CAS+volatile实现的

synchronized优化

synchronized是重量级锁,体现了悲观锁的思想,jdk6对它做了一些优化

每个对象的都有对象头,对象头中记录了class指针和Mark Word,Mark Word中平时存储了对象的hash码,分代年龄等.当加锁时,这些信息就根据情况被替换为标记位,线程锁记录指针重量级锁指针,线程ID等内容

每个线程的栈帧中都会包含一个锁记录结构,内部可以存储锁定对象的Mark Word

轻量级锁

当线程尝试去获取锁的时候是通过CAS获取,如果获取失败,发现锁已经被其他线程占据,会将锁升级.如果获取成功,这个时候可以避免锁的升级

自旋锁

如果已经是重量级锁,当线程尝试去获取锁,如果获取锁失败,不会立刻陷入阻塞,而是自旋重试,重试几次后如果还失败,才会陷入阻塞

偏向锁

当线程重复给某个对象加锁时,会发生锁重入,因此在第一次加锁时,将线程id加入MarkWord

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值