JVM系统学习

本文详细记录了黑马满老师视频中的内容,涵盖了JVM的优点、Java字节码运行环境、内存结构(程序计数器、虚拟机栈、本地方法栈、堆、方法区和直接内存)、垃圾回收机制、内存溢出及诊断技巧,以及性能优化策略,如字符串去重和类加载器优化。
摘要由CSDN通过智能技术生成

文章概述

学习的是黑马满老师的视频:视频地址
之前学习都是只看不记笔记,导致学了就忘,想去,再复习也没地方复习,所以现在学习会开始记笔记,下面就是学习完视频记录的所有内容,其中一些图片内容来自视频截图。

引言

jvm的好处

java的(二进制字节码)运行环境

  1. 一次编写,到处运行
  2. 自动内存管理,垃圾回收功能
  3. 数组下标越界检查
  4. 底层用虚方法实现多态

image.png

内存结构

1 程序计数器

Program Counter Register
Java代码运行流程:
代码-解释器-机器码-cpu

  • 作用:记住下一条指令地址
  • 特点
    • 线程私有
    • 不会存在内存溢出

2 虚拟机栈

栈-线程运行需要的内存空间
栈桢-每个方法运行时需要的内存(存参数、局部变量、返回地址)
每个线程只有一个活动栈桢,对应当前正在执行的方法

相关面试题

  1. 垃圾回收是否设计栈内存?
    • 不需要,会自己弹出栈
  2. 栈内存分配越大越好吗?
    • 不是,物理内存时一定的,栈内存越大,线程数就会少了。
  3. 方法内的局部变量是否线程安全?
  • 局部变量是线程私有的线程安全。
  • 作为方法参数传递的时候会有线程安全问题(会被共享)。
  • 作为方法返回值也有有问题。(会被共享)
  • 结论:没有逃离方法的作用访问范围是安全的,局部变量引用对象并逃离作用范围,有线程安全问题。

栈内存溢出

错误名称:java.lang.StackOverflowError
原因:

  • 栈桢过多(递归没有结束条件,循环引用一个里面有另一个)
  • 栈桢过大

线程运行诊断

  • 案例1 : cpu占用过多
    • 定位怎么占用

(1): linux 下
top:查看系统占用,看是哪个进程占用过高
ps :看是哪个线程占用过高
ps H -eo pid,tid,%cpu | grep 进程id
(2):用jdk的方法
根据jstack找到有问题的线程,进一步定位到问题的行数

  • 案例2 :程序运行很长时间没有结果

jstack+进程id 能看见死锁

3 本地方法栈

native method stacks:用c/c++编写的api
比如: Object的clone()
wait()、hashcode()、wait()、notify()、notifyAll()

4 堆

heap堆:用new 创建的对象使用堆内存
特点:

  • 线程共享,需要考虑线程安全问题
  • 有垃圾回收机制

堆内存溢出

java.lang.OutOfMemoryError:Java heap space

堆内存诊断
  1. jps 工具
    • 查看系统中有哪些java 进程
  2. jmap工具
    • 查看堆内存占用情况
  3. jconsole工具
    • 连续监测

使用jmap的时候出现问题,image.png
经搜索可能是jdk版本问题,于是改成jdk11
image.png
image.png
image.png
搞错了…再来,要改环境里的mac版本,因为用的是命令行
mac下的操作:
先进入/Library/Java/JavaVirtualMachines找到版本号名称
vim ~/.bash_profile
修改 JAVA_HOME的版本号
source /etc/profile保存即可
参考https://www.jianshu.com/p/4fd5f6bc6dfb
然后就OK了
image.png
但是问题仍然没有解决…
image.png于是使用提示的
image.png
…??又出现了一开始的问题,人麻了
权限问题
image.png
还是不行…
https://zhuanlan.zhihu.com/p/399352540
刷到一个帖子…还是去年的,算了放弃,麻了还是用jconsole


  • 案例:垃圾回收之后内存占用仍然很高

