浅读 《深入理解Java虚拟机(第二版)》 理解

一、内存区域与管理

1、老生常谈虚拟机有哪些区域:程序计数器、虚拟机栈、本地方法栈、堆、方法区

线程独享,生命周期随线程:程序计数器、虚拟机栈、本地方法栈

线程共享:堆、方法区

方法区,对于这块其实在java程序员比较常见的虚拟机HotSpot中,被习惯称为“永久代”,因为HotSpot团队使用了永久代来实现了方法区。但对于其他虚拟机(例如:IBM J9等)不存在永久代的概念

2、oom与stackoverflow

oom:无法申请到足够的内存资源,则会oom
stackoverflow:栈深度超过虚拟机栈允许的最大深度

一句话,有内存申请就有概率发生oom,但是认为程序计数器所需比较小,一般不会发生oom;stackoverflow栈溢出,只发生在栈区域;

oom发生可能由内存泄露(memory leak)和内存溢出(memory overflow)导致,对于memory leak 我们可以根据GC Roots引用链清晰看到泄露的对象,它们与GC Roots对象相关联,导致无法被回收;对于memory overflow调整堆参数(-Xmx -Xms)
内存泄露(memory leak):与GC相关,对象长时间无法得到回收,导致可以内存空间逐渐减少
内存溢出(memory overflow):与内存分配相关,使用超过系统可用内存的量,导致系统无法满足其内存需求的现象。

至于stackoverflow,根据书中所述和本人测试,确实在单线程中只会出现stackoverflow,多线程中才有概率出现OOM;对于该现象,我认为单线程中只需要建一个栈,当我们进行死循环或者递归时,栈扩展所需的空间微乎其微,但是栈深度不断扩大,导致栈深度超过虚拟机最大深度而stackoverflow,而非oom;多线程下,需要为每个线程建栈,导致建栈申请空间时无发申请到足够空间而出现oom;

二、对象访问、引用与管理

1、访问对象:句柄访问、直接指针方位;二则都好理解

句柄访问:栈中reference持有对象的堆中句柄地址,句柄地址存有堆中对象实例地址、方法区中对象类信息地址;

直接指针:栈中reference持有堆中对象实例地址,对象实例持有方法区中对象类信息地址;

显而易见地,直接指针访问缺少中间商,速度稍快;而句柄访问,当对象地址发生变化(如:GC发生可能导致),只需要改变句柄中持有的对象地址即可,而直接指针则需要改变栈中reference持有的地址;java 中选择的是直接指针

2、对象引用

强引用:我们编程中最常见,直接赋值=new OBJ();

软引用(SoftReference):关联有用但非必须对象;当OOM前会对该部分对象进行回收

弱引用(WeakReference):关联非必须对象;下次GC一定会被回收

虚引用(PhantomReference):没用,通过引用也无法获得对象实例;唯一作用是GC时会收到系统通知

实际我们编程中基本都是强引用,可能软引用还听过一下如:TimerReference 在hystrix中的应用;其他的基本上没听过,更没用过,毕竟谁没事为了引用一个对象还去设计去继承对应的父类(SoftReference,WeakReference,PhantomReference)

3、对象管理
垃圾回收自己去看吧,都看烂了。。。。。

三、比较常用的JDK内置命令

jps 显示所有进程
jps -v 显示进程和启动参数

jstat -gcutil pid 监视java状况

jinfo -sysprops pid 显示system.getProperties()内容

jmap -heap pid 内存映射情况

jstack pid 显示栈信息,通常和其他命令一起使用,来排查问题

四、类加载机制
1、类的生命周期
类的生命周期:加载、连接、初始化、使用、卸载
链接包含三个过程:验证、准备、解析

加载、验证、准备、初始化、卸载这五个过程必须严格按顺序执行,对于解析,解析可发生在初始化之后,这是为了支持JAVA的动态绑定。

加载:
a、通过全限定名获取类的二进制字节流
b、静态结构转为数据结构(字节流到方法区)
c、生成类的Class对象,作为方法区这个类的访问入口

验证:
验证类的字节流符合当前虚拟机的要求,不包含危害虚拟机的信息
包含:文件格式验证(0xCAFEBABY)、元数据验证、字节码验证、符号引用验证

