JAVA JVM学习

news2025/4/13 4:16:30

1.JVM介绍

越界检查肯定有用,防止覆盖别的地方的代码。

JVM来评价java在底层操作系统的差异。 

 

2.程序计数器

我们java源代码会变成一条一条jvm指令。

在物理上实现程序计数器,是用一个寄存器。这样速度更快。

 程序计数器不会内存溢出

 

 2.1 线程私有

比如我的线程1,在分配的时间片,只执行到了指令9,这个时候切换到线程2。

程序计数器是线程1私有的。记录线程1下一条要执行的是指令10

 

 2.2 程序计数器不会内存溢出

3.虚拟机栈

一个线程运行需要一个虚拟机栈

一个栈帧就对应着一个方法的调用。

而如果方法1间接调用了方法2,就会把栈帧2放进栈。依次类推。

 然后从栈顶挨个等待CPU的宠幸。

栈顶那个叫活动栈帧。

3.1 关于栈的几个问题

1.每次栈顶的方法执行完毕都会自动弹出栈,不需要垃圾回收管理栈。

2.栈的内存划大,反而会让线程数变少。划大了,只能方便方法的递归调用。

可以通过-Xss设置栈的内存大小。

 3.栈方法内的

多个线程执行一个方法,里面的变量每次都不一样,互不影响,肯定线程安全。

 但如果这个变量是static 变量,会被共享。

如果不加线程保护就不安全。

那如果是多个线程修改同一个对象,那就是线程共享的。

StringBuilder不是线程安全的。

那我们这个时候就得改成StringBuffer了。

 3.2 栈内存溢出

1.栈帧太多。比如没有递归终止条件

就会报错,lang包下的StackOverflowError

 2.栈帧过大

在这里可以用-Xss 256k来指定栈大小。修改虚拟机参数。

 

3.3 线程运行诊断

3.3.1 案例1,cpu占用太高

linux里使用top检测后天内存使用情况,但只能定位到哪个进程的占用高。

 找到32665这个线程占用最高。

二进制的32665转成16进制7F99,jstack里面的线程编号是16进制的。

 

 然后我们找到这个线程

可以看出来哪个类的那行代码出了问题。

 3.3.2 案例2很久没出结果

 然后jstack 进程号 ,查看这个进程

 

 往下翻,发现了一个死锁。说我的Tread 1和0 发生了死锁。

线程1锁了a,然后休眠,线程b想锁a,发现获得不了,就死锁了。

3.4 本地方法栈

调用操作系统底层的本地方法(C或C++),这些本地方法用,就在本地方法栈。

比如object类里的clone方法,这个方法是没有实现的,用C或C++实现。

4.堆

 一个堆溢出的异常,OutOfMemoryError

可以配置-Xmx大小,来指定堆空间的大小

 4.1 堆内存诊断

4.1.1 JPS工具

jps 可以查看运行的进程id。

4.1.2 Jmap工具

 jmap工具用不了说明JDK版本是高于8的,

JDK8需要Jhsdb jmap -heap -pid 加进程号。

我们可以看到堆的一些配置,堆的最大size。

 

 4.1.3 Jconsole

控制台上输入Jconsole,可以监控本地的进程

 

4.1.4 垃圾回收后,内存依然占用很大

用jvisualvm,这是个虚拟机的图形可视化工具

 

 然后挨个检查对象里的内容,哪些内存占用高。

然后发现,这个占用大的对象,一直也没到生命周期的结尾。

根本没有走完一生,不可能被回收。

5. 方法区

1.8 之前,永久代用的堆空间的内存,用永久代实现的方法区。

1.8之后 用的是本地内存的metaSpace元空间实现的方法区。

5.1 方法区的内存溢出

5.1.1 1.8版本方法区的溢出

我们在这里建了1万个类,并加载。

我们可以把最大元空间大小设为8m,-XX:MaxMetaspaceSize=8m

 然后就可能会溢出了。

 5.1.2 1.6版本方法区的溢出

 当溢出的时候报错信息为 PermGen,永久代空间内存溢出。

 5.1.3 内存溢出的场景

spring 动态代理那,都在加载类。

6.常量池

首先一个类编译成字节码的时候,有类基本信息,常量池,类方法定义(包含了虚拟机指令)。

我们找到这个class文件,反编译它。

javap -c xxx.class

得到的结果,前面的一部分是类的基本信息

 常量池,例如 #1  为常量的编号,#1同时引用了#6和#20。

 

再往下看,会发现,编译器会给我们这个类一个默认构造方法。

方法里都是指令加常量,类似汇编语言。

这里的main方法就成了四条指令。比如你想执行指令0,就要引用 #2这个静态成员变量。

 

6.1 运行时常量池

 每个类都有自己的常量池,当这个类在运行时,就把它的常量池放在运行时常量池。

6.2 StringTable面试题(有串池)字符串的生成过程

 

然后我们来看常量池,在刚运行时,a,b就是里面的符号,还没有变成String字符串对象。

执行指令才把符号变成字符串对象。

 

 然后我们就会把刚生成的这个字符串放入串池StringTable,串池是哈希表。

如果串池里有这个对象,就直接拿来用。

6.3 字符串的拼接

6.3.1 拼接两个变量

 

  String Builder的to String 方法,就是 再new 一个String 对象。

字符串的拼接就是String  -> StringBuilder ->String

这题我们就知道是啥了,S3是串池的对象,S4是新new出来的东西,肯定不相等。输出为false

 6.3.2 拼接两个字符串常量

 这样它就会去串池里直接找已经有的对象。

 

那为什么连接这么快呢?

 

6.4 StringTable字符串常量池

这里会往串池里放新串,如果串池里有,那就不再创建。

‘’a‘’就是字符串对象常量,当用双引号引起来的时候就在串池了。

这里产生的 ab是在堆里的字符串对象。

 这里使用了s.intern(),但由于常量池已经有“ab”,所以这个时候s2应该是常量池里的''ab''。

 

因为永久代被字符串常量池占用内存。

 

在JDK1.8下跑几道题。

public class Main {

