一.JVM
jvm定义:Java Virtual Machine,java程序的运行环境(java二进制字节码),java源代码j经过javaC编译成class字节码,class字节码使用java程序加载到JVM运行。
好处:
- 一次编写到处运行的基石。jvm屏蔽了字节码与底层操作系统(windows,linux等)之间的差异。
- 自动内存管理机制,提供垃圾回收功能。
- 数组下标越界检查,C语言没有越界检查,可能导致数组新元素覆盖程序的其他部分,覆盖其他代码的内存。
- 多态,面向对象编程的基石,代码扩展性得到极大提高,jvm在内部使用虚拟方法表的机制来实现了多态。
比较:jvm,jdk,jre比较
二.内存结构
1.Program Counter Register程序计数器(寄存器)
作用:记住下一条jvm指令的执行地址。它是对物理硬件的屏蔽和抽象,物理上是通过寄存器(cpu组件中读取最快的单元)实现的。
二进制字节码(JVM指令) | java源代码 |
---|---|
0: getstatic #20 | //PrintStream out = System.out; |
3: astore_1 | //– |
4: aload_1 | //out.println(1) ; |
5: iconst_1 | // |
6: invokevirtual #26 | //– |
9: aload_1 | //out.println(2) ; |
10: iconst_1 | // |
11: invokevirtual #26 | //– |
14: aload_1 | //out.println(3); |
15: iconst_1 | // |
16: invokevirtual #26 | //– |
19: aload_1 | //out.println(4) ; |
20: iconst_1 | // |
21: invokevirtual #26 | //– |
24: aload_1 | //out.println(5) ; |
25: iconst_1 | // |
26: invokevirtual #26 | //– |
29: invokevirtual #26 | //– |
jvm指令加载到内存结构中会分配对应的内存地址。
程序计数器取出对应地址的jvm指令,jvm指令经过解释器解释成机器码才能交给cpu去执行。
特点:
- 线程私有。
- 不会存在内存溢出。
多线程通过cpu的调度器分配,每个线程有不同的时间片,在一个线程的时间片用完后,程序计数器记住下一次轮到此线程执行的指令的内存地址,继续执行。
2.Java Virtual Machine Stacks (Java 虚拟机栈)
定义:
- 每个线程运行需要的内存空间,称为虚拟机栈。
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧(线程正在执行的方法对应的栈帧),对应着每次方法调用时所占用的内存。
栈帧:每个方法(包括方法内的参数,局部变量,返回地址)运行时需要的内存。
图解定义:
问题辨析
1.垃圾回收是否涉及栈内存?
栈内存:一次次方法调用所产生的栈帧内存。栈帧内存:栈帧在每一次方法调用后都会弹出栈,也就是栈帧内存会被自动回收掉。所以不需要垃圾回收管理栈内存。
垃圾回收值会回收堆内存中的无用对象,栈内存不需要。
2.栈内存分配越大越好吗?
栈内存可以通过运行虚拟机代码时,通过一个参数指定。-Xss size
方法内的局部变量是否线程安全?
线程安全:局部变量对多个线程是共享的,还是属于某个线程私有的。
某个变量是否线程安全判断方法:1、是否是方法内的局部变量 2、是否逃离了方法的作用范围(有可能被别的线程访问到,不再线程安全)。
1.方法内的局部变量没有逃离方法的作用范围,那么它就是线程安全的。
2.局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全。
2.局部变量引用的是基本类型变量,不考虑线程安全。
场景1:有两个线程同时调用这个方法m1,会不会造成x值得混乱呢。不会:x是方法内的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1的栈帧中有对应的x变量是线程1私有的,线程2的栈帧中有对应的x变量是线程2私有的,各自加5000,互不影响。
public class Demo {
public static void m1() {
int x = 0;
for (int i = 0; i< 5000;i++) {
x++;
}
System.out.println(x);
}
}
场景2:有两个线程同时调用这个方法m2,会不会造成x值得混乱呢。会:x是类变量,线程共享的,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1和线程2的栈帧中是对同一个变量x操作。
public class Demo {
static int x = 0;
public static void m2() {
for (int i = 0; i< 5000;i++) {
x++;
}
System.out.println(x);
}
}
场景3:针对下面程序Demo1,有两个线程同时调用这个方法m1,会不会造成sb值得混乱呢。不会:x是方法内的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1的栈帧中有对应的sb变量是线程1私有的,线程2的栈帧中有对应的sb变量是线程2私有的,互不影响。
针对方法m2, sb作为参数传入,其他的线程就有可能访问到它,sb不再是线程私有的,是线程共享的。如下示例:
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(new Runnable() {
@Override
public void run() {
m2(sb);
}
}).start();
}
主线程中创建了一个StringBuilder对象sb,又在新线程里面把sb作为参数传给了m2方法,主线程和新线程都在修改sb,sb是主线程和新线程共享的,线程不安全。
场景4:m3方法中的sb对象是否线程安全呢?答:不安全。
sb虽然是方法内的局部变量,但是作为方法的返回值返回了,这样其他的线程有可能拿到sb的引用对象,并发的修改它。
public class Demo1 {
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
2.2 栈内存溢出
- 栈帧过多导致栈内存溢出
两个类循环引用导致栈内存溢出 - 栈帧过大导致栈内存溢出
2.3线程运行中断
案例1:cpu占用过多
定位
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep pid (用ps命令进一步定位是哪个线程引起的cpu占用过高)输出的线程编号是10进制的
- jstack pid 将此进程下jvm使用的线程和用户定义的线程都列出详情, 输出的线程id是十六进制的,可以将上一步得到的十进制线程id进行转换,对应后,可以根据线程id找到有问题的线程,进一步找到引起问题的代码行数
案列2:程序运行很长时间没有结果
- jstack pid 输出的最后有 Found one Java-level deadlock:…
- 定位到发生死锁的位置
3.本地方法栈
jvm调用本地方法时,给本地方法分配的内存。
本地方法:不是由java代码编写的方法,是有此C或者C++语言编写的代码。java方法不方便与底层操作系统打交道,而是通过调用本地方法与底层操作系统打交道。所有对象的父类Object类里面的方法声明就是用native声明的,是没有方法实现的,java通过native方法接口间接调用C或者C++的接口实现。
从下面说道的堆和方法区是线程共享的
4. Heap 堆
通过new关键字,创建对象都会使用堆内存
特点:
1)它是线程共享的,堆中对象都需要考虑线程安全问题 2)有垃圾回收
4.2 堆内存溢出
OutOfMemoryError Java heap space
堆空间参数:-Xmx
4.3堆内存诊断
1.jps工具
查看当前系统中有哪些java进程
2.jmap工具
查看堆内存使用情况 jmap -head pid
3.jconsole工具
图形界面的,多功能检测工具,可以连续检测
案例:垃圾回收后,内存占用仍然很高
- 方法区
定义
组成
5.3 方法区内存溢出
1.8以前会导致永久代内存溢出
java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize=8m
1.8以后会导致元空间内存溢出
java.lang.OutOfMemoryError:Metaspace
-XX:MaxMetaspace=8m
类加载器:用来加载类的二进制字节码
ClassWriter 用来生成类的二进制字节码
场景:动态产生class,并加载这些类的场景是很多的
Spring:用到字节码技术,都会用到Cglib,生成的代理类,是spring中aop的核心。
myBatis:用到字节码技术,都会用到Cglib。 产生map接口的实现类。
5.4 运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
- 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
5.5 StringTable
特性
- 常量池中的字符串仅是符号,第一次用到时才变成变量
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
1)1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回。
2)1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会把此对象复制一份,放入串池,会把串池中的对象返回。
5.6 StringTable的位置
1.6方法区中永久代中,常量池的一部分
1.7-8 堆中
永久代回收效率很低,只有fullGC才会回收,等到老年代空间不足时,才会触发FullGC。java应用程序中大量的字符串常量,会分配到StringTable中,如果StringTable回收效率不高,会占用大量的内存。
基于这样的缺点,StringTable被转移到堆中。MinorGC就可以触发,大大减轻对内存的压力。
5.7 StringTable 垃圾回收
当内存空间不足时,StringTable中没有被引用到的字符串常量仍然会被垃圾回收。
5.8 StringTable 性能调优
- 调整-XX:StringTableSize=桶个数 hash查找会变快
- 考虑将字符串对象是否入池
6.直接内存 Directory Memory
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本高,但读写性能高
- 不受JVM内存回收管理
java代码不能运行系统缓存区,而是在堆里面分配一块java缓冲区,将数据从系统缓存区读到java缓冲区。
ByteBuffer.allocateDirect(_1Mb);在系统内存分配一块直接内存,java代码可以直接访问。java代码和系统都可直接访问。少了一次缓冲区的复制操作,速度提升一倍。
直接内存溢出
OutOfMemory: Direct buffer memory
6.3 直接内存分配和回收原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferrenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
直接内存禁用显示回收对直接内存的影响。
垃圾回收
1.如何判断对象可以回收
1.1引用计数法,循环引用导致垃圾回收
1.2可达性分析算法
确定一系列根对象,根对象:肯定不能被当成垃圾回收的对象。
垃圾回收前,对堆内存中的所有对象进行一遍扫描,确定每一个对象是不是被根对象做直接或者间接的引用,如果是,此对象就不能被垃圾回收,否则,就被作为垃圾将来被回收。
-
java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
-
扫描堆中的对象,看是否能够沿着GC Root对象为七点的引用链找到该对象,找不到表示可以回收。
-
哪些对象可以作为GC Root?
-
Memory Analyzer: 快速的多功能的java堆分析工具.
-先用jmap获取堆内存快照。jmap -dump:format=b,live,file=1.bin pid
1)System Class(系统类):启动类加载器加载的类,是一些核心类,代码运行过程中都会用到的
2)Native Stack jvm执行操作时候会调用操作系统的方法,操作系统在执行时引用的java对象也是可以作为根对象。
3)Thread 活动线程中使用的对象不能被当成垃圾回收,线程运行时由一次次的方法调用组成,每次方法调用都会产生栈帧,栈帧内使用的对象(局部变量引用的对象,方法参数引用的对象)被作为根对象。
4)Busy Monitor synchronized正在加锁的对象不能被回收,因为后面还要释放锁。
1.3 五种引用
1)强引用–只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
new 一个对象通过复制运算符赋值给了一个变量,此变量就强引用了创建的对象。只要沿着GC Root链能够找到此对象,就不会被垃圾回收。
2)软引用(SoftReference) --仅有软引用引用该对象时,在垃圾回收后,内存仍不足会再次触发垃圾回收,回收软引用对象。可以配合引用队列来释放软引用自身。
当没有强引用引用对象,只有软引用引用对象,当发生垃圾回收,并且内存不够时候,会将软引用引用的对象回收掉。
应用:不重要的资源可以使用软引用
3)弱引用(WeakReference)–
当没有强引用引用对象,只有弱引用引用对象,当发生垃圾回收,不管内存是否充足时候,会将弱引用引用的对象回收掉。可以配合引用队列来释放弱引用自身。
软弱引用配合引用队列进行工作:软弱引用自身也是一个对象,如果在创建软弱引用时给它们分配了队列,当软弱引用引用的对象被回收的时候,软弱引用就会进入对应的队列。
软弱引用被回收时,需要通过引用队列找到它们,再进行处理。
4)虚引用
必须配合引用队列使用,创建ByteBuffer的实现类对象时,就会创建一个Cleaner的虚引用对象,ByteBuffer会分配一个直接内存,然后将直接内存地址给虚引用对象。当ByteBuffer被回收时,锁使用的直接内存不会被回收,让虚引用对象进入引用队列,虚引用所在的引用队列由一个叫ReferenceHandler的线程定时寻找是否有新入队的Cleaner,如果有就会调用Cleaner的虚方法。虚方法根据前面记录的直接内存地址,调用Unsafe.freeMemory将直接内存释放掉。
虚引用引用的对象被垃圾回收时,虚引用对象本身就会被放入引用队列,从而间接的用一个线程来调用虚引用对象的方法,然后调用Unsafe.freeMemory将直接内存释放掉。
5)终结器引用
当对象重写了终结方法finalize,并且没有强引用引用它时,就可以被垃圾回收。当没有强引用引用对象时,虚拟机为我们创建对象对应的终结器引用,当对象没有被强引用引用时,终结器引用对象也加入一个引用队列,由优先级很低的一个线程finalizeHandler,查看队列中队列中是否有终结器引用,如果有就找到对应的对象并调用其finalize方法,调用完后,等真正发生垃圾回收,就可以将此对象占用的内存回收掉。效率很低,线程有限地低 -
垃圾回收算法
标记清除算法(Mark Sweep):速度较快,会造成内存碎片。
标记整理:速度慢,没有内存碎片。
复制:不会有内存碎片,需要占用双倍内存空间。 -
分代垃圾回收
堆内存将内存区域分成新生代(朝生夕死)和老年代(长时间存活的对象)。
新生代又分成伊甸园、缓存区From和幸存区To。
新生代垃圾回收:Minor GC
老年代垃圾回收:Full GC
对象首先分配在伊甸园区域
1)新生代空间不足时,触发Minor GC,伊甸园和缓存区From存活的对象使用复制算法复制到缓存区To中,存活的对象年龄加一,并交换From和To。
2)Minor GC会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
3)当对象寿命超过阈值时,会晋升到老年代,最大寿命是15(4bit)
4)当老年代空间不足,会先触发Monir GC后,空间仍然不足,会触发Full GC (STW时间更长)
3.1 相关VM参数
堆初始大小:-Xms
堆最大大小:-Xmx或-XX:MaxHeapSize
新生代大小:-Xmn或-XX:NewSize=size+ -XX:MaxNewSize=size
幸存区比例(动态):-XX:InitialSurvivorRatio=ratio 和-XX:+UseAdptiveSizePolicy
幸存区比例:-XX:SurvivorRatio=ratio
晋升阈值:-XX:MaxTenuringDistribute=threshold
晋升详情:-XX:+PrintTenuringDistribution
GC详情:-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC -
垃圾回收器
4.1串行 -XX:+UseSerialGC=Seria(工作在新生代采用复制算法垃圾回收)l + SerialOld Serial (工作在老年代采用标记整理算法垃圾回收)
1)单线程 2)堆内存较小,适合个人电脑
4.2吞吐量优先 -XX:+UseParallelGC(新生代的垃圾回收器,复制算法) ~ -XX:+UseParallelOldGC(工作在老年代采用标记整理算法垃圾回收)
1)多线程 2)堆内存较大,多核CPU 3)让单位时间内的STW的时间最短
4.3响应时间优先 -XX:+UseConcMarkSweepGC(标记清楚算法的垃圾回收器)~ -XX:+UseParNewGC ~ SerialOld
1)多线程 2)堆内存较大,多核CPU 3)尽可能让单次STW时间最短
4.4 G1
jdk9开始作为默认的垃圾回收器取代了CMS
适用场景
同时注重吞吐量和低延迟,默认的暂停目标是200ms
超大堆内存,会将堆划分为多个大小相等的region
整体上是标记+整理算法,两个区域之间是复制算法
1)垃圾回收阶段
2)Young Collection
会STW
Eden区总内存占满后,会使用复制算法垃圾回收将存活对象复制到suvivor区,suvivor区总内存占满后,会使用复制算法垃圾回收将年龄满的对象复制到old区,年龄比满的复制到新的survior区。
3)Young Collection + CM(concunrent Marking)
在Young GC时会进行GC Root的初始标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
-xx:InitiatingHeapOccupancyPercent=percent(默认45%)
4)Mixed Collection
会对E、S、O进行全面垃圾回收
最终标记(Remark)会STW,并发标记的时候一些工作线程可能又产生了新的垃圾,所以需要最终标记。
拷贝存活(Evacuation)会STW,不是所有的老年代都会拷贝,会根据最大暂停时间(-:MaxGCPauseMillis=ms)有选择的将回收价值最高的老年代进行垃圾回收(如果堆内存太大,老年代垃圾回收占用时间过多),时间足够会全部进行垃圾回收。
5)Full GC辨析
SerialGC
新生代内存不足发生的垃圾收集-minor gc
老年代内存不足发生的垃圾收集-full gc
ParallelGC
新生代内存不足发生的垃圾收集-minor gc
老年代内存不足发生的垃圾收集-full gc
CMS
新生代内存不足发生的垃圾收集-minor gc
老年代内存不足
G1
新生代内存不足发生的垃圾收集-minor gc
老年代内存不足
CMS/G1老年代内存不足引发的垃圾收集分两种情况:
1.老年代内存占堆内存比例达到45%,触发并发标记和混合收集。垃圾回收速度高于垃圾产生速度,不是full gc,属于并发垃圾收集阶段,暂停时间短。
2.垃圾回收速度跟不上垃圾产生速度,并发收集退化为串行收集。响应时间变长。
也就是并发收集失败后才进入full gc,GC日志打印有full gc字样。
6)Young Collection跨代引用
找到根对象,再可达性分析,再找到存活对象,复制到幸存区。
根对象部分存在于老年代,遍历蒸饭柜老年代效率低,采用卡表技术把老年代的区域再进行划分。分成
一个个大小为512k的card,如果老年代有个对象引用了新生代对象,对应的卡标记为脏卡,这样做GC root遍历的时候就不用遍历整个老年代,而是遍历脏卡,减少搜索范围,提高效率。 -
卡表与RememberedSet
-
在引用变更时通过post-write barrier + dirty card queue
-
concurent refinement threads更新Remembered Set
7)Remark
pre-wrire barrier + satb_mark_queue
当对象的引用发生改变时,jvm给其加入一个写屏障。经对象加入一个队列中,变成灰色,表示还没有处理完,等并发标记结束后,进入最终标记,从队列中取出再进行一次检查,再变成黑色。
8)JDK 8u20字符串去重
优点:节省大量内存
缺点:略微多占用了cpu时间,新生代回收时间略微增加
-xx:+UseStringDeduplication
String s1 = new String(“hello”);
String s2 = new String(“hello”);
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复。
- 如果它们值一样,让它们引用同一个char[]
- 注意,与String.intern()不一样
-String.intern()关注的是字符串对象
-而字符串去重关注的是char[]
-在jvm内部,使用了不同的字符串表
9)JDK 8u40并发标记类卸载
所有对象经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它它所加载的所有类
-xx:+classUnloadingWithConcurentMark默认启用
很多框架程序都使用了自定义的类加载器,很有用,jdk的启动、扩展、应用程序类加载器不会使用的
10)DK 8u60回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象。
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。
11)JDK 9并发标记起始时间的调整
- 并发标必须在堆空间占满前完成,否则退化为FullGC
- JDK 9之前需要使用 -xx:InitiatingHeapOccupancyPercent
- jdk 9可以动态调整
-xx:InitiatingHeapOccupancyPercent用来设置初始值
进行数据采样并动态调整-xx:InitiatingHeapOccupancyPercent=percent
总会添加一个安全的空档空间
减少并发垃圾回收退化为fullGC
垃圾回收调优
类加载与字节码技术
-
类文件结构
1.1 魔数
1.2 版本
1.3 常量池
图解方法执行流程
1)原始java代码
2)编译后的字节码文件
javap -v demo.class
3)常量池载入运行时常量池(是方法区的一部分)
把class文件中的常量池信息载入运行时常量池,将来用到方法引用、成员变量引用,具体数值时候去运行时常量池找到。
4)方法的字节码载入方法区
5)main线程开始运行,分配栈帧内存
局部变量表,操作数栈决定了需要的栈帧内存大小
6)执行引擎读取方法内的数据多态原理小结
当执行invokevirtual指令时,
(1).先通过栈帧中的对象引用找到对象
(2). 分析对象头,找到对象的实际Class
(3). Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
(4).查表得到方法的具体地址
(5).执行方法的字节码 -
字节码指令
-
编译器处理
javac编译器在编译期间对字节码的优化和处理
语法糖,就是指java编译器把.java源码编译为*.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的额外福利。为了便于阅读几下给出的是几乎等价的java源码形式,并不是编译器还会转换出中间的java源码,切记。
3.1默认构造器
3.2自动拆装箱
3.3 泛型集合取值
泛型也是在jdk1.5开始加入的特性,但java在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码后就丢失了,实际的类型都当做了object类型来处理:
所以在取值时,编译器真正生成的字节码文件中,还要额外做一个类型转换的操作;
泛型擦除的是字节码上的泛型信息(方法体),可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息。
局部变量类型表:方法的参数的一些泛型信息
list变量对应LocalVariableTypeTable的字节码信息:
这种LocalVariableTypeTable的泛型信息虽然没有被擦除,但是不能通过反射拿到泛型信息。只有方法的参数和返回值上带有的泛型信息才能被拿到。
3.4 可变参数
3.5 foreach循环
3.6 switch字符串
3.7switch枚举
3.7枚举类
3.8 try with resourses
3.9
3.10方法重写时的桥接方法
我们都知道,方法重写时对返回值分两种情况:
父子类的返回值完全一致
子类返回值可以是父类返回值的子类
3.11匿名内部类
4. 类加载阶段
4.1类加载
java源代码编译成字节码后,通过类加载器将类的字节码载入方法区。内部采用C++的instanceKlass描述java类,它的重要field有:
如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的
4.2链接
- 验证-验证字节码是否符合jvm规范,安全性检查
- 准备:为static变量分配空间,设置默认值
static变量在jdl1.7之前存储于instanceKlass末尾,从jdk7开始,存储于——java_mirror末尾
static变量分配空间和赋值是两个步骤,分配空间是在准备阶段完成,赋值在初始化阶段完成
如果static变量是final的基本类型以及字符串常量的,那么编译阶段就确定了,赋值在准备阶段完成
如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
- 解析 将常量池中的符号引用解析为直接引用
4.3初始化
() v方法
初始化即调用()v,喜怒及会保证这个类的构造方法的线程安全
发生的时机
概括的说,类初始化是懒惰的
main方法所在的类,总会被首先初始化
首次访问这个类的静态变量或静态方法时
子类初始化,如果父类还没有初始化会引发
子类访问父类的静态变量,只会触发父类的初始化
Class.forName
new会导致初始化
不会导致类初始化的情况
访问类的static final静态常量(基本类型和字符串)不会触发初始化
类对象.class不会触发初始化
创建该类的数组不会触发初始化
类加载器的loadClass方法
Class.forName的参数2为false时
练习
懒惰式单例模式初始化
- 类加载器
5.1 启动类加载器
通过特殊的虚拟机参数把自己编写的类交由启动类加载器加载
5.1 扩展类加载器
我们自己编写的一个类是由应用程序类加载器加载的,因为启动类加载器和扩展类加载器找不到这个类。
同名的类分别放在扩展类加载器和应用程序类加载器类路径下,哪个类加载器会加载它?
答:扩展类加载器。双亲委派模型决定的
5.3双亲委派模型
就是指调用类加载器的loadClass方法时,查找类的规则
注意:这里的双亲翻译成上级似乎更合适,因为他们并没有继承关系
5.4线程上下文类加载器
我们在使用jdbc时,都需要加载Driver驱动,不知道你注意没有,不写
Class.forName(“com.mysql.jdbc.Driver”)
先不看别的,看看DriverManager的类加载器:
System.out.println(DriverManager.class.getClassLoader());
…
5.4自定义类加载器
什么时候需要自定义类加载器
1)想加载非classpath任意路径路径下的类文件
2)都是通过接口来实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常用于tomcat容器(同名同包的类也可以在tomcat运行)
实现自定义类加载器的步骤
1.继承ClassLoader父类
2.要遵从双亲委派机制,重写findClass方法
注意:不是重写loadClass方法,否则不会走双亲委派机制
3.读取类文件的字节码
4.调用父类的defineClass方法来加载类
5.使用者调用该类加载器的loadClss方法
6.运行期优化
6.1即时编译
分层编译
JVM将字节码执行状态分成了5个层次:
0层:解释执行(Interpreter)
字节码被加载到虚拟机后,靠解释器一个字节一个字节的解释执行,解释成一个个真正的机器码。当字节码被反复调用次数达到一定程度后,启用编译器(C1和C2编译器)对字节码进行编译执行
1层:使用C1即时编译器编译执行(不带profiling)
2层:使用C1即时编译器编译执行(带基本profiling)
3层:使用C1即时编译器编译执行(带完全的profiling)
4层:使用C2即时编译器编译执行
profiling是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等
即时编译器和和解释器的区别
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
解释器是将字节码解释为针对所有平台都通用的机器码
JIT会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取采用解释执行的方式运行;另一方面,对于占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。执行效率上简单比较一下,Interpreter<C1<C2,总的目标是发现热点代码(hotspot名称的由来),优化之。
C2编译器做的优化,刚才的一种优化手段称之为【逃逸技术】,发现新建的对象是否逃逸,可以使用-xx:-DoEscapeAnalysis关闭逃逸分析。
方法内联
字段优化
6.2反射优化
内存模型
1.java内存模型
java内存结构和java内存模型是不一样的,java内存模型是Java Memory的意思。
简单的说,JMM定义了一套在多线程读写共享数据时(成员变量,数组),对数据的可见性、有序性和原子性的规则和保障。
1.1原子性
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做500次,结果是0吗?
1.2问题分析
以上的结果可能是正数,负数或者0。为什么呢,因为java中对静态变量的自增,自减并不是原子操作。
如果是单线程以上8行代码是顺序执行(不会交错),没有问题:
但多线程下这8行代码可能交错运行,原因:操作系统的的多线程模型是抢先式多任务,线程轮流拿到cpu的使用权。
2.可见性
2.1退不出的循环
先看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:
2.因为t线程要频繁从主存中读取run的值,JIT编译器会将run的值缓存到自己的工作内存中的告诉缓存中,减少对主存中run的访问,提高效率。。
3.1s后,main线程修改了run的值,并同步至主存,而t是从自己的工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
2.2解决方法
可见性,保证的是在多个线程之间,一个线程对volitile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况;
注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是属于重量级操作,性能相对更低。
如果在前面的死循环中加入System.out.println()会发现即时不加volitile关键字修饰符,线程t也能正确看到对run变量的修改了,为什么?
synchronized防止当前线程从高速缓存获取run的值,破坏了JIT的优化
3.有序性
3.1诡异的结果
I_Result是一个对象,有一个属性r1用来保存结果,问,可能有几种?
这种现象就做指令重排,是JIT编译器在运行时的一些优化,这个现象需要通过大量的测试才能发现:java并发压测工具jcstress
3.2解决方法
volitile修饰的变量,可以禁用指令重排
3.2有序性的理解
这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性,例如著名的double-checked locking模式实现单例
public final class Singleton {
private Singleton() {}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
//实例没创建,才会进入内部的synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) {
//也许有其他线程已经创建实例,所以再创建一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
以上的实现特点是:
懒惰实例化
首次使用getInstance()才使用synchronized加锁,后续使用无需加锁。
但在多线程条件下,上面的代码是有问题的,INSTANCE= new Singleton()对应的字节码为:
其中47两步的顺序不是固定的,也许jvm会优化为:先将引用地址赋值给INSTANCE变量后,再执行构造方法,如果两个线程t1,t2按照如下时间序列执行:
3.4 happens-before
happens-before规定了哪些写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结:
下面所有的变量都是指成员变量或静态成员变量。
1)线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
2)线程对volatile变量的写,对接下来其他线程对该变量的读可见
3)线程start前对变量的写,对该线程开始后对该变量的读可见
4)线程结束前对变量的写,对其他线程得知它结束后的读可见(比如其他线程调用t1.isAlive()或t1.join()等待它结束)
5)线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或t2.isInterrupted)
6)对变量默认值(0,false,null)的写,对其他线程对该变量的读可见
7)具有传递性,如果x hb-y,且y hb->z 那么有 x hb->z