先装jvisualVM
https://blog.csdn.net/Tanganling/article/details/119790892


5 方法区

定义:是所有java虚拟机线程共享的一个区(现在在本地内存上),存储了和类的结构相关的一些信息:运行时常量池、成员变量,方法数据,成员方法,构造器方法以及一些特殊方法。虚拟机启动时创建,在逻辑上是堆的一个组成部分。永久代和元空间是方法区的一个实现。方法区也会内存溢出
image.png

内存溢出

  • 1.6永久代(class,类加载器、串池等),和堆在一起
  • 1.8元空间(class,类加载器、串池等),放在本地内存

内存不足时:java.lang.OutOfMemoryError
场景:spring和mybatis用cjlib代理动态生成类生成很多运行时的类,永久代垃圾回收比 较差。

常量池

实际上就是一张表,根据这张表找到要执行的类名、方法名、参数类型、字面量等信息。
二进制字节码包括:类基本信息,常量池,类方法定义,虚拟机指令
getstatic:获取静态变量
ldc: 加载一个参数
invokevirtual:执行一次虚方法调用

运行时常量池

一个类运行时要放到内存里,放在内存中的位置就是运行时常量池

StringTable 串池

常量池中的信息都会被加载到运行时常量池,还没变成java字符串对象。
ldc的时候,会把到StringTable里面去找,没有的话会把符号变成字符串对象放进串池。用到的时候才区创建(懒加载)。
StringTable是一个hashtable的结构,不能扩容。

  • 拼接

String s3 = s1+s2 ;
创建一个新的StringBuilder并调用构造方法。
image.png
image.png
s3是串池中的,s4是new的,所以false
image.png
s5是看拼接后的值(在编译器拼接的,因为都是常量,之前的s1,s2都是常量)串池中是否有,所以是true。
image.png
动态拼接的元素不会存在串池中,像下面这种,"ab"不会存在串池中,会放到堆里,只有常量才会放进去
image.png
调用s.intern()方法会尝试放入串池,并把串池中的对象返回
image.png
两个都是true,因为都是同一个
image.png
上ture 下false

StringTable位置

1.8从永久代移到了堆中,因为永久代只有在full gc的时候才回收,现在min gc就会回收

  • 性能调优
    • 串池底层是哈希表,桶的个数可以设置大了,效率就会提升。设置虚拟机参数-XX:StringTableSize=20000
    • 使用intern,将数据入池,避免重复创建对象放入堆中,减少内存占用

6 直接内存

是操作系统内存

  • 常见于NIO操作,用于数据缓冲区
  • 回收成本高,读写性能高
  • 不受JVM内存回收管理
    • 使用unsafe分配和释放内存(freeMemory)的
    • ByteBuffer实现内部(gc的时候ByteBuffer对象没了,所以这个引用就没了,间接调用后面的)虚引用+回调,用一个Cleaner创建需引用对象,虚引用关联的实际对象被回收以后,就会调用clean方法执行删除任务image.png

内存溢出:java.lang.OutOfMemoryError: Direct buffer memory
为什么快??image.png
image.png
在操作系统划出一个区域,java代码可以直接访问(java和系统都能用)
之前要走两次缓存,现在走一次就行了,少了一次缓冲区的复制操作

垃圾回收!!

判断是否可以被回收

  • 引用计数

会出现循环引用的问题

  • 可达性分析算法

根对象:肯定不会是垃圾的

  • 系统类
  • 操作系统方法(Native Stack)
  • 活动线程
  • 锁(monitor)

所以不会被根引用的都gg

1 四种引用

