记住 它是线程私有 是用来记录 某线程下一次执行字节码指令的地址的
记住:栈是先进后出的 如下图二 里面存放的是一个个栈帧,活动栈帧 只有一个 栈帧里面存放的是方法参数 局部变量 返回地址啥的
如下 图一图二三是 分别运行到 method1 ,method3 以及结束执行 method2,3 返回到method1时的栈内信息 可以看到图二 图中下方中间 运行到method3时的参数及局部变量信息
1:不涉及,栈内存是方法执行完就回收了
2.不是 反而大了不好,栈内存设置大了意味着每调用一个方法就要使用这样大小的一块内存空间
如果原本支持200个线程并发,当你设置大了后就会浪费内存空间的同时并且线程并发数会减少
3. 是线程安全的 只有被线程共享的变量才有线程安全问题
m1线程安全的因为里面局部变量始终在该方法内,
m2,m3都不是线程安全的,作为参数传进来或者作为返回值返回的变量都可能被其它线程共享
调用方法太多时,正常不会 一般是因为你写递归,结束条件没写好,或者 你写的代码比如
部门类里有员工,员工类里又有部门,使用时就 套娃似的 太多层了
就会报这错
比如某死循环代码或者如下这典型死锁代码 要怎样定位呢?
jps
然后jstack 进程id
信息如下图 如果死锁的话最小面就会有死锁线程的信息
如果 进程内线程太多了 就可以使用如下指令 先大致看到是哪个线程的问题
然后再到 jstack 信息中 根据 线程编号去找(记得转换为16进制),这样就方便多了
有些与操作系统打交道的方法 由 C或者C++实现
比如 Object里的clone方法,本地方法栈的存在就是给他们提供运行的内存空间
使用如下代码示例看看 当一段代码运行时 堆内存的分布及使用吧
package com.robert.concurrent.test;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestConcurrentHashmap")
public class TestConcurrentHashmap {
public static void main(String[] args) {
log.debug("初始状态...");
TimeSleepUtil.sleep(30);
byte[] data = new byte[1024*1024*20];
log.debug("给对象分配内存空间后状态...");
TimeSleepUtil.sleep(20);
System.gc();
log.debug("回收后状态...");
TimeSleepUtil.sleep(30);
}
}
再每次log的时候 在 idea 下面的windows终端 内输入 jps查询当前运行进程 再使用
jmap -heap 线程id 去查看堆信息
下面三图分别为 初始化状态 给对象分配内存空间后状态 回收后状态 下的堆的配置信息及内存的信息我们主要看内存使用信息 可以看到图一比图二 新生代多出了20M占用内存 最后手动GC后
更是比初始时占用内存都要少很多,现在看不懂没关系,接着看
以上的代码运行时 终端 jconsole连接后 能看到对应的内存的实时变化
jvisualvm 这个工具 也是jdk自带的, 这个又比jconsole好点,比如如下 我可以看到 占用内存从大到下的前二十个对象的信息
堆转储命令 jmap -Dump命令使用转储堆内存快照到指定文件
从官网的介绍可以看到 方法区 跟 堆一样 也是 线程的共享区域 它存储着 每个类的结构信息
比如 运行时常量池,属性,方法还有方法和构造器的代码
方法区只是个规范 jdk1.8之前它的实现是叫永久代 之后是原空间
大致结构如下
前面不是说方法区是用来存放类信息的嘛,下面这块代码 就是加载类的信息 1.8后 方法区是存在系统的内存中的 我的电脑内存是16G 需要加载类次数太多
所以调整 方法区 内存大小 -XX:MaxMetaspaceSize=8m 修改为8m
如果是Jdk1.8之前报的错就是下面这样 永久代内存溢出
一般是使用第三方框架或者写底层代码时 才会出现的错误 比如 spring mybatis会动态加载些类
正常 自己不会写这种加载很多次类的代码
使用如下 jdk提供的反编译的工具 javap 反编译 class文件
C:\study\workspace\target\classes\com\robert\concurrent\test>javap -v TestConcurrentHashmap.class
可以看到 反编译后的字节码信息 类文件信息如下 这是被工具 翻译成了人能看懂的
第一部分:文件相关信息 比如 修改日期 文件大小,文件路径,权限,版本啥的
Classfile /C:/study/workspace/target/classes/com/robert/concurrent/test/TestConcurrentHashmap.class
Last modified 2021-10-15; size 714 bytes
MD5 checksum 189bce0860301ad4051a45b5b748fee3
Compiled from "TestConcurrentHashmap.java"
public class com.robert.concurrent.test.TestConcurrentHashmap
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
第二部分: 常量池 就相当于 类中常量的一张表通过编号就能找到 现在是一些 编号 在运行中时
会被替换为相应内存地址
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."":()V
#2 = String #25 // c.TestConcurrentHashmap
#3 = Methodref #26.#27 // org/slf4j/LoggerFactory.getLogger:(Ljava/lang/String;)Lorg/slf4j/Logger;
#4 = Fieldref #5.#28 // com/robert/concurrent/test/TestConcurrentHashmap.log:Lorg/slf4j/Logger;
#5 = Class #29 // com/robert/concurrent/test/TestConcurrentHashmap
#6 = Class #30 // java/lang/Object
#7 = Utf8 log
#8 = Utf8 Lorg/slf4j/Logger;
#9 = Utf8
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/robert/concurrent/test/TestConcurrentHashmap;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 MethodParameters
#21 = Utf8
#22 = Utf8 SourceFile
#23 = Utf8 TestConcurrentHashmap.java
#24 = NameAndType #9:#10 // "":()V
#25 = Utf8 c.TestConcurrentHashmap
#26 = Class #31 // org/slf4j/LoggerFactory
#27 = NameAndType #32:#33 // getLogger:(Ljava/lang/String;)Lorg/slf4j/Logger;
#28 = NameAndType #7:#8 // log:Lorg/slf4j/Logger;
#29 = Utf8 com/robert/concurrent/test/TestConcurrentHashmap
#30 = Utf8 java/lang/Object
#31 = Utf8 org/slf4j/LoggerFactory
#32 = Utf8 getLogger
#33 = Utf8 (Ljava/lang/String;)Lorg/slf4j/Logger;
第三部分:成员属性,方法还有方法和构造器的代码
下面这些代码里的 #1 #3 这种都会在运行时去常量池中获取对应常量然后执行
{
public com.robert.concurrent.test.TestConcurrentHashmap();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/robert/concurrent/test/TestConcurrentHashmap;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #2 // String c.TestConcurrentHashmap
2: invokestatic #3 // Method org/slf4j/LoggerFactory.getLogger:(Ljava/lang/String;)Lorg/slf4j/Logger;
5: putstatic #4 // Field log:Lorg/slf4j/Logger;
8: return
LineNumberTable:
line 5: 0
}
先看看,后面反编译用字节码能看到
先看看,后面说答案
从下面两图 与 idea中memory(右下角)可以查看某类字符串对象个数,第一遍打印 时字符串对象增加,想想也正常 用到才放到常量池很合理不然浪费内存不是?
因为串池StringTable没有所以新增然后放进去,第二遍打印时 因为串池中有了所以直接取串池中的 然后如图三有局部变量引用就放到图四局部变量表
没错,从字节码可以看出 s1+s2 经过编译器优化后 就等于下图一 右侧注释代码 因为他们是可变的
是的,编译器优化了 因为都是常量不可变,编译期就可以直接把代码改了然后优化 不过这种代码正常没人写
调用intern方法 就是 去到串池中找如果存在 那么 就返回 串池中字符串 不存在就放进去后再返回
串池中的字符串 所以不管存不存在调用intern的返回值都是串池中的字符串常量,但是 不存在时
会把调用者的也改成 串池 中的字符串 所以 s.intern 正常s是放在堆中的new出来的对象,如果
调用intern方法时 串池StringTable中没有该串 那么 s = “ab” 而不再是原本new出来的放在堆中的String对象了
串池1.8是放在堆中的,minor GC时就可以回收无用的字符串 之前版本好像不是 这里不管了
答案全在注释中,上面看懂了的话应该可以分析出来了,我这再稍微分析下
StringTable中的字符串也是会垃圾回收的,下三图 图一蓝色部分是程序初始状态时 串池的字符串的数量 图二图三 分别是 10个及1000个字符串往串池中放后的 串池中字符串的数量,当 存放的数量 大于串池中新增的字符串数量时 说明 你存放的字符串对象中 有值是重复的 或者 超出了新生代的垃圾回收的阈值 产生了垃圾回收
前面我们直到 StringTable 串池 的作用是存放 着一堆的字符串 他们是不重复的,所以我们如果一个项目中有大量字符串,然后内存不是很多的话,可以在使用时将这些字符串使用intern方法入池,也就达到了节省内存的效果,因为重复的字符串被剔除了 然后串池内的存储方式比正常的String对象内存小很多 没有啥对象头markword那些信息 同时 因为 StringTable存储的数据结构是类似HashTable的形式
有个bukect数组 然后 他下面就挂着存放字符串的链表 链表中的字符串都是有hash冲突(就是他们的hash值对数组长度取模)的 值就是 某bukect下标 所以 当项目中发生频繁入池的操作时 为了提升效率 可以添加如下VM参数 -XX: StringTableSize=20000 这种来减少hash碰撞的次数从而,提升入池效率
简单说一下,如图二 原本我们读取 某磁盘文件中的内容时 需要先读到系统内存 然后从系统内存拷贝一份到java内存 然后代码内才能获取到 这玩意相当于 系统内存与java内存共享的一块内存,我们可以直接把磁盘中的内容读到直接内存 然后java代码中就可以读取了 所以大大提升了效率
如下图 我们可以使用 ByteBuffer.allocateDirect(_1GB) 这种形式来分配一块直接内存,然后用就完事了
package com.robert.concurrent.test;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.ByteBuffer;
@Slf4j(topic = "c.TestStringTable")
public class TestStringTable {
private static final int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
// 使用java内存存储
byte[] content = new byte[_1GB];
System.in.read(content);
// 使用直接内存存储
ByteBuffer direct = ByteBuffer.allocateDirect(_1GB);
}
}
这里只简单说下,因为涉及到一个unsafe对象,与虚引用的概念
unsafe这个对象是针对系统层面的一些操作暴露出来的一个算是接口类的玩意 前面多线程中 CAS就是调用它的一些方法,这里也是用它来分配直接内存
直接使用unsafe对象分配直接内存的示例代码如下
直接内存释放的原理就是创建了一个虚引用this对象 绑定的对象是 一个 实现了Runnable接口的Deallocator对象 当 虚引用对象释放时 调用 Deallocator对象的run方法然后执行 直接内存的释放
释放直接内存的代码如下
命令如下 在vm参数中新增这个后 代码中写的System.gc() 就无效了 为了防止新手乱写代码,因为System.gc()是fullGC 而系统的GC是 先youngGC再 full
但是这样的话我们就无法通过回收直接内存的引用对象来回收直接内存了,那要怎样回收直接内存呢?
如图 反射获取到 unsafe对象然后调用其 freeMemory方法来释放直接内存
如果使用此方法 当 出现循环引用时将无法回收
如下 使用一些节点作为根节点 不是这些根节点的字节点的对象被标注为垃圾 可以进行垃圾回收,JVM使用的就是这种
有如下有四大类 系统类对象, 本地方法栈中对象, 线程中的(比如 图四中倒数那两个就是一个作为main方法参数的string数组,另一个是main方法内用到的arraylist对象),锁对象
先看下概览,下面会详细说明 前面三种
我们一般正常创建出来的对象就是强引用关联的,这里不多说
上面有说到,当第一次垃圾回收后 内存还是不足 此时就会回收软引用关联的对象,这样做的好处是什么呢? 当我们使用大内存对象的时候 如果程序运行的足够久 那么内存将被一直占用着 ,很有可能造成OOM 此时就可以使用软引用 去关联 该类大内存对象 当内存进行一轮垃圾回收(youngGC+fullGC)后还不足时 就直接回收掉它们
我先把vm参数中的堆内存参数置为了80M 这个看你机器具体情况,我是在保证能放下四个4M字节数组对象的情况下调整的当调到80M的时候新生代刚好够放一个4m对象而不发生垃圾回收
-Xmx80m -XX:+PrintGCDetails -verbose:gc
package com.robert.concurrent.test;
import lombok.extern.slf4j.Slf4j;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
@Slf4j(topic = "c.TestSoftReference")
public class TestSoftReference {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) {
ArrayList> arrayList = new ArrayList();
for (int i = 0; i < 5; i++) {
System.out.println();
SoftReference softReference = new SoftReference(new Byte[_4MB]);
log.debug(softReference.get().toString());
arrayList.add(softReference);
log.debug("size:{}",arrayList.size());
for (SoftReference softReference1 : arrayList) {
System.out.println(softReference1.get());
}
}
log.debug("软引用对象添加完后 list的size为:{}",arrayList.size());
System.out.println();
for (SoftReference softReference : arrayList) {
System.out.println(softReference.get());
}
}
}
打印日志如下,我分析下
可以看到 当我存放前三个4M对象时没啥事,当存放第四个时发生了一次young GC 但是 没有回收这四个对象中的一个 说明回收了其它优先级更高的垃圾 比如 虚引用关联的
当存放到第五个4M字节数组对象时 这时候 young GC 与 full GC都进行了两轮 第一轮发现清除无用对象后 还是放不下第五个对象 于是第二次 fullGC 直接把 前面那四个软引用关联的对象给回收了
23:18:38.977 c.TestSoftReference [main] - [Ljava.lang.Byte;@7ff2a664
23:18:38.979 c.TestSoftReference [main] - size:1
[Ljava.lang.Byte;@7ff2a664
23:18:38.984 c.TestSoftReference [main] - [Ljava.lang.Byte;@2b4a2ec7
23:18:38.984 c.TestSoftReference [main] - size:2
[Ljava.lang.Byte;@7ff2a664
[Ljava.lang.Byte;@2b4a2ec7
23:18:38.988 c.TestSoftReference [main] - [Ljava.lang.Byte;@564718df
23:18:38.988 c.TestSoftReference [main] - size:3
[Ljava.lang.Byte;@7ff2a664
[Ljava.lang.Byte;@2b4a2ec7
[Ljava.lang.Byte;@564718df
[GC (Allocation Failure) [PSYoungGen: 17450K->2754K(24064K)] 66603K->51978K(78848K), 0.0017831 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
23:18:38.991 c.TestSoftReference [main] - [Ljava.lang.Byte;@51b7e5df
23:18:38.991 c.TestSoftReference [main] - size:4
[Ljava.lang.Byte;@7ff2a664
[Ljava.lang.Byte;@2b4a2ec7
[Ljava.lang.Byte;@564718df
[Ljava.lang.Byte;@51b7e5df
[GC (Allocation Failure) --[PSYoungGen: 19978K->19978K(24064K)] 69202K->69210K(78848K), 0.0046974 secs] [Times: user=0.14 sys=0.02, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 19978K->17899K(24064K)] [ParOldGen: 49232K->49220K(54784K)] 69210K->67120K(78848K), [Metaspace: 7160K->7160K(1056768K)], 0.0141763 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) --[PSYoungGen: 17899K->17899K(24064K)] 67120K->67120K(78848K), 0.0047847 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 17899K->0K(24064K)] [ParOldGen: 49220K->1471K(33280K)] 67120K->1471K(57344K), [Metaspace: 7160K->7160K(1056768K)], 0.0072875 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
23:18:39.023 c.TestSoftReference [main] - [Ljava.lang.Byte;@18a70f16
23:18:39.023 c.TestSoftReference [main] - size:5
null
null
null
null
[Ljava.lang.Byte;@18a70f16
23:18:39.023 c.TestSoftReference [main] - 软引用对象添加完后 list的size为:5
null
null
null
null
[Ljava.lang.Byte;@18a70f16
Heap
PSYoungGen total 24064K, used 17219K [0x00000000fe580000, 0x0000000100000000, 0x0000000100000000)
eden space 20992K, 82% used [0x00000000fe580000,0x00000000ff650d00,0x00000000ffa00000)
from space 3072K, 0% used [0x00000000ffa00000,0x00000000ffa00000,0x00000000ffd00000)
to space 3072K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x0000000100000000)
ParOldGen total 33280K, used 1471K [0x00000000fb000000, 0x00000000fd080000, 0x00000000fe580000)
object space 33280K, 4% used [0x00000000fb000000,0x00000000fb16fe48,0x00000000fd080000)
Metaspace used 7160K, capacity 7364K, committed 7552K, reserved 1056768K
class space used 839K, capacity 884K, committed 896K, reserved 1048576K
Disconnected from the target VM, address: '127.0.0.1:13467', transport: 'socket'
Process finished with exit code 0
如上例 当 引用对象关联的对象为null时 那这个引用对象如果没必要存在我们的list中时 我们可以
使用引用队列 在适当时机去将他们从list中移除从而将 关联对象为null的引用对象给回收
package com.robert.concurrent.test;
import lombok.extern.slf4j.Slf4j;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
@Slf4j(topic = "c.TestSoftReference")
public class TestSoftReference {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) {
ArrayList> arrayList = new ArrayList();
ReferenceQueue referenceQueue = new ReferenceQueue();
for (int i = 0; i < 5; i++) {
System.out.println();
SoftReference softReference = new SoftReference(new Byte[_4MB],referenceQueue);
log.debug(softReference.get().toString());
arrayList.add(softReference);
log.debug("size:{}",arrayList.size());
for (SoftReference softReference1 : arrayList) {
System.out.println(softReference1.get());
}
}
Reference reference = referenceQueue.poll();
while(reference != null && reference.get() == null){
arrayList.remove(reference);
reference = referenceQueue.poll();
}
log.debug("软引用对象添加完后 list的size为:{}",arrayList.size());
System.out.println();
for (SoftReference softReference : arrayList) {
System.out.println(softReference.get());
}
}
}
软引用关联对象是在当内存进行一轮垃圾回收(youngGC+fullGC)后还不足时 第二轮就考虑回收掉它们
弱引用关联对象则是 在内存不足时第一轮垃圾回收 就可能回收掉
根据可达性分析算法 将不被根节点引用的 对象做标记 然后标记了的内存区间的起始终止地址记录到一个空闲内存地址列表里
优势:速度快
缺点:产生许多内存碎片没有充分利用内存 内存碎片可能有 5M 3M 2M 但是此时来个6M的对象就放不下
如下,标记好垃圾后 把有用对象往前挪
优点:没有内存碎片
缺点:因为整理要挪动对象存放的内存中的位置 比较耗时
如图 一块内存拆成两块 先标记 然后 把有用对象放到to区然后清空from区 然后 from与to交换位置
优点:没有内存碎片
缺点:占用双倍内存
分为如下三类,根据业务场景选择
主要是看下面图中的适用场景
如图 还是跟以前差不多,只是现在伊甸园区被分成了多块
并发标记是在老年代中对象内存达到阈值时发生
前两种收集是在老年代内存不足时就进行full GC 后两者会先进行并发标记,看垃圾清理速度是否大于其它工作线程的垃圾产生速度
老年代中被新生代对象所引用的话 会被做标记 ——》脏对象
由于标记过程中引用可能会被修改,所以在当前标记对象引用被修改时加入一个标记队列并加上写屏障,然后后面重标记时再次取出来判断是否有引用
这个跟前面说到的intern去重不同,那个是串池中字符串不重复,这个是
JDK8中的优化 如果 两个字符串对象的值相同,那么在新生代垃圾回收时 他们底层的值也就是字符数组最终会变成同一个
除了jdk底层外,有时候我们会手动加载一些类,那么这些类对象没被引用时给他卸载 这个jdk8中默认开启了
如下解释,巨型对象是指大于一半region的对象,它不会在survivor区复制 它在ygc时如果没有引用那么优先被回收,毕竟占内存大,早消灭好
前面说过老年代对象内存占用堆内存45%时进行并发标记 JDK9中进行了调整,这个初始值可以设置,后面还会有算法采样数据后进行动态调整
前言:常见的调优一般在图一这几个方面根据业务需求选择合适的回收器,,GC产生时就必然影响性能,从sql与代码层面 做到减少ygc 避免 ogc
这玩意是个经验活 入门阶段 要了解前面的堆内存分布,垃圾回收器,回收算法 然后 掌握图中说的这些
JVM的参数大全官网链接
使用如下命令查看GC的所有默认参数
"C:\work\jdk\jdk1.8.0_101\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
在终端输入就会显示如下默认参数,图中蓝色选中就是 GC时间占比相关参数 99就是 (99-1)/100
垃圾回收单次时间只能占1%的时间
新生代因为 使用的标记复制算法 所以死亡对象回收代价为0毕竟不复制它们就行,并且消耗时间远低于FGC
当然也不是说新生代内存越大越好 如下这曲线与介绍 太小肯定不行,太小的话因为新生代内存不足可能一些没达到生存年龄的对象都直接被放到老年代了,太大也不行,太大的话一次ygc消耗的时间也会相对长点 推荐是 25%~50%
默认晋升老年代阈值是15 可以通过配置修改
如下案例图
案例一:是因为新生代内存小了,所以频繁MinorGC 然后 因为新生代内存小的原因,许多对象没有存活到年龄阈值15就被放到老年代去了 导致fullGC
案例二:是因为重新标记的影响 重新标记会扫描整个堆 ,性能影响较大,所以在重新标记前新生代再做次垃圾回收 如下图左上方参数配置
案例三:这个是因为元空间小了 设置大点就好了
包含了如下这些部分
魔术,版本信息,常量池信息,本类与父类信息,接口信息,属性,方法,成员变量信息,后面会以一个最简单的类来展示它的类文件中的信息 看看就好 如想要深入了解访问如下官网链接
Java Language and Virtual Machine Specifications
如下可以看到 常量池中的东西包含了很多 什么方法名 类名 文件名 变量名 参数名 参数长度 等等
如下图一为源码,a++ 是把变量a的值入操作数栈 再在局部变量表中将a加一 而++a恰好相反
有英语基础的这里应该很容易看懂 if 如果 eq等于 ne不等于 cpm比较 lt 小于等于 都是英文单词首字母的缩写
cinit 简单来说就是把 static的变量与代码块 放到一个方法中 这个方法 就是cinit 类加载时就被执行
就是把 非静态成员 都放到一个init方法中执行 然后构造方法放最后
图一为代码,图二为反编译后的字节码指令 可以看到 可以明确调用对象的 都是 invokespecial
不能明确即有可能方法被子类重写的 则是 invokevirtual 而static方法统一都是 invokestatic
package com.robert.concurrent.test;
public class TestHelloWorld {
public static void main(String[] args) {
TestHelloWorld testHelloWorld = new TestHelloWorld();
testHelloWorld.a2();
testHelloWorld.a3();
testHelloWorld.a4();
TestHelloWorld.a1();
}
static void a1(){}
private void a2(){}
private final void a3(){}
public void a4(){}
}
看完上面这么多字节码指令 想必你已经看出来一些规律
方法运行前 局部变量表就有了 比如有a,b,c三个局部变量那么槽位1,2,3就被填充了
运算之前必须 有load指令 进操作数栈 运算之前必须要load俩操作数进操作数栈 最后再进行iadd之类的指令进行运算 赋值操作 就是 istore 就是改变局部变量表的变量的值 ,i++ 是先load指令进操作数栈再改变局部变量表的值 ++i则相反 记住操作数栈的操作数一旦入栈就只能 被运算而不能改变,唯一有点陌生的指令就是 ldc ld:load c:constant 就是从常量池中加载常量 x 到操作
这里是通过类加载的链接阶段生成了一个虚方法表的玩意 然后具体运行的时候就到 虚方法表中找
调用对象中没找到该方法就找父类的
异常的捕捉是靠 一个exceptiontable的玩意
finally的原理就是 把 finally中的指令往try后添加一份 所以一定会执行,另外finally中不能加return 虽然语法没错 但是可能导致 异常不会被捕捉
12.2 两道面试题
以下返回 20 很明显 但是这类finally中写return 的做法可能使得异常不会被catch住
下面打印 10 return 的变量会被放在一个槽位中 临时存储 除非finally中也有return 否则返回值不会被修改
对代码块加syncronized锁字节码如下 可以看到是通过 monitorenter与 monitorexit 俩命令完成加锁与锁释放的 当异常时 也会调用 monitorexit释放锁
语法糖是啥呢 就是 一个优化 比如默认的无参构造 我们不写 编译器就会帮我们加上
从下图可以看到 字节码 是 调用 add方法都是add的 Object类型 然后get时使用了一个checkcast进行强转 以此来做到泛型擦除
参数和返回值的具体类型 因为可能通过子类重写而改变 所以 反射时 也是先用父类获取 然后获取到后返回 rawtype
如下,我把字节码改了 执行就报错 魔术不对
如下为验证代码
package com.robert.concurrent;
public class LoadClass {
public static void main(String[] args) throws ClassNotFoundException {
// //静态常量不会触发初始化
// System.out.println(B.b);
//类对象.class 不会触发初始化
// System.out.println(B.class);
// //创建该类的数组 不会触发初始化
// System.out.println(new B[]{});
// //不会初始化类B 但会加载B A
// ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
// contextClassLoader.loadClass("com.robert.concurrent.B");
// //类对象.class 不会触发初始化
// ClassLoader contextClassLoader2 = Thread.currentThread().getContextClassLoader();
// Class.forName("com.robert.concurrent.B",false,contextClassLoader2);
//
// //上面是不会触发类初始化的请况 下面会
//
// //首次访问类的静态变量或者方法时
// System.out.println(A.a);
// //子类初始化如果父类还没初始化会引发
// System.out.println(B.c);
// //子类访问父类静态变量 只会触发父类初始化
// System.out.println(B.a);
// //会初始化类B并先初始化类A
// Class.forName("com.robert.concurrent.B");
}
}
class A {
static int a = 0;
static{
System.out.println("a init");
}
}
class B extends A{
final static double b = 5.0;
static boolean c = false;
static{
System.out.println("b init");
}
}
跟代码可以发现 是 大致逻辑是 如果某个用户写的应用类从没被加载过那么
检查应用程序类加载器加载类缓存 是否存在 存在就返回 不存在的话看父类(扩展程序类加载器) 加载过的类缓存 中是否存在 存在就返回 不存在 继续找最上层父类(启动类加载器)加载过的类缓存是否存在 不存在 则 启动类加载器进行加载 没找到 则返回到子类 扩展程序类加载器进行加载
没找到 都是 抛ClassNotFoundExeption异常 被捕捉后 啥也不干 继续运行应用程序最终返回空 直到应用程序类加载器进行加载 找到了 就加载 加载是调的native方法 也就是操作系统实现的 然后返回咯
package com.robert.concurrent.test;
import lombok.val;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class MyClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader myClassLoader = new MyClassLoader();
Class> person1 = myClassLoader.loadClass("Person");
val person2 = myClassLoader.loadClass("Person");
System.out.println(person1 == person2);
MyClassLoader myClassLoader2 = new MyClassLoader();
Class> person3 = myClassLoader2.loadClass("Person");
System.out.println(person1 == person3);
person3.newInstance();
}
}
class MyClassLoader extends ClassLoader{
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
String classPath = "c:\\study\\myclasspathForTest\\"+name+".class";
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
Files.copy(Paths.get(classPath),out);
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name, out.toByteArray(), 0, out.toByteArray().length);
}
}
打印结果如下 可以知道 加载器的不同 == 也会不同
就是热点代码会被缓存起来 hotpot虚拟机也是 热点的意思
解释如图一 简单逻辑的方法 内联就是不进入方法了 直接 与调用者代码合体
比如循环的时候 判断循环次数 小于 集合长度 集合长度不会一直获取 第一次获取后会被缓存起来
简单来说就是反射调用某方法 底层代码是如果超过一定次数 就变为 直接调用该方法