对新生代、老年代、永久代概念的解释:
有的概念中会说堆内存中分为新生代和老年代,方法区有有永久代。
方法区、堆内存是Java虚拟机规范中的概念。
而新生代和老年代是Hotspot实现垃圾回收(GC)算法时提出的概念。
永久代也是方法区实现中提出的一些概念,JDK1.7实现方法区用的是永久代,JDK1.8用的是元数据区。
怎么判断对象可以被释放?
1、用计数器算法:在对象中保存一个计数器记录引用数,但存在两个对象相互引用的问题。(所以Java和C#里都不是通过引用计数器实现的)
2、可达性分析算法:主流的商用程序语言(Java、C#)都是通过可达性分析算法来判定对象是否存活的。
如下图,判断对象通过引用是否可达Gc roots,如果不可达则回收,可达则保留。
GC Roots可以是:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象:例如方法中的局部变量所引用的对象。
- 方法区中类静态属性引用的对象:例如类的静态变量所引用的对象。
- 方法区中常量引用的对象:例如字符串常量池中的字符串对象。
- 本地方法栈中 JNI(Java Native Interface)引用的对象:例如本地方法中引用的 Java 对象。
回到正题,线程共享部分就差不多了,继续说说线程独占部分。
继续思考:一个类被实例化或者被调用,是通过线程执行的,因此Java虚拟机就需要为其分配一块区域供其执行,并且保证其不被打扰(即线程独占区域)。
线程独占区域里具体有啥?
先贴一个图熟悉熟悉:
下面慢慢对其进行讲解。
一个实例化的对象,要想发光发热,那它得有方法或者字段给人调用才行。
方法的调用,在虚拟机中是怎样子的? 想要了解就要去认识虚拟机栈
JVM中每个线程独占区域都有一个私有的Java虚拟机堆栈,与线程同时创建。
虚拟机栈中存储的是栈帧。
每个方法执行时,就会在虚拟机栈中创建一个栈帧,每个方法从调用到执行的过程,就对应着栈帧在虚拟机栈中从入栈到出栈的过程。
虚拟机栈可以理解为是线程中方法执行的模型。
比如:主线程中的main方法、线程new Thread执行时的run方法,Java程序执行,就是线程在执行一个个方法。所以也可以理解为虚拟机栈就是一个线程执行的模型。
扩展
如上也能发现,通过递归方式循环调用方法存在弊端,会让虚拟机栈消耗过多资源。
所以尽量不用递归,递归的逻辑一般都可以通过while实现。
因为方法肯定是在一个线程中执行,虚拟机栈在线程中有独占区域,由每个线程都独立分配,因此也能明白为什么说方法中的局部变量是线程安全的。
继续深入,来了解下栈帧。
栈帧是从虚拟机栈中分配的。
每次调用一个方法都会创建一个栈帧。
无论方法完成是正常的还是突然的(它抛出一个未捕获的异常),当它的方法调用完成时,一个栈帧将被销毁。
栈帧中包含了如下结构:
局部变量表(Local Variable Table)【官方文档】
局部变量先是存在方法区中的一个字节码指令,方法使用时,方法栈帧加入虚拟机栈中执行才给局部变量赋值(八大基本类型的默认初值是在方法区中),先把初值放入操作数栈,然后赋值给局部变量表中对应变量。
在编译程序代码的时候就可以确定栈帧中需要多大的局部变量表,具体大小可在编译后的 Class 文件中看到。
局部变量表的容量以 Variable Slot(变量槽)为最小单位,每个变量槽都可以存储 32 位长度的内存空间。
在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用(在方法中可以通过关键字 this 来访问到这个隐含的参数)。
其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot。
基本类型数据以及引用和 returnAddress(返回地址)占用一个变量槽,long 和 double 需要两个。
用于字节码指令的执行时对数据的操作。
后进先出。
操作数栈的最大深度同样也是在编译期确定大小。
栈帧被创建时,操作栈是空的。操作栈的每个项可以存放 JVM 的各种类型数据,其中 long 和 double 类型(64位数据)占用两个栈深。
方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作(与 Java 栈中栈帧操作类似)。
操作栈调用其它有返回结果的方法时,会把结果 push 到栈上(通过操作数栈来进行参数传递)。
动态链接(Dynamic Linking)
方法区中存了类的信息,类的信息里当然包含有方法的信息(方法的二进制字节码指令),动态链接就是可以帮助找到此方法的二进制字节码指令。执行方法其实在虚拟机中就是执行这些二进制字节码指令。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在运行时转化为直接引用,这部分称为动态链接。
返回地址(Return Address)
方法开始执行后,只有 2 种方式可以退出 :方法返回指令,异常退出。
方法正常执行完后出栈,返回地址告诉当前方法,出栈后返回到哪里去(上一个方法里)。
方法如果抛出异常并且没有捕获到该异常,则会导致方法调用的突然完成,突然完成的方法调用永远不会向调用者返回值。
其他部分:省略。
扩展
到此恍然,以前刚学Java的时候老师所说的堆栈概念中的栈,指的只是局部变量表。
继续正题,虚拟机栈是线程方法的执行模型,但在虚拟机线程中不是只有一个虚拟机栈就够了,因此再来看看:本地方法栈和程序计数器。
和虚拟机栈功能类似,虚拟机栈是为虚拟机执行Java方法而准备的,本地方法栈是为虚拟机使用Native本地方法而准备的。
《Java虚拟机规范》中对Native方法使用的语言、使用方式与数据结构没有强制规定。
HotSpot虚拟机中虚拟机栈和本地方法栈合二为一。
程序计数器(Program Counter Register)记录当前线程执行字节码的位置,字节码解释器工作时,就是通过改变计数器的值来选取下一条需要执行的字节码指令。
如果执行Java方法,存储的是字节码指令地址,如果执行native 方法,则计数值为空。
程序计数器是唯一一个在《JVM规范》中没有任何OOM的区域。
程序计数器用来记录方法中的字节码指令执行到哪个位置了。
程序计数器记录的是字节码指令的内存地址。
为什么要记录?
因为多线程中CPU会切换线程。
直接内存不是JVM运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域,是我们常说的堆外内存。
JDK1.4引用NIO,它可以使用Native函数库,直接分配堆外内存,然后通过一个存在Java堆中的DirectByteBuffer对象作为操作这块内存区域的引用进行操作。
直接内存不受Java堆大小限制,当内存总和大于及其物理内存时,会OOM。
扩展:Netty高性能的一个原因就是使用直接缓存区,直接操作堆外内存,避免在每次调用本地I/O操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。
这块主要对JIT做些入门级的讲解(因为我的了解也只是入门级),此文中很多都是对 《深入理解Java虚拟机》 一书的引用。
说JIT之前,要先说一个概念:热点代码。
Java程序最初是通过解释器进行解析执行,当虚拟机发现某个方法或代码块的运行特别频繁时,就把这些代码认定为热点代码。
为了提高热点代码的执行效率,一些虚拟机就会把热点代码编译成与本地平台相关的机器码,并进行优化,完成这些操作的就是即时编译器(Just In Time Compiler)。
《Java虚拟机规范》中并没有对即时编译进行约束。所以JIT的具体逻辑就由各虚拟机自行实现,在此以HotSpot虚拟机为例来说。
运行过程中会被即时编译器编译的“热点代码”有两类:
1、被多次调用的方法。
2、被多次执行的循环体。
从这里能产生2个疑问:
1、虚拟机如何知道这个代码被执行了多次?
2、多次到底是几次?
下文就来慢慢回答这2个问题:
要识别热点代码,就需要用到热点探测(Hot Spot Detection)机制,主要的热点探测有2种:
1、基于采样的热点探测:虚拟机会周期性检查各个线程栈顶,如果发现某个方法经常出现在栈顶,就认为这个方法是热点方法。
2、基于计数器的热点探测:建立计数器,统计方法或者代码块执行次数,超过设定的阈值,就认为是热点方法。
在HotSpot虚拟机中采用的是计数器热点探测,其为每个方法准备了2个计数器:方法调用计数器、回边计数器。
方法调用计数器:统计方法调用次数,它默认值在Client模式下是1500次,在Server模式下是10000次。这个阈值可以通过虚拟机参数 -XX:ComplieThreshold来设置。
方法调用计数器统计的是一段时间之内的方法调用次数,如果一段时间内没有超过阈值,那么这个方法的调用计数器的数值就会减半,这个过程称为热度衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期。
可以使用 -XX:-UseCounterDecay参数来关闭热度衰减,关闭后计数器统计的就是方法调用的绝对次数,这样系统运行时间长后,大部分方法都会被编译成本地代码。
可以使用 -XX:CounterHalfLifeTime参数设置半衰周期,单位是秒。
回边计数器:统计一个方法中循环体代码执行的次数(空循环不算回边,不会被统计),触发OSR编译。
栈上替换(On Stack Replacement,OSR):即方法栈帧还在栈上,方法就被替换了。
回边:在字节码中遇到控制流向后跳转的指令,称为回边(Back Edge)
回边计数器没有热度衰减的过程,当回边计数器溢出时,还会把方法计数器的值也调整到溢出状态。
3.1、扩展:Client编译和Server编译
HotSpot中内置了两个JIT编译器,即Client Compiler(C1)和Server Compiler(C2),默认采用的是解释器和C1或者C2中一个编辑器配合的方式进行工作。
HotSpot在启动的时候会根据自身版本以及宿主机器的硬件性能自动选择运行模式,比如会检测宿主机器是否为服务器、比如J2SE会检测主机是否有至少2个CPU和至少2GB的内存。
以Server模式运行,更注重编译的质量,启动速度慢,但是运行效率高,适合用在服务器环境下,针对生产环境进行了优化。
以Client模式运行,更注重编译的速度,启动速度快,更适合用在客户端的版本下,针对GUI进行了优化。
用户可以通过java -client或-server去强制指定虚拟机运行模式。
学过Java线程安全的应该都对synchronized关键字比较熟悉,其就是用于对Java代码进行加锁,这里不对其进行过多说明了。
synchronized使用很简单,但是这个关键字具体是怎么实现的呢?JVM怎么知道代码上面加了锁?
JVM在堆内存中为一个对象分配一个空间后,应该会用一种方式把这个对象和方法区中存储的类信息(元信息)关联起来,怎么关联呢?
就是在为对象分配的空间中,除了存储字段信息外,还有一个对象头,对象头中就存储了有方法区中元信息的引用,当然除了元信息引用外还有其他关键信息,如下图所示:
Class Meta Address就是方法区中类的元信息引用。
Array Length:如果对象是数组这里会存储数组对象的数组长度。
Mark Word:这里就是重点,继续往下看。
Mark Word
先解释一下,Mark Word是一块内存区域,里面存的值是上面表格红框部分的某一行。
具体存的哪一行根据右边的State决定。
Mark Word这块内存区域多大?根据JDK是32位还是64位,32位内存区域就32位大小,64就64位大小。
Bitfields(bit域)、tag(状态位)、state(状态)、Hashcode(当前对象的hashcode)、age(垃圾分代回收中的age)
默认情况下JVM锁会经历:未锁定->偏向锁-> 轻量级锁-> 重量级锁这四个状态
流程分析:
对象创建一开始先是未锁定。
当多个线程要抢这个对象的锁,会用cas操作把tag中的01改成00,让状态变成轻量级锁。
如果某个线程将状态位改成了00,然后就会将00前面的位数改成这个线程引用(Lock record address,这里的地址就是当前线程的地址)
其他没抢到的线程会进行自旋,自旋会有一定的次数阈值,达到一定数量会升级为重量锁。
怎么升级为重量级锁?
先要介绍一个东西Monitor(监视器),monitor也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的monitor。
JVM把状态位由00改为10.
然后把Lock record address改成monitor address。
那么谁获得了锁记录在哪里?记录在Monitor中有个onwer字段,会标记谁获取了锁。
升级为重量级锁是为了解决其他线程不再自旋消耗资源的问题,那么具体怎么解决的?
Monitor中还有一个EntryList(锁池)字段,相当于一个等待队列。
JVM先会把等待线程的引用存入这个队列中。
然后JVM让等待线程挂起。线程就会处于Blocked状态
然后比如持有锁的线程1,在代码块中调用了wait方法,那么会发生什么情况?
线程1会挂起线程。
onwer会释放掉这个线程的引用,onwer=null
然后这个线程的引用会被放入Monitor的另一个字段waitSet(等待池)中。
其他所有等待线程去抢锁。
假设线程2抢到了锁,如果线程2中执行了notify方法,那么会把waitSet中的一个等待线程拿出来,抢一次锁(onwer),没抢到放入Entrylist中。(notifyAll也一样,只是释放了waitSet里所有线程)
然后线程2的代码块执行完后,执行解锁操作,从monitor中退出(exit monitor)。
那么onwer又变成了null。
轻量级锁和重量级锁都是多线程的情况下。
还有一种情况,虽然加了synchronized关键字,但是程序是在单线程中执行。
在JDK6 以后,默认已经开启了偏向锁这个优化,通过JVM 参数 -XX:-UseBiasedLocking 来禁用偏向锁若偏向锁开启。
如上图就是偏向锁的Mark Word存储的字段。
如果只有一个线程抢锁,可获取到偏向锁。
如果生成偏向锁后又有线程来抢锁,那么会把锁升级为轻量级锁。
一旦锁升级,偏向锁就会关闭,不会回退为偏向锁。
如果重量级锁中的线程都释放了,那么会退回到未锁定的状态。
偏向标记第一次有用,出现过争用后就没用了。 -XX:-UseBiasedLocking 禁用使用偏置锁定,
偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步(jvm为了少干活:同步在JVM底层是有很多操作来实现的,如果是没有争抢,就不需要去做同步操作)
全部评论