image.png

  • 强引用
    • new一个对象给一个变量,该变量就强引用了这个对象
    • 就是从gc root能找到
    • 所有gc root对他的链都断了,就gc
  • 软引用
    • 没有被强引用引用
    • 如果gc完了返现内存还是不够,且没有强引用引用时,就会gc软引用
    • 使用场景:
      • 内存敏感的情况下,创建对象时用SoftReference包裹,容易被释放。也可以用弱引用。
      • 软引用本身也占空间。用引用队列清除。在创建SoftReference和引用队列关联。
      • 软引用关联的对象被回收时,软引用自身就会进入队列。
      • 手动remove
  • 弱引用
    • 不管内存是否充足,都会gc
    • 软弱引用可以配合引用队列使用(释放通过引用队列,遍历释放)
    • 使用场景:
      • 用weakReference
  • ??什么事软弱引用没有说
  • 虚引用
    • 必须配合引用队列使用
    • 之前的ByteBffer就是
    • 虚引用引用的对象被垃圾回收时,会进入引用队列,有线程会去队列里找,然后会找,调用clean方法,去删掉(unsafe)
  • 终结器引用
    • 必须配合引用队列使用
    • 对象重写了终结方法没有强引时,虚拟机会创建终结器引用,并放入引用队列,会有线程(优先级很低)去队列里找,调用finallize()方法,并回收。
    • 太复杂不考虑

image.png

2 垃圾回收算法

  • 标记清除

优点:速度快
缺点:会产生碎片

  • 标记整理

优点:没有碎片
缺点:因为要移动,速度较慢

  • 复制

把存活的的放到to,清除from,然后交换from和to,to总是空闲的
优点:不会产生碎片
缺点:会占用双倍的内存空间

3 分代回收

区域划分:
image.png

  • 工作流程

对象一开始分配伊甸园中,新生代放不下时,会触发Minor GC。
gc之后会用到幸存区,使用复制算法活下来的放到幸存区to中,同时寿命+1。
之后在min GC的时候,对伊甸园和幸存区同时复制算法并修改寿命。
当对象寿命达到阈值(最大15,4 bit)时,放到老年代。
当老年代空间不足时,尝试min gc,空间仍然不足,会执行Full GC(时间更长),实在不行就outof Memory

  • tips
    • Minor gc 和Full GC会引发stop the world,暂停其他线程,gc线程先工作
    • 大对象(新生代不够了)直接晋升到老年代。
    • 一个java线程的内存溢出不会导致整个线程gg
    • 相关参数
      image.png

4 垃圾回收器

  1. 串行
    • 单线程
    • 堆内存较小,适合个人电脑

image.png,老年代时标记整理

  1. 吞吐量优先
    • 多线程
    • 堆内存较大,要求多核cpu支持,适合工作在服务器上
    • 单位时间内,stw最短,让整体stw大的事件尽可能小
    • 所有的线程都会暂停去回收垃圾

image.png(并行)新生代复制,老年代标记整理,这两个开关是连带开启的。
多个线程一起回收垃圾。几个核几个线程
image.png可以控制线程数
image.png可以自适应新生代(伊甸园、幸存区、阈值)大小
image.png:会改变堆的大小改变吞吐量的大小。
image.png:最大暂停毫秒数。和上面是冲突的,因为堆大了之后要回收的垃圾多了。

  1. 响应时间优先(cms)
    • 多线程单线
    • 堆内存较大,要求多核cpu支持,适合工作在服务器上
    • 尽可能让单次stw的时间最短,让每个用户体验都好。

image.png:并发标记清除。工作在老年代。
并发失败的时候会退化成image.png单线程,标记整理
image.png:配合上面使用,工作在新生代,复制算法
不需要stw ,可以和用户线程一起执行,工作在老年代
image.png并行(所有工作的)的是cpu核数,并发(处理垃圾的线程)的一般设置为1/4。
image.png:
浮动垃圾:垃圾清理时产生的垃圾,所以需要预留一点空间保留浮动垃圾。
这个值越小,开始gc越早
image.png:标记之前先对新生代做一次垃圾回收,因为新生代的可能会引用老年代,还要做可达性分析等等,但是他们大多有存活不了多久。所以多做了很多无用的查找工作。减少重新标记的压力。

  • 工作流程