准备:
为类变量分配内存并设置初始值,(注意:设置初始值不是赋值,static int a = 2,此时a初始化为0)

解析:
符号引用替换为直接引用
包含:类或接口解析、字段解析、类方法解析、接口方法解析

初始化:
就是执行类构造器cinit>() 方法的过程,并对类变量赋值,同时执行static{}块
注意:cinit>() 与实例构造器init>()的区别。cinit>() 是非必须的,对于没有static{}块以及不用类变量赋值的,编译器是不会为类生成cinit>() 方法的,一个类的cinit>()最多执行一次

2、加载机制
先给个结论:双亲委派
双亲委派模型:一个类加载器收到类加载请求,首先自己不会去加载这个类,而是将请求委派给父类加载器去完成,(每个层次的加载器都是如此,)只有当父类加载器无法加载到时,子类加载器才会尝试自己去加载。

类加载器包含:
启动类加载器(BootStrap ClassLoader):负责加载$JAVA_HOME\lib下符合要求的文件,也可通过-Xbootclasspath指定

扩展类加载器(Extension ClassLoader):负责加载$JAVA_HOME\lib\ext下符合要求的文件,也可通过java.ext.dirs指定

应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader实现,一般称为系统加载类,加载ClassPath中的类库,若用户未自定义类加载器,程序中默认使用该加载器

优点:保证了java类型体系的稳定,防止JDK核心类库被篡改
缺点:上层代码无法调用下层接口
例如:1、JNDI无法调用接口提供者(SPI:service provider interface)代码;(SPI厂商提供各自的JNDI服务),通过线程上下文类加载器,设置setContextClassLoaser解决
2、OSGi 模块热部署,动态性
名词解释:
JNDI(Java Naming and Directory Interface)是一个Java平台的API,java标准服务,它提供了一种方式来访问和操作命名和目录服务。这些服务可以是轻量级的,如简单的属性文件,也可以是重量级的,如LDAP(轻量级目录访问协议)服务器或DNS(域名系统)。
OSGi(Open Service Gateway Initiative)是一个面向Java的动态模块化规范。它旨在为Java平台提供模块化层,使得开发者能够创建动态化、模块化的Java系统。

五、java内存模型(JMM:Java Memory Model)
在这里插入图片描述
JAVA所有共享变量都存放在主内存(main memory)之中,每条线程都有自己的工作内存(work memory),工作内存保存了被本线程使用的共享变量的主内存副本,线程对该变量的操作(修改、赋值)都在工作内存中进行,不能直接读写主内存。
可以看出,该操作会导致主内存与工作内存中变量会不一致,如何解决呢?
先给结论:CAS (compare and swap)

JMM定义了8种操作来实现:
lock:作用于主内存,标识变量为当前线程独占状态
unlock:作用于主内存,将变量从锁定状态释放,释放后才能被其他线程再次锁定
read:作用于主内存,将主内存变量读取到工作内存,以便后续load使用
load:作用于工作内存,将read到工作内存的变量值放到工作内存变量副本中
use:作用于工作内存
assign:作用工作内存,赋值
store:作用于工作内存,将工作内存变量传送到主内存中,以便后续write使用
write:作用于主内存,将store操作从工作内存传送的变量值放到主内存变量中

1、volatile型变量
volatile可以说是java虚拟机最轻量级的同步机制,但由于java运算并非原子操作,所以volatile无法保证变量是的线程安全的。
volatile特性
a、可见性:被volatile修饰的变量对所有线程立即可见
b、禁止指令重排序优化,所谓的指令重排序优化是指:CPU会将没有依赖的汇编指令代码进行重新排序执行,以达到优化的目的;(从硬件架构上讲,指令重排是指CPU允许将多条没有依赖的指令不安程序规定的顺序分开发送给各相应电路单元进行处理。)
c、无法保证原子性