    public static void main(String[] args) {
        String s1="a";
        String s2="b";
        String s3= "a"+"b";//在连接后,字符串常量里就有新的  "ab"
        String s4= s1+s2;//堆里自己的对象,两变量的连接用的StringBuilder,然后toSting。
       String s5= "ab";//s5用的s3时创建的字符串常量。
        String s6= s4.intern();//发现常量池已经有,那s6就用常量池的。

        System.out.println(s3==s4);//false
        System.out.println(s4==s5);//false
        System.out.println(s4==s6);//false

        System.out.println(System.identityHashCode(s4));// 地址为460141958
        System.out.println(System.identityHashCode(s6));//地址为1163157884
        System.out.println(System.identityHashCode(s3));// 地址1163157884


       System.out.println(s3==s5);//true

        
        // write your code here
    }
 //输出为false
        String x2=new String("c")+new String("d");
        String x1="cd";
        x1.intern();
        System.out.println(x1==x2);
  //输出为false
        String x1="cd";
        String x2=new String("c")+new String("d");
        x1.intern();
        System.out.println(x1==x2);

可以发现,new出来的就是不一样。拼接的才可能一样。

6.5 String table的垃圾回收

它在底层就是一个hash表,数组加链表的形式(被称作桶)。因为要解决冲突。

 在这里疯狂的像StringTable里加数据

 当内存不够的时候,就会触发垃圾回收机制。

6.6 String table的性能调优

6.6.1调整桶数

桶数多,哈希重装小,比较分散,查找速度就快。

调整 -XX:StringTableSize=桶个数
StringTable的底层是哈希表。

可以通过添加选项来调整bucket的大小,也就是哈希表的桶的个数。这个参数调的好,哈希冲突就少,少了的话那运行时间就会快。
-Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
如果字符串常量的个数非常多,那么把桶的大小调高一些。

对于要用到的字符串对象有大量重复的情况,考虑将字符串对象入池 节省堆内存的使用(放入串池就共享字符串啦><)
 

6.6.2 通过串池

如果要往一个list里疯狂插字符串。

可以先添加进串池,再加进list。

 

 7.直接内存

属于是操作系统的内存。

这里java要用系统的命令,cpu就到了内核态。

7.1 传统阻塞IO

整了两个缓冲区。

 

7.2 DirectBuffer

整了一块和操作系统共享的内存区,能不快吗?

 7.3 直接内存溢出(没有垃圾回收)

 

 

 因为ByteBuffer 对象在被回收时,会调用unsafe对象里的直接内存回收方法。

7.3.1 直接内存释放的原理

直接内存的分配与释放,是通过一个unsafe对象完成的。

 调用unsafe的freeMemory方法来释放直接内存。

7.4 禁用显式的垃圾回收对直接内存的影响

为了防止程序员,不用System.gc();

就在虚拟机参数那里设置这个操作的无效。

禁用了System.gc();会影响直接内存的释放

 这里我们只能用unsafe对象,调用freeMerory的方法来直接释放。

8.堆的垃圾回收

8.1 如何判断一个对象可以回收

8.1.1 引用计数法

看一个对象被需要被引用的数量。

但这个东西有一个缺点。

在这种循环引用中,各自的计数都为1,就不会被释放。造成内存的泄露。

8.1.2 可达性分析算法(jVM正在用地)

1.首先得确定一些根对象,(根对象:那些一定不会被垃圾回收的对象)。

2.在垃圾回收之前,会对堆内存中所有的对象扫描。看看每个对象是否被根对象直接或间接地引用,如果没有就作为垃圾被回收。

 但哪些对象可以作为根对象呢?

 

8.2 JVM的四种引用

8.2.1 强引用

 

 A1对象被两个GC Root 对象直接强引用,不可能回收它。

 当引用消失的时候,就会被回收。

8.2.2 软引用

A2对象有强引用的时候不可能被回收,除非它的强引用消失,这个时候只剩下一个弱引用。

在垃圾回收时,如果回收完毕后,内存还是不够,那就回收对象2。

当我们软引用的对象消失后,软引用本身也是一个对象。

如果在创建引用时,分配了一个引用队列, 这个软引用就会被放进  软引用队列

 

8.2.3 弱引用

与软引用差不多

区别就是:只要发生了垃圾回收,这个对象只有弱引用,就回收它。

弱引用的对象被回收,这个弱引用也会被放到队列。

8.2.4 释放引用队列可以释放内存

引用也占内存,可以通过队列来释放引用,来释放内存。

软引用,弱引用,强引用,可以创建队列,也可以不创建队列。

8.2.5 虚引用(释放绑定直接内存的对象)

必须关联一个引用队列。

比如前面学的ByteBuffer,整的是直接内存,当ByteBuffer的强引用消失,它会被垃圾回收,但它指向的那块直接内存却不会被回收。

 当ByteBuffer对象被回收时,让虚引用对象进入队列。

 被一个refrencehandler线程来定时的检查,检查有没有cleaner,如果有,调用unsafe.freeMemory方法,释放这块的直接内存。

8.2.6 终结器引用(对象被回收的真正机制)

必须建立引用队列。

所有的java对象都继承object 父类,里面都有一个finallize()方法。

当这个对象实现了finallize方法,才会有终结器引用。

当没有强引用存在时,这个对象就会被创建一个终结器引用。

当这个对象被垃圾回收时,终结器引用就会被加入到引用队列。由一个finallizeHandler线程来调用对象里的finalize()方法,完成对象的终结。

8.2.7 不推荐用终结器引用

处理终结器引用的线程的优先级很低,对象可能会迟迟得不到终结,内存迟迟得不到释放。

8.3 垃圾回收算法

8.3.1 标记清除算法

就是顺着GC Root往下查可达性,标记那些不可达的对象。

 然后清理这些对象的内存,如何清?

记录这些对象的起始和终结地址,放在空闲的地址列表里,下次给新对象分配内存时,看看这个地址列表里有没有一块空间能容纳我们的新对象。就是覆盖,用新对象覆盖。

优点:快速

缺点:内存碎片太多

8.3.2 标记整理算法

就是顺着GC Root往下查可达性,标记那些不可达的对象。

然后整理一下我们的内存空间,重新根据分配空闲内存地址。

 这就是通过外部技术来解决内存碎片。

整理牵扯到对象在内存上的移动,效率就低。

8.3.2 复制算法

就是顺着GC Root往下查可达性,标记那些不可达的对象。