image.png
先初始标记(很快),然后并发标记,不影响用户线程,不用stw。
然后重新标记(因为并发标记的时候可能会产生新的垃圾),这里需要stw。
然后开始并发清楚,循环往复

  • 降低了吞吐量,因为给了一核去处理垃圾,剩下的才去做工作,少了一核干事
  • 因为是标记清除,所以产生很多碎片,所以退化一下,到时候垃圾回收的时间就会突然增多。
G1

适用场景:

  • 同时注重吞吐量和低延迟
  • 超大的堆内存,划分为多个大小相等的Regin
  • 整体上是标记整理,区域之间用的是复制算法

image.png:打开

回收阶段

image.png

  • 新生代回收(Young Collection)

image.png
触发新生代垃圾回收,会stw
image.png
用复制算法放进幸存区
image.png
是young gc ,有的会进入老年代,年龄不够的会放在幸存区

  • 新生代回收+并发标记(Young Collection + CM)

Young Gc 进行GC Root的初始标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由以下jvm参数决定
image.png
image.png

  • Young Collection 跨代引用
    • 去老年代找根对象效率很低,采用卡表技术
    • o 引用 e 的时候 标记为脏卡
    • e 中有remember set 记录有哪些脏卡
    • 通过remember set 找脏卡,再通过脏卡找gc root
    • image.png
    • image.png
  • 混合收集(Mixed Collection)

上个操作完成之后
队 E、S、O进行一个全面的垃圾回收

  • 进行最终标记:收集并发时没发现的垃圾,会STW
  • 拷贝存活:会STW,并不是将所有的垃圾都回收,因为要保证暂停时间尽可能短,所以会有选择的进行垃圾回收。回收垃圾最多的区域 (这也是为什么叫garbage first)

image.png

GC 辨析

image.png
G1 老年代不足时,到达阈值时
并发标记+混合收集
回收速度如果低于产生垃圾的速度,这时候只有小的暂停
反之退化成串行

remark

在并发标记阶段,处理了c之后,又有人引用了c。
对象引用发生改变时,会加入一个写屏障。
写屏障是把c加入一个队列中并设置为灰色
并发结束之后,进入remark,进行进一步的判断处理。
以此避免被当作垃圾处理掉。
image.png

优化
  • 字符串去重

String底层使用的是char数组
所以将新分配的字符串加入一个队列,新时代回收时,查看是否有字符串重复,如果值一样,引用同一个char[]
优点:节约了内存
缺点:略微占用了cpu内存,新生代回收时间略微增加。
image.png 默认开启

  • 并发标记类卸载

image.png
并发标记之后,就知道哪些类不再被使用,
条件:当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。

  • 巨型对象
    • def:大于region的一半
    • 不进行拷贝
    • 回收时优先考虑。
    • 老年代没人引用时,可以在新生代回收时处理掉。
  • 并发标记时间调整

动态调整阈值,防止退化成full gc

垃圾回收调优

最快的GC就是不发生GC
  • 数据是不是太多
    • select *from
  • 数据表示是否太臃肿
    • 把对象所有的内容都查出来
    • 对象大小 16字节(基本-包装)
  • 是否存在内存泄漏
    • static map,放进去但不移除(static map =new ,new的在堆中,引用在元空间里)
      • 使用软弱引用
      • 第三方缓存实现
新生代gc
  • 在新生代new 的内存分配非常廉价
    • TLAB。线程局部分配缓冲区,每个线程用自己私有的伊甸园内存来完成对象的分配
  • 新生代死亡对象的回收代价是0
  • 大部分对象用过即死
  • Minor GC 的时间远远低于Full GC

