Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.MD

idea 插件 :jclasslib byte

然后可以在 view -> show bytecode with jclasslib 进行字节码查看

JIT

逃逸分析包括:
   全局变量赋值逃逸
   方法返回值逃逸
   实例引用发生逃逸
   线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量
 java 1.7 之后 默认开启逃逸分析优化
    -XX:+DoEscapeAnalysis : 表示开启逃逸分析
    -XX:-DoEscapeAnalysis : 表示关闭逃逸分析
 TestLockEliminate 锁销除    同样基于逃逸分析,当加锁的变量不会发生逃逸,是线程私有的完全没有必要加锁。
 EscapeAnalysisTest 栈上内存分配 

常量池

  1. final类型的8种基本类型的值会进入常量池。
  2. 非final类型(包括static的)的8种基本类型的值,只有double、float、long的值会进入常量 池。
  3. 常量池中包含的字符串类型字面量(双引号引起来的字符串值)。

静态内部类实现单例

静态内部类方式

  要理解静态内部类方式,首先要理解类加载机制。

  虚拟机把Class文件加载到内存,然后进行校验,解析和初始化,最终形成java类型,这就是虚拟机的类加载机制。加载,验证,准备,解析、初始化这5个阶段的顺序是确定的,类的加载过程,必须按照这种顺序开始。这些阶段通常是相互交叉和混合进行的。解析阶段在某些情况下,可以在初始化阶段之后再开始---为了支持java语言的运行时绑定(动态绑定,多态的原理)。

  在Java虚拟机规范中,没有强制约束什么时候要开始加载,但是,却严格规定了几种情况必须进行初始化(加载,验证,准备则需要在初始化之前开始):

  1. 遇到 new、getstatic、putstatic、或者invokestatic 这4条字节码指令,如果没有类没有进行过初始化,则触发初始化

  2. 使用java.lang.reflect包的方法,进行反射调用的时候,如果没有初始化,则先触发初始化

  3. 初始化一个类时候,如果发现父类没有初始化,则先触发父类的初始化

  我们仅说与本期主题相关的初始化阶段:

  类初始化阶段是类加载过程的最后阶段。在这个阶段,java虚拟机才真正开始执行类定义中的java程序代码。在编译的时候,编译器会自动收集类中的所有静态变量(类变量)和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是根据语句在java代码中的顺序决定的。收集完成之后,会编译成java类的 static{} 方法,java虚拟机则会保证一个类的static{} 方法在多线程或者单线程环境中正确的执行,并且只执行一次。在执行的过程中,便完成了类变量的初始化。如果我们的java类中,没有显式声明static{}块,如果类中有静态变量,编译器会默认给我们生成一个static{}方法。

  对于静态变量来说,虚拟机会保证在子类的static{}方法执行之前,父类的static{}方法已经执行完毕(即如果父类没有加载则先加载父类)。由于父类的static{}方法先执行,也就意味着父类的静态变量要优先于子类的静态变量赋值操作。

  对于实例变量来说,在实例化对象时,JVM会在堆中为对象分配足够的空间,然后将空间清零(即所有类型赋默认值,引用类型为null)。JVM会收集类中的复制语句放于构造函数中执行,如果没有显式声明构造函数则会默认生成一个构造函数。子类默认生成的构造函数第一行默认为super();即如果父类有无参的构造方法,子类会先调用父类的构造方法再调用本身的构造方法。因为它继承父类成员的使用,必须先初始化这些成员。如果父类没有无参的构造方法则子类继承会报错,需要子类通过super显式调用父类的有参构造方法。如果类中显式定义一个或多个构造方法,则不再生成默认构造方法。

  对于静态变量,上面的描述还不太准确。
   类初始化阶段,JVM保证同一个类的static{}方法只被执行一次,这是静态内部类单例模式的核心。
   JVM靠类的全限定类名以及加载它的类加载器来唯一确定一个类。
   (这个很重要,经常会有这方面的坑!比如反序列化时,被序列化的对象使用java默认的类加载器加载,
   而使用了反序列化的一方使用的框架(如springBoot就有自己的类加载器)强制使用自己的类加载器加载某个类,
   则会因为JVM判定不是一个类而报ClassNotFoundException!)

  所以修正一下的说法便是,静态内部类单例模式的核心原理为对于一个类,JVM在仅用一个类加载器加载它时,静态变量的赋值在全局只会执行一次!

  使用静态内部类的优点是:因为外部类对内部类的引用属于被动引用,不属于前面提到的三种必须进行初始化的情况,所以加载类本身并不需要同时加载内部类。在需要实例化该类是才触发内部类的加载以及本类的实例化,做到了延时加载(懒加载),节约内存。同时因为JVM会保证一个类的<cinit>()方法(初始化方法)执行时的线程安全,从而保证了实例在全局的唯一性。
public class Student {
    private Student() {
    }

    /**
    * 此处使用一个内部类来维护单例 JVM在类加载的时候,是互斥的,所以可以由此保证线程安全问题
    */
    private static class SingletonFactory {
        private static Student student = new Student();
    }

    /* 获取实例 */
    public static Student getSingletonInstance() {
        return SingletonFactory.student;
    }
}

字符串常量池如何存储数据

在jdk7+, StringTable 的长度可以通过一个参数指定:
stringtable是类似于hashtable的数据结构,hashtable数据结构如下:
-XX:StringTableSize=9999

volatile

如果对声明了volatile的 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据
写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操
作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一
致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当
处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状
态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存
里。(CAS)

volatile的两条实现原则:

1) Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在执行指令期间,声 言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以 独占任何共享内存[2]。但是,在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕 竟锁总线开销的比较大。在8.1.4节有详细说明锁定操作对处理器缓存的影响,对于Intel486和 Pentium处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和目前的处理器中,如果 访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区 域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁 定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。 2) 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。 IA-32处理器和Intel 64处 理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致 性。在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统 内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的 缓存的数据在总线上保持一致。例如,在Pentium和P6 family处理器中,如果通过嗅探一个处理 器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理 器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

Synchonized

从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对 象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter 和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有 详细说明。但是,方法的同步同样可以使用这两个指令来实现。 monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结 束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有 一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽 等于4字节,即32bit, 锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏 向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高 获得锁和释放锁的效率