 整了两块,把存存活的复制到另一块。

然后清理原来的,原来的又变成空白,作为备胎。 

优点:没随碎片。

缺点:占用双倍内存。

8.4 分代垃圾回收机制

虚拟机不可能只用一种算法回收,都是多种结合。

老年代:很久才发生一次垃圾回收

新生代:频繁发生垃圾回收。

8.4.1 新生代第一次内存满(Minor GC)

新生代中的回收被称作Minor GC

我们刚new的对象,放在伊甸园中,当我们创建很多对象,这里的空间就不够了。就会触发一次垃圾回收。就会触发一次复制回收,把GCRoot可达性查询后标记的对象,放进幸存区To中。

复制过去了,会让这些幸存对象的寿命加一。

 然后交换幸存区位置。

 
 8.4.2 新生代第二次内存满 (Minor GC)

新生代中的回收被称作Minor GC

又满了,又触发了垃圾回收机制。

这次可能幸存区中,也有对象过期。

1是这次处理后,伊甸园留下来的对象。

2是这次处理后,幸存区留下来的对象。

 

8.4.3 第N次内存满,弄进老年代。(Minor GC)

当我们发现一个对象存活了很多代,就试图给它弄进老年代。

 

8.4.4 当新生代和老年代里面都放不下新对象(Full GC)

此时就会触发俩个代理,整个的垃圾回收。

8.4.5 总结 

老年代的对象回收,时间更长(STW),老年代采用的算法可能是标记清除或标记整理。

因为它里面的对象不是那么容易被回收。

8.4.6 相关VM参数

8.5 垃圾回收器

STW就是等待时间。

吞吐量优先就是让总STW最短。

响应时间优先:每次让STW最小。

8.5.1 串行垃圾回收器

打开它的参数

新生代用serial串行完成垃圾回收。

老年代用SerialOld完成垃圾回收。

其他线程需要在安全点阻塞,因为里面的对象地址可能会变化。

这个时候只有垃圾回收线程在工作。

8.5.2 并行吞吐量优先

垃圾回收发生时,CPU是这样占用的。

 

MAXGcPauseMillis=ms,最大暂停毫秒数,默认为200。

GCTimeRatio默认是99,一般设为19,使用不超过 1/(1+ratio)的时间用来垃圾回收。比如总时间为100s,不能超过20s来垃圾回收,如果达不到这个要求,paralleGC就会调整堆的大小,一般增大堆,然后垃圾回收的不频繁,垃圾回收的总时间下降,得到吞吐量的提高。但是即使垃圾回收次数变少,总的垃圾回收时间变短,但每次的垃圾回收时间会变长MAXGcPauseMillis就会变大。

8.5.3 响应时间优先(CMS收集器的过程)

concurrent 是并发的。用户线程和垃圾回收线程是并发的。

为啥响应时间短?因为有部分标记的时间是和用户线程一起并发的,之前的标记或清理都是STW(stop to world暂停)。

 有四个阶段。

 然后清除的时候就可以并发清除。

要解答这个问题,我们就要弄清楚:为啥 CMS 垃圾回收器为啥要分成「初始标记」、「并发标记」、「重新标记」、「并发清除」这四个步骤!我直接一步走到底不行吗?为啥要那么麻烦分成四个步骤呢?

答案只有一个:为了降低 Stop the World 的时间!

8.5.4 CMS的分析

1.需要在堆内存使用率那里设置阈值。

 2. 为什么在初始标记和重新标记阶段需要STW。

3.CMS拥有标记清除算法内存碎片的弊端

4.那为什么我们不用标记压缩算法?

因为你在压缩的过程,用户线程也得搬家。适合大STW场景。

5.CMS的CPU敏感问题

 8.5.5 G1垃圾回收器

优先回收垃圾较多的区

如果堆内存花费都小的时候下,它和CMS花费的时间都不相上下。

 

 

8.5.6 G1的youngCollection阶段(第一阶段)

堆里有很多区,E为新生代区,当新生代区满了之后就开始垃圾回收,剩余的放进幸存区S,幸存区的满了之后,触发垃圾回收,没被回收的会放进老年区O。

这个阶段进行GCRoot的初始标记。

 

8.5.7 G1的young Collection和 Concurrent Mark阶段(第二阶段)

这个阶段就是新生代和CM一起发生的阶段。

当老年区的占用堆空间的比例到达阈值,就会触发并发标记。 

8.5.8 G1的Mixed Collection阶段(第三阶段)

优先回收垃圾较多的区。

       在这个阶段就要开始回收幸存S区和老年O区里的垃圾,但是不是回收每一个老年O区,而是有选择地回收老年O区,老年区O里的存活对象一般较多。

       参数MaxGcPauseMills最大暂停时间,此时挑垃圾最多的老年区回收,这样复制的时间更少。

8.5.9 G1的young Collection 中可能发生的跨代引用问题

老年区会再分为card 小区,card里会记录老年代对象引用了哪个新生代对象。在GCRoot查找阶段,有这个映射关系,就会减少时间。

card小区里,如果这个小card区记录了新生代的引用,就称之为脏卡。