调优:将新生代调大,但是越大越好吗?

  • 小了不好,频繁minor gc,stw多了
  • 太大了,老年代就小了,full gc 暂停时间更多
  • 一般在25%~50%
  • 标记+复制,复制占大头,但是新生代要复制的不多,所以还是尽可能大一点比较好
  • 所以:新生代容纳【并发量*请求响应】的数据,因为大部分都会被回收,所以 这样能较少触发mingc
  • 幸存区要大到能保留当前活跃对象和需要晋升对象
    • 晋升阈值大了,有的长期存活的一直在消耗性能(复制)。
    • 所以可以让一些长期存活的早点晋升
老年代gc

在cms 中,老年代越大越好,full gc 之后才对老年代调优,否则先尝试调优新生代。
如果fullgc老年代占用内存,可以将内存预设调大1/4~1/3
image.png老年代空间达到多少比例时,就触发fullgc

案例
案例1 Full GC 和Minor GC 频繁

原因,来了大量对象,有的对象没活多久也被分配到了老年代

  • 解决方案
    • 提高新生代大小和晋升阈值
案例2 请求高峰期发生Full GC,单次暂停时间特别长(CMS)

一般是重新标记阶段,需要扫描新生代和老年代。

  • 解决方案
    • 可以在重新标记之前先对新生代对象进行一次垃圾清理
案例3 老年代充裕情况下,发生Full Gc(cms jdk 1.7)

永久代溢出

类加载与字节码技术

1 类文件结构

image.png

  1. 魔数

0-3字节 ,共四个字节,标识时什么类型的

  1. 版本

4-7字节,标识java的版本

  1. 常量池

8-9字节表示常量池长度

image.png

这部分逐渐放弃。。。
image.png
image.png

2 字节码指令

javap -v xxx.class

字节码执行流程

main方法类加载,放在方法区,常量池(class数据)放入运行时常量池
小的数字放在放的方法的字节码指令,超过范围放在常量池

  • x=x++为什么没用

因为先把x放到操作数栈,然后x++,最后又将操作数栈的值给x,这时候还是0。
i++是先load再自增,++i是先自增再load

  • cinit

编译器按按从上至下的顺序, 将所有的静态代码块和静态成员赋值的代码,合并成一个特殊的方法,()V

  • init

实例对象。
init方法,也是按顺序,构造方法最后执行

  • 方法调用

构造方法:invokespecial
私有:invokespecial
final:invokespecial
public:invokevirtual(因为有可能重写)
接口方法:invokeInterface(支持多态)
静态方法:invokestatic
new 方法:堆中给对象分配内存—分配成功之后把对象的引用放入操作数栈—将栈顶的元素复制一份—invokespecial执行栈顶元素的构造方法并移除—把剩下的一个出栈存到局部变量表中
不要通过对象调用静态方法、变量,会产生两条没用的虚拟机指令
image.png

  • 多态的原理

虚方法表在链接阶段就会生成,就生成了虚方法表的入口地址

  1. 通过栈桢中国的对象引用找到对象、
  2. 分析对象头,找到对象的实际classsss
  3. class结构中有vtable,在类加载阶段已经根据方法重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码
  • 异常的处理

有一个Exception table,监测try代码,出现异常之后进行类型匹配,看是否一致或者是子类异常。
多个catch:异常表里面有多个 。因为只会有一个异常,所以局部变量表只占一个槽位(节省了栈桢的内存)
multi-catch:出现了多个异常。xxx E | xxxE | xxxE:三个不同类型的异常入口是一样的。

  • finally

将finally的代码既放到了try块又放到了catch块
除了try和catch中无法捕获的,都会跳到最后,类型是any,去执行,当然这里面也复制了finally的东西

image.png
20。
try里面放了finally的代码,最终肯定要执行。
所以千万不要在finally中写return,因为可能会异常被吞

image.png
10
没有return语句,会有一个slot来暂存,而且也会throw异常

  • synchronized

如何成对加锁解锁?
先a_load把lock对象加载到操作数栈,表示synchronized开始
然后把引用复制一份放进来存入槽位,给monitorenter和monitorexit来用 (两个会消耗lock引用)
monitorenter 开始lock引用
加载之前的lock,执行monitorexit解锁,异常也一样,都会加载lock对象的引用,进行解锁。

