Java Virtual Machine - Java二进制字节码文件的运行环境
JVM是一种规范,可以更具这样的规范开发属于自己的JVM。
Java程序执行原理:
Java源代码 --> 二进制字节码文件(其中包含JVM指令)
解释器将JVM指令翻译为对应机器的机器码,最后交由CPU执行。
好处:
Program Counter Register 程序计数(寄存)器
作用
物理上通过寄存器来实现,记住下一条JVM指令的执行地址。
指令执行时,会将下一条指令的地址放到程序计数器中,解释器访问程序计数器得到下一条指令并进行解释与执行。
特点
线程运行时内存空间。每个线程有每个线程的虚拟机栈。
栈中的数据为栈帧(Frame),一个栈帧实际上就是每个方法运行时需要的内存。
调用方法时,会分配栈帧存储方法各种内容,然后压入虚拟机栈中。
每个线程只能有一个活动栈帧,对应正在执行的方法
Error:StackOverFlow
Native Method Stack
本地方法就是指引擎通过其他语言实现的方法。
JVM可以通过调用本地方法接口调用一些本地方法,由本地方法栈承载调用的本地方法。
通过new关键字创建的对象都会使用堆内存。
Error:OutOfMemoryError:Java heap space
工具:
实际案例:当执行多次垃圾回收之后,内存占用依然很高
Method Area
所有JVM线程共享,存储和类的结构相关的各种信息。在JVM启动时启动,逻辑上是堆的一部分,但具体实现不同,定义位置也不同。
方法区一般包含常量池、类和类加载器等。
Oracle Hotspot方法区实现说明:
JDK1.6使用PermGen永久代实现,是堆的一部分,由JVM进行管理
JDK1.8使用Metaspace元空间实现,是本地内存的一部分,由元空间自己管理
可能出现在spring,mybatis等框架的使用中。
这些框架使用cglib自动生成一些代理类,mapper类等。可能会使方法区内存溢出。
常量池:在.class文件中,是一张常量表,虚拟机指令查找要执行的各种信息:类名,方法名等
运行时常量池:当一个类被加载时,它的常量池信息就被放入运行时常量池,并会把其中的符号地址变为真实地址
底层采用类似哈希表的形式实现,由于是避免重复的,因此能够大量减少堆内存的占用,适用于大量字符串操作时。
JDK1.6是放在方法区中的,即PermGen永久代。
JDK1.8更改为放在堆内存中。
几个特性:
直接赋值的字符串常量是存在于StringTable中的,且是唯一存在的。
通过字符串对象拼接操作生成的字符串,JVM会使用StringBuilder对象进行处理,这是相当于new了一个新的String对象,是存在于堆内存中的,不会放入串池中。
编译期间可能会发生变化,所以不能作为字符串常量进行处理
通过字符串常量拼接操作生成的字符串,JVM会直接加载StringTable中存在的串,存在于StringTable中。
这种实际上是javac在编译期间的优化,编译期间已经确定
可以通过intern方法,主动放入某个字符串对象。不管成功与否,都会返回串池中的对象。
StringTable调优:调大Map桶的个数,减少碰撞次数
不属于JVM内存管理,而是系统内存管理的一部分。
常用语NIO操作时,用于数据缓冲区,分配回收成本较高,读写性能好。
JDK内部使用Unsafe类对直接内存进行操作,我们可以使用ByteBuffer可以分配直接内存。
引用计数法:判断一个对象被其他对象引用的次数,如果引用数为0,则判断可以回收。
无法解决循环引用的问题,造成内存泄漏
可达性分析方法:
GC Root根对象:一些一定不被回收的对象
System Class Native Stack Busy Monitor(Lock加锁的对象) Thread
判断方法:扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收。
软、弱引用可以和相应的引用队列配合使用,因为软弱引用本身就是一种对象,会占用一定内存空间,如果要回收这两种对象,就需要引用队列辅助进行。
虚引用
终结器引用(对象finalize方法):需要两次GC
虚引用和终结器引用必须和引用队列配合使用。
标记清除
标记整理
复制算法
对不同需求的对象分别存放到新生代和老年代中,分别选取不同的回收算法,回收周期进行处理,尽量达到最优性能。
垃圾回收过程会触发stop the world,暂停其他所有用户线程
回收较频繁。
伊甸园eden:新对象默认放置位置
当满之后,执行Minor GC,执行复制算法,到To区,并使存活年龄加1。
幸存区:From&To
存放伊甸园中一次复制算法垃圾回收幸存的对象,记录存活年龄。
存放长时间生存的对象,执行频率低。
新生代对象寿命超过一定阈值(最大为15,占4bit)时,将晋升到老年代。
老年代空间不足时会触发Full GC,标记整理,时间更长。
对于大对象,如果超过eden的大小,会直接放到老年代中。
语法糖:java编译器在编译过程中,自动生成和转换一些代码,减轻程序员负担。
默认构造器:默认生成类的无参构造器
自动拆装箱:基本类型及包装类之间的相互转化
泛型集合取值:
泛型擦除:编译期间会对泛型类型进行忽略,以Object进行操作
读取时会做一个强制类型转换。
可变参数
for…each
数组赋初始值的简化写法
switch用于字符串和枚举类型:转化成两个switch,一个用hashcode和equals找结果,一个是最后的switch
Java Memory Model JMM
JMM定义了一套多线程读写共享数据时,对数据可见性,有序性和原子性的规则和保障。
synchronized关键字,通过monitor管理。
既能保证原子性,也能保证可见性,但是属于重量级操作,性能相对更低。
volatile关键字:避免线程从自己的高速缓存中读取变量。
适用于一个写线程,多个读线程的情况。
JIT指令重排带来的问题。
volatile关键字修饰,能够避免指令重排。
compareAndSwap,是一种乐观锁的思想。
通过对比读到的旧共享变量值和当前的共享变量值,判断是否进行下一步操作或重试。
需要和volatile结合使用,可以实现无锁并发,适用于竞争不激烈,多核CPU的场景下。
底层通过Unsafe类直接调用操作系统底层的CAS指令。
CAS和synchronized
CAS为最乐观的估计,以重试来解决。
synchronized是最悲观的估计,排他性很强。
假设,如果一个对象由多线程访问,但是访问交错进行,那么可以用轻量级锁进行优化。
当是同一线程的不同代码块反复进入时,可以不用反复加锁,可重入。
但如果不同线程进入时,会进行锁升级,将轻量级锁设置为重量级锁,进入阻塞状态。
自旋优化:在竞争时,不直接进入阻塞状态,而是通过自旋反复访问,从而减少因阻塞造成的资源浪费。
自旋超过一定次数,会进入阻塞。
偏向锁会在第一次,用CAS将线程ID设置到对象的头,之后如果发现是自己,就不用CAS重新检查。