 将来在新生代里进行垃圾回收时,可以通过Remembered Set ,直接去老年区里的脏卡里回收,减少GCroot时间。

当引用发生变更时,会完成脏卡的更新用过P[ost-Write Barrier,就是自己写的一小段代码,来更新卡区。

8.5.10 重新标记Remark阶段(G1和CMS) 

使用三色标记,已经确定存活的标记成黑色,正在处理中(可能被回收或存活)的标记成灰色,已经处理完的标记成白色。

8.5.11 G1在 JDK 8u20里实现的 字符串去重

intern是看常量池里有没有值一样的字符串,关注的是String对象。

我们这里是看有没有值一样的字符数组,关注的是字符数组。

使用一个参数打开这个字符串去重的开关:-XX:+UserStringDeduplication

 

8.5.12  G1     在JDK 8u40并发标记类卸载

8.5.13 G1回收巨型对象 

通过卡表查看老年代的incoming引用巨型对象情况。

 

8.5.14 G1 在JDK 9 中调整并发标记的起始时间

之前:并发标记的发生是有一个内存阈值:老年代占堆内存的比例,默认为45%。

现在JDK9:这个阈值不固定,我们设置的阈值,仅作为一个初始阈值。后面还会动态的调整。

8.5.15 G1在JDK9中的更高效垃圾回收

去看文档

8.6 垃圾回收调优

查看当时,虚拟机的参数设置

"F:\JAVA\JDK8.0\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"

 可以看到这些

通过需求来决定合适的回收器

CMS,G1,ZGC 适用于高响应,低延迟。

ParallerGC适用于高吞吐量。

 

 

 

建议新生代占整个堆的 1/4以上和1/2一下。

 

 

 

 

-XX:MaxTenuringThreshold=threshold
调整最大晋升阈值
-XX:+PrintTenuringDistribution

打印晋升的详细信息,以便判断晋升阈值是否更合适。

Desired survivor size 48286924 bytes, new threshold 10 (max 10) 
- age 1: 28992024 bytes, 28992024 total 
- - age 2: 1366864 bytes, 30358888 total 
- - age 3: 1425912 bytes, 31784800 total ...


9.类加载和字节码技术

 javac 指令编译后的文件如下。

常量池里value的含义。

 0a翻译成10进制是10。表明了这是个方法引用。

 太繁琐了,感觉面试不会问。

9.1 多态的原理

每个方法都有它的地址,所以不同的多态对象,调用同名的一个方法,调用的是自己的方法(如果自己重写)或父类的。虚方法表的右侧可能记录的还有父类这个方法的地址。

 

9.2 finally面试题

1.finally里的return会吞异常。本来要抛出异常给上层的,突然返回了。

2.如果在finally里的return之前执行了其它return , 那么最终的返回值是finally中的return

9.3 编译过程(语法糖)

编译之后肯定变成了字节码,写成下面代码的形式是方便理解。

9.3.1 加一个默认构造器

9.3.2 自动拆装箱

 9.3.3 泛型集合取值

9.3.4 泛型反射

虽然List里放的是object类型,但在一张局部变量类型表中,记录了它承载的泛型是Integer。

可以直接反射解析方法里的类型。

 

9.3.5 可变参数

9.3.6 forEach循环

数组就是变成了简单的for

集合则是通过迭代器。

 

 9.3.7 Switch和字符串

 会被编译成如下:就是把字符串中转了一下。让case里永远都是一个字符或一个数字

 

9.3.8 Switch和枚举类

编译器其实就是把MALE,FEMALE属性变成了数字。

 

 

9.3.9 枚举类

9.3.10 try-with-resource资源对象关闭

 编译器帮我们写的finally如下:

9.3.11 方法重写时的桥接

子类重写方法的返回值的类型,可以是父类方法返回值的子类型。

 实际上编译器对子类里多写了一个方法。该方法仅对JVM可见,作为桥接手段

9.3.12 匿名内部类一

这个写法,实际上是编译器,根据里面的内容重建了一个类。 

 

9.3.13 匿名内部类二

 

目的就是不让x再变,因为x再变,内部类里不会跟着变。

 

9.4 类加载

 9.4.1 instanceKlass

instanceKlass:就是类的字节码在本地内存中的C++描述。

将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • _java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法

如果这个类还有父类没有加载,先加载父类,并且加载和链接可能是交替运行的

这个过程,比如Person.class就是堆中的一个类对象,即Person.class就是一个对象。

  • Person.class里记录着它对应的instanceKlass的地址,
  • 它对应的instanceKlass里的属性_java_mirror记录着Person.class的地址
  • 当声明一个Person实例的时候,这个实例有Person.class的地址,找到Person.class,然后找到instanceKlass,来拿自己要用的属性和方法等等......

 

9.4.2 什么是class对象?

(165条消息) Class.forName()用法详解_mocas_wang的博客-CSDN博客_class.forname

看看这个人的笔记

9.5 类的链接

9.5.1 验证

验证类的书写是否符合JVM规范

9.5.2 准备

 

 9.5.3 解析

 然后我们来看看解析是什么意思。

解析:将常量池里的符号引用解析为直接引用(即实际地址),就知道了那些类或方法或属性的地址。

未解析时:常量池中的看到的对象仅是符号,未真正的存在于内存中。

9.6 初始化

 

9.6.1 练习

这里的初始化不会有线程安全问题。

 

9.7 类加载器

比如定义一个student类,首先问问Extension加载器有没有加载过这个类,如果加载过,那就不加载了;如果没有加载过,接着问它的上一层Booststrap加载器,看看它有没有加载过,如果Booststrap加载器也没加载,那就由Application 加载器来加载。如果Booststrap加载器加载器加载了,那就加载了。自底向上访问是否加载。

 Bootstrap加载器来getParent,无法直接访问,因为它是由C++代码写的。

  • 类加载器的作用:通过一个类的全限定名来获取描述此类的二进制字节流,并将此类相关信息加载到JVM的方法区,并创建一个 java.lang.Class 对象作为此类的访问接口, class 对象的引用也保存在方法区内。
  • 每一个类加载器都有独立的类名称空间。比较两个类是否相等的前提是两个类是由同一个类加载器加载的,否则两个类比不相等。从JVM角度来讲,只有两种类加载器:启动类加载器其他的类加载器。因为前者是JVM虚拟机的一部分,后者是独立于JVM实现的。

 

9.7.1 类加载的过程

  1. 根据JVM内存配置要求,为JVM申请特定大小的内存空间;

    JVM启动时按照其配置要求,申请一块内存,并根据JVM规范和实现将内存划分为几个区域。class二进制文件信息被放入“方法区”,对象实例被放入“java堆”等

  2. 创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;

    JVM申请好内存空间后,JVM会创建一个引导类加载器(Bootstrap Classloader)实例,引导类加载器是使用C++语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等。
    引导类加载器(Bootstrap Classloader)会读取{JRE_HOME}/lib下的jar包和配置,然后将这些系统类加载到方法区内。

  3. 创建JVM 启动器实例 Launcher,并取得类加载器 ClassLoader

    此时,JVM虚拟机调用已经加载在方法区的类sun.misc.Launcher 的静态方法 getLauncher(), 获取 sun.misc.Launcher 实例

  4. 使用上述获取的 ClassLoader 实例加载我们定义的类;

    通过 launcher.getClassLoader() 方法返回 AppClassLoader 实例,接着就是 AppClassLoader 加载 我们自定义类.

    加载自己写的类之前先要加载我们写的类中用到的其他类。在 org.luanlouis.jvm.load.Main 类被编译成的class文件中有一个叫常量池(Constant Pool)的结构体,通过这个常量池中的 CONSTANT_CLASS_INFO 常量判断该class用到了哪些类,并通过类加载器去加载这些类。

  5. 加载完成时候JVM会执行Main类的main方法入口,执行Main类的main方法;

  6. 结束,java程序运行结束,JVM销毁


 

9.7.2 启动类加载器(引导类加载器 BootStrap ClassLoader)

启动类加载器是使用C++语言实现的(HotSpot),负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等。
启动类加载器(Bootstrap Classloader)会读取 {JRE_HOME}/lib 下的jar包(如 rt.jar)和配置,然后将这些系统类加载到方法区内。
由于类加载器是使用平台相关的底层C/C++语言实现的, 所以该加载器不能被Java代码访问到。但是,我们可以查询某个类是否被引导类加载器加载过
 

Class.forName既可以完成类的加载,也可以顺便完成类的链接和初始化操作。

那我们要怎么样才能知道这个类是由哪个类加载器加载的呢?

我们通过Class.forName来加载我们写好的F类

 所有的类对象都可以通过getClassLoader()这个方法的得到它的加载器。

这里如果打印出的是null,则说明是启动类加载器加载的这个类。

  1.  java -Xbootclasspath:<new bootclaspath>  ,是用新路径替换原有路径(jre下的lib)。
  2. Xbootclasspath/a:<追加路径>  被指定的文件追加到默认的bootstrap路径中。

9.7.3 扩展类加载器(Extension ClassLoader)

JDK9中,被平台类加载器(platform ClassLoader)取代

  • 此加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 {JAVA_HOME}\lib\ext 目录下的类库, 开发者可以直接获取此加载器。
  • 拓展类加载器是是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的。
  • 该加载器加载的类需要打成jar包。

再打印它的类加载器,就成了扩展类加载器。 

 

 本来应该由应用程序加载器加载的,怎么被截胡了呢?

原理解释:Application ClassLoader先问Extension ClassLoader  是否加载过这个类,在Extension ClassLoader 的路径(也即是lib下的ext文件夹)中发现了我们的jar包,所有Application ClassLoader 就不加载 我们的jar包。

9.7.4 应用程序加载器 (Application ClassLoader)

此加载器负责加载用户类路径上指定的类库,若没有指定自定义加载器,则此加载器一般是程序中默认的加载器。
应用类加载器将拓展类加载器当成自己的父类加载器。

9.7.5 用户自定义类加载器(Customized Class Loader)

用户可以自己定义类加载器来加载类。所有的类加载器都要继承 java.lang.ClassLoader 类并重写 findClass(String name) 方法。用户自定义类加载器默认父加载器是 应用程序加载器



9.7.6 双亲委派模式

工作过程:一个类加载器收到类加载的请求,它首先会把这个请求委派给父类加载器去完成,层层上升,只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。


父加载器和子加载器的关系是组合关系而不是继承关系。子加载器中有一个私有属性 parent 指向父加载器。

  1. 当应用程序加载器尝试加载类的时候,首先尝试让其父加载器–拓展类加载器加载
  2. 如果拓展类加载器加载成功,则直接返回加载结果 Class<T> instance , 加载失败,则会询问是否引导类加载器已经加载了该类;
  3. 当没有加载器加载时,应用加载器才会自己加载。

从源码看双亲委派模型:

public Class<?> loadClass(String name) throws ClassNotFoundException {  
    return loadClass(name, false);  
}  

protected Class<?> loadClass(String name, boolean resolve)  
    throws ClassNotFoundException  
{  
    synchronized (getClassLoadingLock(name)) {  
        // 首先,检查是否已经被当前的类加载器加载,如果已经被加载,直接返回对应的Class<T>实例  
        Class<?> c = findLoadedClass(name);  
            //初次加载  
            if (c == null) {  
            long t0 = System.nanoTime();  
            try {  
                if (parent != null) {  
                    //如果有父类加载器,则先让父类加载器加载  
                    c = parent.loadClass(name, false);  
                } else {  
                    // 没有父加载器,则查看是否已经被启动类加载器加载,有则直接返回  
                    c = findBootstrapClassOrNull(name);  
                }  
            } catch (ClassNotFoundException e) {  
                // ClassNotFoundException thrown if class not found  
                // from the non-null parent class loader  
            }  
            // 父加载器加载失败,并且没有被引导类加载器加载,则尝试自己加载  
            if (c == null) {  
                // If still not found, then invoke findClass in order  
                // to find the class.  
                long t1 = System.nanoTime();  
                // 自己尝试加载  
                c = findClass(name);  

                // this is the defining class loader; record the stats  
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);  
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);  
                sun.misc.PerfCounter.getFindClasses().increment();  
            }  
        }  
        //是否解析类   
        if (resolve) {  
            resolveClass(c);  
        }  
        return c;  
    }  
} 