3 编译期处理(语法糖)

编译期间的优化和处理:语法糖 (编译过程自动生成和转换一些代码)

  1. 默认构造器

会生成默认的构造器(没有自己写的情况下),因为所有的都继承自Object。

  1. 自动拆装箱

int->Integer:装箱
Integer->int:拆箱

  1. 泛型集合取值

范型擦除: 编译之后统一设置成Object
返回的时候返回的是Object:用checkcast,将object变成Integer(相当于自动做了一次强制类型转换)
方法体内的信息被擦除了, 局部变量的范型没有被擦除,但是不能通过反射获得,只有在方法的参数和返回值上能通过反射获得

  1. 可变参数

String …args -> String[] args
如果不传参数,传的是一个空的String数组,而不是null值

  1. foreach

数组赋值可以直接int arr[] = {},会变成 int arr[] = new int[]{}
数组foreach 其实就是变成fori
list的foreach是使用的迭代器(实现了Iterator的都可以用)

  1. switch 字符串

实际上比较的hash code

  • 为什么既比较hashcode,又利用equals比较呢?

hashcode是为了提高效率,减少比较,equals是为了比较hash冲突,
因为大多的hashcode都是唯一的。

  • 为什么要重写hashcode和equals?
    • 用于只需要比较一个值的情况,比如不同的对象,id一样就可以认为是同一个

equals相等,hashcode必定相等,hashcode用于快速比较,equals用于绝对相等。hashcode存在冲突情况,所以要进一步用equals比较

  • 为什么重写equals必须重新给i额hashcode
    • 如果两个对象相等,则 hashcode ⼀定也是相同的
    • 两个对象相等,对两个 equals() ⽅法返回 true
    • 两个对象有相同的 hashcode 值,它们也不⼀定是相等的
    • 综上, equals() ⽅法被覆盖过,则 hashCode() ⽅法也必须被覆盖这部分借鉴于:https://juejin.cn/post/7025465769307668516,
  1. switch 枚举

ordinal()

  1. 枚举

本质是一个class,值实际上都是实例对象
是一个final的不能被继承 ,继承了枚举父类(支持范型)
image.png
image.png
构造方法私有
$values把枚举对象放入数组

  1. try-with-resources

对于需要关闭的对象,可以放到try里面,会帮我们关闭。实际上是编译之后给了我们一个finally块关闭
try( = ){
}
catch(){
}
如果try块出现了异常,则捉住异常,两个异常(压制异常)(try块和关闭异常)都不会丢。
被压制的异常就是try()里面的

  1. 方法重写的桥接方法
  • 父子类返回值完全一致
  • 子类的返回值可以是父类返回值的子类型
  1. 匿名内部类

编译器生成了一个额外的类

  • 当引用了final修饰的局部变量
    • 匿名内部类里面有属性,用构造方法接受这个值
  • 为什么要final
    • 因为是通过构造方法传递的,所以传递的时候只有这一个入口接受,所以必须设置为不可改变的final,不然值变化了,匿名内部类的属性值无法改变。

4 类加载阶段

加载

将类的字节码载入方法区,内部采用c++的instanceKlass类(Java不能直接访问),field有:
image.png
加载的过程中,会在堆中生成一个xxx.class的镜像(类对象)
xxx.class和持有instanceKlass的地址,_java_mirror持有xxx.class的地址。
要获取class信息,访问对象头来获得类对象(镜像),通过类对象到元空间获得instanceKlass
image.png
镜像起到的是桥梁作用,存在堆中
instanceKlass这样的元数据是存储在方法区(元空间)

链接

  • 验证

验证类是否符合JVM规范,安全性检查(比如被修改了魔数就不好了)。

  • 准备