举个例子:使用多线程对一个static volatile int a = 0 变量进行累加操作共1000次,其值 <= 1000
查看字节码就一目了然了(部分)
0: getstatic
3:iconst_1
4: iadd
5: putstatic
8: return
虽然 volatile保证了getstatic指令将变量a传输到栈顶是正确的,但是其他线程执行iconst、iadd指令的时候,导致a的值变大了,此时栈顶的值就是过期数据了,所以putstatic时可能将此时较小的数据同步到主内存中。

2、线程
线程实现:使用内核线程实现(KLT:kernel level thread)、使用用户线程实现(UT:user thread)、用户线程加轻量级进程混合实现
程序一般不会直接去使用内核线程,而是采用内核线程的高级接口—轻量级进程(LWP:light weight process)去实现,轻量级进程就是我们通常意义上讲的线程。
在这里插入图片描述
java线程模型是基于操作系统的线程模型而实现的。
java线程状态:新建、可运行、运行、等待、阻塞、终止
在这里插入图片描述
3、java线程安全与锁
java线程安全等级由强到弱:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

synchronized关键字通过编译后会在同步代码块前后形成monitorenter和monitorexit字节码指令,在执行monitorenter指令时,首先尝试获取对象锁,获取到锁后,锁计数器加1,对应的,执行monitorexit指令时,对象锁计数器减1,当计数器为0时,锁就被释放了。如果获取对象锁失败,那么线程就要阻塞等待,直到对象锁被另外线程释放。
JUC包下可重入锁ReentrantLock通过lock(),unlock()配合来实现相似功能。

synchronized与ReentrantLock都属于互斥同步手段,尽管ReentrantLock可以实现公平锁以及绑定多个条件,但二者仍然属于同步阻塞;非阻塞同步的需要依靠硬件指令集来完成,这类指令集:
测试并设置(Test-and-Set)
获取并增加(Fetch-and-Increment)
交换(Swap)
比较并交换(Compare-and-Swap,CAS)
加载链接/条件存储(Load-Linked/Store-Conditional,LL/SC)

4、锁优化
自旋锁
由于java线程模型使用的是操作系统的线程模型,因此线程挂起和恢复都要转入内核态中完成,对性能带来了很大影响。
如果共享数据的锁定状态只会持续很短一段时间,为了这一小段时间将线程挂起和恢复并不值得,所以我们可以让后面请求锁的线程“稍等一下”,但不放弃处理器的执行时间,看看持有线程是否会释放锁,为了让线程等待一会,我们只需让线程忙循环,这就是所谓的自旋锁。为了防止自旋时间过长,浪费资源,我们一般会设置自旋次数。

锁消除
是指虚拟机即时编译器在运行时,对一下代码上要求同步,但被检查到不可能共享数据竞争的锁进行消除。
例如:对局部变量的锁,一般不存在锁竞争的情况,因此虚拟机会对此情况锁进行消除

锁粗化
通常来说,推荐将同步代码块的范围限制在尽量小的范围,这样如果存在锁竞争,那等待锁的线程也能尽快拿到锁。
但是如果一系列操作需要对同一对象反复加锁和解锁,频繁的加解锁操作会导致没必要的性能消耗,因此可以将锁范围扩大(粗化)到整个操作序列的外部,只需加一次锁即可。

轻量级锁
虚拟机的对象头部信息会存储 对象自身的运行时数据,这部分数据被称为“Mark Word”,它是实现轻量级锁和偏向锁的关键。
在这里插入图片描述
在代码进入同步块的时候,如果同步对象没有被锁定(锁标识为“01”),虚拟机首先会在当前线程的栈帧建一个锁记录(Lock Record)空间,存储锁对象 Mark Word 的拷贝,然后,虚拟机会通过CAS操作尝试将对象 Mark Word 更新指向 Lock Record 的指针,如果更新成功则线程拥有了对象的锁。

偏向锁
偏向锁会偏向第一个获取他的线程,如果接下来的操作,没有其他线程获取该锁,则持有偏向锁的线程不需要进行同步操作。偏向锁就是在无竞争的情况下把同步取消掉,连CAS操作也不做了。可以通过 -XX:+UseBiasedcLocking开启,-XX:-UseBiasedLocking关闭。如果程序中大多数锁总是被多个线程访问,偏向锁则是无意义的,有时候关闭偏向锁,还可以提升性能。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值