9.7.7 SPI机制,JDBC打破双亲委派和线程上下文类加载器

可以看下面这篇

SPI 机制以及 jdbc 打破双亲委派

在获得数据库连接池时,我们只用了一个DriverManager类,但显然我们也需要加载mysql-connector-java.jar包里的类,DriverManager类是JDK核心库里的类,由bootstrap类加载器加载。

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/xxx?serverTimezone=GMT", "root", "123456");

如果我们不手动写

 Class.forName("com.mysql.jdbc.Driver");

也可以在DriverManager类里完成 对com.mysql.jdbc.driver类的加载。

如何打破的双亲委派模式?

  • 首先,肯定DriverManager不能是直接调用Class.forName方法加载的(可以调用指定加载器的Class.forName方法),因为Class.forName 默认会采用调用者自己的类加载器去加载DriverManager 的类加载器启动类加载器,加载不到第三方驱动类com.mysql.jdbc.driver,所以DriveManager 必须要使用次一级的类加载器去加载。

SPI    和   线程上下文类加载器

先介绍一个SPI(Service Provider Interface )机制(或者说JDK指定的规范)

  •       遵守JDK的约定:在jar包的META-INF/service包下,用接口全限定名(所谓全限定名 = 包名 + 类名)建立一个文件,文件的内容是 实现的类(第三方,要被加载的类)的名称。
  • ServiceLoader的load方法,把这个文件里的所有类都加载了,返回类对象集合。

  • load方法的内部使用了线程上下文类加载器
  • 我们的jvm默认把应用程序加载器赋给 当前线程,所以getContextClassLoader拿到的是Application ClassLoader。

 

 

 