为静态变量分配空间,设置默认值。

  • 1.7之后,静态变量存在堆中,类镜像的末尾
  • 分配空间在准备阶段完成,赋值在初始化阶段完成(final针对基本类型和字符串常量在初始化阶段就完成了,但是引用类型,就是需要new 的不是,会在初始化阶段)
  • 解析

将常量池中的符号引用解析为直接引用。能确切知道类和属性在内存中的位置

初始化

()V 方法
初始化调用()V 方法,虚拟机会保证这个类的构造方法线程安全。
类的初始化是懒惰的
初始化:

  • main方法所在的类,总会被首先初始化。
  • 首次访问这个类的静态变量或者静态方法时会初始化(注意和下面的的对比,final没事)
  • 子类初始化,如果父类还没初始化,会先初始化父类
  • 子类如果访问父类的静态变量,只会触发父类的初始化。
  • Class.forName()会初始化
  • new会初始化

不会初始化的情况

  • 访问类的static final静态常量(基本类型和字符串)不会初始化
  • 类对象.class不会触发初始化
  • 创建该类的数组不会初始化
  • 类加载的loadClass方法不会初始化
  • Class.forName(),的参数2 是false 不会初始化

静态内部类实现懒加载能实现线程安全

5 类加载器

image.png

  • 双亲委派机制:

当我想要加在这个类的时候,要问问我的上级同不同意。
优先级最高的是启动类加载器,其次是扩展类加载器,最后是应用程序加载器

启动类加载器

getClassloader值为null:启动类加载器

扩展类加载器

放在了jar包中和自己写了一个一样(路径同名)的,jar包中的是扩展类

双亲委派机制

(并没有继承关系,只是叫这么名字)
源码解读:

  1. 检查是否已加载
  2. 没有的话,向上级loadclass
  3. 没有上级了,委派bootstrapClassloader
  4. 找不到自己来

线程上下文类加载器

例如jdbc,启动类加载器事lib下的,肯定没有mysql的驱动,所以必须要用应用程序类加载器来加载
image.png
spi:写进去特定的类的路径,可以实现自动加载
image.png
直接用上下文类加载器获取的应用程序加载器,破坏了双亲委派机制,没有用启动类加载器去获得。

自定义类加载器

  • 为什么需要自定义类加载器?
  1. 想加载非classpath任意路径中的类文件
  2. 框架设计通过接口解耦
  3. 隔离,不同应用的相同类名可以加载:tomcat
  • 实现

如何一样?包名类名相同且类加载器一样

  • 命名空间????????

运行期优化

JIT:just in time。即使编译器
解释器遇到相同的字节码会执行重复的解释。平台无关性
JIT会将一些字节码编译成机器码,存入code Cache。特定平台
执行层次
C2>C1>解释器

  • 逃逸分析???

对象在方法里定义后,可能被其他方法甚至线程访问。
就会分析逃逸分析,可能对象都不会创建了

方法内联

热点方法并且长度不太长,会进行内联,即将方法内代码拷贝粘贴到调用者位置

字段优化

尽量使用局部变量,少使用成员变量和静态成员变量。JVM会帮助优化

反射优化

运行15(膨胀阈值)以上,从反射方法调用变成正常方法调用
可以不要膨胀(不使用本地方法,直接使用生成的),没必要,因为首次生成很耗时。

内存模型(JMM)

1 java内存模型

JMM:定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。

原子性问题

用synchronized(对象){
} 解决
每个对象都有一个monitor

synchronized支持锁冲入,
但是还是推荐在两个for的时候,锁加载外面,避免多次竞争

2 可见性

  • 一个线程中flag= true,while(flag),在主线程中设置为false,停不下来??

因为jvm的优化,flag会从高速缓存取值(线程中的值),并取不到主存中已经改变的值。
解决方案:用volatile修饰,必须从主存中取数据。
但是volatile并不能保证原子性,一般在一个人写,多个人读的情况。synchronized可以保证可见性和原子性,但是是重量级锁,效率不高。

  • 一个线程中flag= true,while(flag),在主线程中设置为false,里面加了个sout就能停下来为啥?

sout里面有synchronized关键字

3 有序性

jvm优化会出现指令重排,所以可以加上volatile 。
dcl就是经典的指令重排

  • 为啥不创建一个大锁?

效率太低了

  • 为什么要两层不为空?

都没创建时可能会被synchronized卡住。进去的时候已经创建好了,但实际上并没有创建好。

  • 为什么要加volatile?

这个instance在外部也会被用到,可能发生指令重排(有一种可能是,创建的对象是个半成品,初始化了一半),volatile会防止指令重排

happens-before

即规定了哪些写操作对读操作可见。
对于成员变量

  1. 线程对volatile的写,对接下来其他线程对其他变量的读可见。

写了volatile修饰的变量,下面可以读

  1. 解锁m之前对变量的写,对接下来对m加锁的其他线程的对该变量的读可见。

锁住了写,解锁之后其他加该锁的地方可以读

  1. start前对变量的写,对线程开始后对该变量的读可见。

start开始前的赋值,线程可以读到

  1. 线程结束之后对变量的写,对其他线程得知它结束后的读可见。

join一个线程,得到线程中赋值得到的值

  1. t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见。(通过t.isInterrupted)

打断前赋的值也是可以获取到的。

  1. 默认是的写,对其他线程对该变量的读可见
  2. 具有传递性。

CAS 与 原子类

def:Compare and Swap ,是一种乐观锁的思想。适用于竞争不激烈(不然一直改),多核CPU情况下(不然没cpu时间去重试 )。
为了保证得到的值是最新的,cas一定要和volatile一起使用。
思想:把旧值和当前的值进行比较,没发生变化,就可以赋值。
相比synchronized,cas是不会阻塞的,所以效率更高

cas底层实现

用unsafe类来调用操作系统底层的cas指令
unsafe进行cas比较的时候,用this对象+偏移量进行比较
用偏移量定位数据在内存中的位置
data 一定要用volatile 修饰,防止数据从高速缓存中读取。

乐观锁与悲观锁

乐观锁:cas
悲观锁:synchronized

原子操作类

AtomicInteger等,底层用的就是cas+volatile

synchronized优化

每个对象都有对象头:包括class指针和mark word。mark word平时存放对象的hashcode、分代年龄,加锁时被替换为标记位,线程锁记录指针,重量级指针,线程ID。

轻量级锁

一开始是轻量级锁,后来会锁膨胀。
在加锁的时候,如果发现已经加入了自己的锁,就会锁重入,否则既要进行锁升级。

锁膨胀

cas(修改mark)无法成功,说明有竞争,会发生锁膨胀,轻量级锁变重量级锁

重量级锁优化

自旋:先不停,重试一下。
自旋多次失败的话会陷入阻塞。
自旋是自适应的:经常成功就多试几次,否则就少甚至不自旋。
自旋会占用cpu时间,,单核就是浪费,多核才能发挥优势。
阻塞的话,保存信息,恢复信息相对时间较长。

偏向锁

直接将线程id存在头里,直接判断头里是不是我自己,不用每次都cas。
但是

  • 撤销是重量级的还是STW
  • 访问hashcode也要撤销锁(头里放的是id,没有hashcode)
  • 可以重偏向
  • 撤销偏向到达某个阈值,整个类都会变成不可偏向的

其他优化

  1. 减少上锁时间

synchronized里面的代码块尽量短一点。

  1. 减少锁的力度

比如hashmap可以只锁住表头,而不是锁住整个表;
image.png
linkedBlockingQueue:入队出队用不同的锁

  1. 锁粗化

同一个对象多次操作(比如链式编程),会把多个锁变程同一个
image.png

  1. 锁消除

逃逸分析(加锁对象是局部变量,不会被其他线程访问),JIT会把锁忽略掉。

  1. 读写分离

image.png
复制一块来写,读不要同步,只有写要同步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值