然后再看看里面的loadInitialDrivers()如何加载com.mysql.jdbc.driver类。

  1. 使用SPI (Service Provider Interface ),也就是线程上下文类加载器(默认为应用程序加载器。
  2. 直接使用指定加载器的的Class.forName加载com.mysql.jdbc.driver

 

 

9.7.8 自定义类加载器

1.想加载任意路径下的类文件

2.一个接口可能有不同的实现,用不同的类加载器 

3.比如一个类可能有多种版本(字节码编译版本),让新旧版本同时工作。tomcat容器

 

步骤:

  1.  只有重写findClass方法,才会委托上级的类加载器优先进行类的加载,只有上级类加载器没有找到Class的时候,才会掉用  findClass() 在本身的类加载器上查找,用本身的类加载器。 
  2. 接下来在findClass里读取那个类文件的字节码(即byte数组),然后调用父类的defineClass方法来完成类的加载
  3. 然后我们的使用者,就可以调用我们自定义类加载器的loadClass方法来加载类。

 

10.运行期优化

看一个小例子,内存for 循环1000次创建对象,外层循环记录每次创建对象花费的时间。

 

 

可以发现创建对象的时间越来越快。这说明JVM在运行期间对我们的代码优化

10.1 即时编译

10.1.1 分层编译 和 逃逸分析

即时编译器:将反复执行的代码的机器码存在Code  Cache上

  1.  第0层就是普通的把字节码编译成机器码。
  2.  C2是C1的优化版本的即时编译器,如果profiling发现某个方法被太频繁的调用,就会让C2来编译这个方法

 

逃逸分析:C2即时编译器可能通过分析发现某些孤立的代码,然后过滤掉这些代码(更改原来的字节码)。可以通过一个开关来关闭逃逸分析。-XX:-DoEscapeAnalysis

 

 

 10.1.2 方法内联

方法内联就是把调用方函数代码"复制"到调用方函数中,减少因函数调用开销的技术

内联例子如下:

private int add2(int x1 , int x2 , int x3 , int x4) {
return add1(x1 , x2) + add1(x3,x4);
}
  
private int add1(int x1 , int x2) {
return x1 + x2;
}

运行一段时间后,代码被内联翻译成:

private int add2(int x1 , int x2 , int x3 , int x4) {
//return add1(x1 , x2) + add1(x3,x4);
return x1 + x2 + x3 + x4;
}

那为什么要方法内联呢?

函数调用过程

  1. 首先会有个执行栈,存储它们的局部变量、方法名、动态连接
  2. 当一个方法被调用,一个新的栈帧会被加到栈顶,分配的本地变量和参数会存储在这个栈帧
  3. 跳转到目标方法代码执行
  4. 方法返回的时候,本地方法和参数被销毁,栈顶被移除
  5. 返回原来的地址执行

有压栈和出栈的过程,因此,函数调用需要有一定的时间开销和空间开销。

当一个方法体不大,但又频繁被调用时,时间和空间开销变得很大,不划算

方法内联的条件

JVM会自动的识别热点方法,并对它们使用方法内联优化。那么一段代码需要执行多少次才会触发JIT优化呢?通常这个值由-XX:CompileThreshold参数进行设置:

  • 使用client编译器时,默认为1500(即C1即时编译器)
  • 使用server编译器时,默认为10000(即C2即时编译器)

方法题如果太大,即时标记为热点,JVM仍然不一定会对它做方法内联优化

  • 如果方法是经常执行的,默认情况下,方法大小小于325字节的都会进行内联(可以通过** -XX:MaxFreqInlineSize=N**来设置这个大小)
  • 如果方法不是经常执行的,默认情况下,方法大小小于35字节才会进行内联(可以通过** -XX:MaxInlineSize=N**来设置这个大小)

我们可以通过增加这个大小,以便更多的方法可以进行内联;但是除非能够显著提升性能,否则不推荐修改这个参数。因为更大的方法体会导致代码内存占用更多更少的热点方法会被缓存,最终的效果不一定好。

同时我们也可以取消某个方法的内联,使用命令

内联的隐藏条件

虽然JIT号称可以针对代码全局的运行情况而优化,但是JIT对一个方法内联之后,还是可能因为方法被继承,导致需要类型检查(比如说多态)而没有达到性能的效果。想要对热点的方法使用上内联的优化方法,最好尽量使用final、private、static这些修饰符修饰方法,避免方法因为继承,导致需要额外的类型检查,而出现效果不好情况

10.1.3 字段优化

对象字段读取优化的

  1. 缓存读取
    static int bar(Foo o, int x) {
      int y = o.a + x;  // 将o.a存入缓存
      return o.a + y; 
    }
    //实例字段Foo.a被读取两次,即时编译器会将第一次读取的值缓存起来,并且 替换 第二次的字段读取操作,以 节省 一次内存访问
    
    
    //优化后为
    
    static int bar(Foo o, int x) {
     int t = o.a;
     int y = t + x;
     return t + y;
    }
  2. 去掉不可达分支
    static int bar(Foo o, int x) {
      o.a = 1;
      if (o.a >= 0)  // 缓存中o.a为1,该行判断可以默认为true(IR图中自动消除该行)
        return x;
      else  // 冗余无效代码,(IR图中自动消除该else分支)
        return -x;
    }
    
    // 优化后程序
    static int bar(Foo o, int x) {
      o.a = 1;
      return x;
    }
    
    
  3.     去除重复赋值语句:如果两次赋值语句之间,变量没有被间接存储到其他字段或没有被方法调用,JVM会消除第一处的冗余赋值指令
    class Foo {
      int a = 0;  // 冗余重复代码,被优化(IR图中被去除)
      void bar() {
        a = 1;  // 冗余重复代码,被优化(IR图中被去除)
        a = 2;  // 最终执行的赋值语句
      }
    }
    
    // 优化后程序
    class Foo { 
      void bar() {
        a = 2;  // 最终执行的赋值语句
      }
    }
    
    // 重复代码消除
    int bar(int x, int y) {
      int t = x*y;  // 重复冗余代码,去除(x*y)
      t = x+y;
      return t;
    }

可能出现异常时,无法进行字段优化

// 除零异常
int bar(int x, int y) {
  int t = x/y;  // 由于可能出现y=0的算术操作异常,所以该行不能被优化去除
  t = x+y;
  return t;
}

如果所存储的字段被标记为 volatile ,那么即时编译器也 不能消除冗余存储

此时如果对变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。

也就是说volatile修饰的变量,每次的变动必须得跟内存互动一下,会影响即时编译器的字段优化

同理,加解锁操作同样也会阻止即时编译器的字段读取优化。(比如volatile修饰的变量为T,你第一次为T赋值完,立马给它加了锁,然后再第二次对它进行赋值。第一次的值,很明显被锁上了,所以不能优化第一次的赋值。)

10.1.4 方法的反射优化   和反射调用慢的原因

 首先我们得知道什么是java的反射机制

  • JAVA反射机制是在 运行状态中,对于任意一个 类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
  • 在执行方法时,局部变量会压栈,如果在方法中new一个类的实例(即类对象),那么会去方法区先找到这个类的信息,如果找不到就先加载这个类,在java堆上会保存一个类的实例,用这个实例的newInstance方法就可以创造出这个类的对象
    java.lang.Class Class 类对象名称 = java.lang.Class.forName(要实例化的类全称);
    类名 对象名 = (类名)Class类对象名称.newInstance();

 这里invoke就是调用  Reflect1这个类里的静态方法foo,因为是静态方法,不需要借助

为什么从第16次之后,速度就变快了呢? foo为Method对象

先来看看JDK里Method.invoke()是怎么实现的。 


它调用一个方法访问器MethodAccessor里的invoce方法,这里的MethodAccessor是一个接口

 MethodAccessor这个接口有三个实现:DelegatingMethodAccessorImpl,MethodAccessorImpl和NativeMethodAccessorImpl

 

DelegatingMethodAccessorImpl里啥也没实现,在前16次它默认调用NativeMethodAccessorImpl(本地方法访问器)

我们先看NativeMethodAccessorImpl(本地方法访问器)的源码

在前16次它不满足if里的条件,会调用方法invoke0(C++实现,性能较低),

 

 invoke0被调用的次数会被记录下来,当调用次数大于inflationThreshould(膨胀阈值默认16,这个阈值可以设置),NativeMethodAccessorImpl就会被替换成一个新的运行期间动态生成的方法访问器类(这个类没有源代码,在运行期间动态生成的)GeneratedMethodAccessor1

 

第17次调用时,使用这个新生成的方法访问器类GeneratedMethodAccessor1,这个类继承了MethodAccessorImpl父类,当然因此也间接实现了MethodAccessor接口,在我们的以往理解中invoke是方法反射调用,但实际在他生成的invoke代码里面,已经变成了Relect1.foo(),因为foo是静态方法,所以他是通过类名.静态方法名调用的,那这还是不是反射调用呢,很明显,已经不是了。

所以我们从第17次开始,java虚拟机已经把我们的反射方法调用转换成了正常的方法调用,性能和直接调用foo方法差距很小。

需要注意的是,膨胀阈值是可以进行设置的,即他是取了环境变量System.getProperty("sun.reflect.inflationThreshold");的值,所以可以自己去指定。

另外还可以通过另一个变量System.getProperty("sun.reflect.noInflation");即noInflation是不要膨胀,如果给他设置为ture,就相当于他会一上来就直接使用这个生成后的MethodAccessor,就不会使用那个本地MethodAccessor了。

当然我们也可以不用设,因为你虽然可以用noInflation来禁用膨胀,但是首次生成GeneratedMethodAccessor1是比较耗时的,如果你这个方法(比如 foo)只是反射调用一次,那感觉就会有些不太划算。

 为什么java反射调用方法效率慢?

原因:

  1. 接口的通用性,java的invoke方法是传object和object[]数组的,基本数据类型的参数需要装箱和拆箱,产生大量额外的对象和内存开销,频繁触发GC。
  2. 编译器难以对动态调用的代码提前优化,比如不能方法内联。
  3. 反射查找时,按名检索类和方法,有一定的时间开销。

11.JMM( java Memory Model)java内存模型

11.1 原子性

通过synchronized 关键字来保证操作的原子性。

 

 

先停一下,学JUC去

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/158705.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

clickhouse整合ldap,无需重启

测试你的ladp服务ldapsearch -x-bdcexample,dccom -H ldap://ldap.forumsys.com应该输出类似以下的内容# extended LDIF # # LDAPv3 # base <dcexample,dccom> with scope subtree # filter: (objectclass*) # requesting: ALL # ​ # example.com dn: dcexample,dccom o…

【Premake】构建工程

Premake 一、什么是Premake&#xff1f; Premake 是一种命令工具&#xff0c;通过读取项目脚本&#xff0c;来生成各种开发环境的项目文件。 开源地址&#xff1a;https://github.com/premake/premake-core 下载地址&#xff1a;https://premake.github.io 实例地址&#xf…

揭秘HTTP/3优先级

编者按 / 相对于HTTP2&#xff0c;HTTP/3的优先级更加简单&#xff0c;浏览器厂商更可能实现统一的优先级策略。本文来自老朋友Robin Marx&#xff0c;已获授权转载&#xff0c;感谢刘连响对本文的技术审校。翻译 / 核子可乐技术审校 / 刘连响原文链接 / https://calendar.per…

【MySQL数据库入门】:面试中常遇到的 ‘ 数据类型 ’

文章目录数据类型1.数据类型分类2.数值类型2.1 tinyint类型2.2 bit类型2.3 小数类型2.3.1 float2.3.2 decimal3.字符串类型3.1 char3.2 varchar3.3 char和varchar比较4.日期和时间类型5.enum和set数据类型 1.数据类型分类 2.数值类型 2.1 tinyint类型 create table tt1(num t…

解决unable to find valid certification path to requested target

问题描述 最近java程序去调用远程服务器接口时报错了&#xff1a; I/O error on POST request for “https://XXX.xyz/create”: sun.secu rity.validator.ValidatorException: PKIX path building failed: sun.security.provi der.certpath.SunCertPathBuilderException: una…

终极 3D 图形工具包:Ab3d.PowerToys 10.2.X Crack

Ab3d.PowerToys改进了 Ab3d.Utilities.Triangulator 通过添加对带孔的多个多边形进行三角剖分的支持&#xff08;之前只能对没有任何孔的单个多边形进行三角剖分&#xff09;。这可用于从文本创建 3D 网格。 Ab3d.Utilities.PolygonAnalyzer 现在是一个公共类&#xff0c;可用于…

【学习笔记】【Pytorch】五、DataLoader的使用

【学习笔记】【Pytorch】五、DataLoader的使用学习地址主要内容一、DataLoader模块介绍二、DataLoader类的使用1.使用说明2.代码实现好的文章学习地址 PyTorch深度学习快速入门教程【小土堆】. 主要内容 一、DataLoader模块介绍 介绍&#xff1a;分配数据集。 二、DataLoade…

EMS运行数据处理-pandas降采样、合并多表

文章目录read_csv读取出错。因为多余异常列数据解决方法pd.to_datetime(df[time_key])但time_key出现不能转换的序列解决方法pandas 提取时间序列年、月、日方法一:pandas.Series.dt.month() 方法提取月份方法二:strftime() 方法提取年、月、日方法三:pandas.DatetimeIndex.mon…

【阶段三】Python机器学习20篇:机器学习项目实战:AdaBoost回归模型

本篇的思维导图: 项目实战(AdaBoost回归模型) 项目背景 本项目应用AdaBoost回归算法进行项目实战,整体流程包括数据收集、数据预处理、探索性数据分析、特征工程、模型构建及优化、模型评估。 数据收集 本次建模数据来源于网络,数据项统计如下: 编号

问题:在 ArcMap 中编辑数据时,无法使用捕捉功能

问题&#xff1a;在 ArcMap 中编辑数据时&#xff0c;无法使用捕捉功能 说明 编辑时&#xff0c;捕捉命令无法按预期运行。无法连接要素&#xff0c;因为指针没有捕捉到地图文档中的边缘和折点。 原因 此问题可能由以下原因之一引起&#xff1a; 捕捉选项已禁用 当前编辑会…

foxmail 发送邮件到 Poste邮件服务的端口设置

Poste服务器 发件端口设置的是 587 端口&#xff0c;没有开通 465 端口。 在foxmail用户账号设置中&#xff0c;发送端口不使用 ssl 默认是 25&#xff0c;使用 ssl 使用的是 465 端口。 一、无效设置的3种情况 1、在发送邮件的时候&#xff0c;不使用 ssl&#xff0c; 端口…

某固态放大器输出单次微波脉冲信号测量方案

某固态放大器输出单次微波脉冲信号测量方案摘要测量指标范围频率测量测量方案仪器选择衰减器混频器信号发生器频谱分析仪可行性分析脉宽和功率测量方案一方案二仪器选择检波器衰减器示波器可行性分析摘要 某固态放大器输出单次微波脉冲信号测量&#xff0c;需测量单个脉冲的频…

Java锁之ReentrantLock(源码详解)

视频地址Java学习文档 ReentrantLock 这个Java中重要的锁&#xff0c;我想可能很多人只是听过&#xff0c;并没有使用过&#xff0c;我在看RocketMQ客户端源码的时候发现大量的使用了这个ReentrantLock&#xff0c;从而引起了我的兴趣&#xff0c;下面我们一起从源码的角度来学…

JDK7时间相关类超详细总结(含多个实例)

JDK7时间相关类一、概述二、Date类1.构造函数2.常用函数1️⃣格式2️⃣实例三、 SimpleDateFormat类1.概述2.构造方法3.常用方法1️⃣格式2️⃣实例四、Calendar类1.概述2.使用方法3.常用方法4.实例五、结语一、概述 本文主要介绍JDK7中的时间相关类 二、Date类 1.构造函数 …

JSP SSM评估文档管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 JSPSSM评估文档管理系统 是一套完善的系统源码&#xff0c;对理解JSP java SrpingMVC mybiats 框架 MVC编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;以及相应配套的设计文档 &#xff0c;系统主要采用B/S模式开发。 研究的基本…

从零备战蓝桥杯——动态规划(子序列篇)

文章目录啥也别说了直接进入正题&#xff1a;不连续子序列问题300. 最长递增子序列1143. 最长公共子序列1035. 不相交的线连续子序列问题674. 最长连续递增序列718. 最长重复子数组53. 最大子数组和编辑距离问题392. 判断子序列困难题&#xff1a;115. 不同的子序列583. 两个字…

使用 ORM 方式查询 Mongodb 里的数据,再也不用记 Mongodb 的语法(ORM Bee)

使用ORM方式查询Mongodb里的数据,再也不用记Mongodb的语法&#xff08;ORM Bee) Mongodb的语法可读性差&#xff0c;要写复杂查询&#xff0c;要求技术能力高&#xff1b;Java驱动&#xff0c;还要使用另一种语法&#xff1b;学习成本太高了。 可以使用ORM方式&#xff0c;轻松…

数字IC设计、验证、FPGA笔试必会 - Verilog经典习题 (四)移位运算与乘法

数字IC设计、验证、FPGA笔试必会 - Verilog经典习题 &#xff08;四&#xff09;移位运算与乘法 &#x1f508;声明&#xff1a; &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN博客 &#x1f9e8;未经作者允许&#xff0c;禁止转载 &#x1f511;系列专栏&#xff1a;牛客Ve…

C++:函数对象:Lambda:Lambda详解(三)

1&#xff1a;定义 lambda表达式就是一个函数&#xff08;匿名函数&#xff09;&#xff0c;也就是一个没有函数名的函数。为什么不需要函数名了&#xff1f; 因为我们直接&#xff08;一次性的&#xff09;用它&#xff0c;不需要其他地方调用它。lambda表达式也叫闭包&#x…

K8s部署前后端分离项目(二)

K8s容器部署两个项目&#xff08;主节点操作&#xff09; 1、创建yaml文件 master节点上创建todo-list.yaml文件&#xff0c;两个jar包设置了两个副本&#xff0c;vue设置了一个副本&#xff0c;端口号固定配置。 当前文件目录为&#xff1a;/home/docker 创建yaml文件命令为&…