Queue

channel.queueDeclare(queueName, true, true, true, null);
  • @param queue the name of the queue
  • @param durable true if we are declaring a durable queue (the queue will survive a server restart) 服务器重启
  • @param exclusive true if we are declaring an exclusive queue (restricted to this connection) 一个queue能不能有多个消费者
  • @param autoDelete true if we are declaring an autodelete queue (server will delete it when no longer in use) 如果一个queue的全部消费者都挂了
  • @param arguments other properties (construction arguments) for the queue
  • @return a declaration-confirm method to indicate the queue was successfully declared
2016/3/15 posted in  RabbitMQ

类文件结构

Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,每个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割若干个8位字节进行存储

Class文件格式只有两种数据类型:无符号数和表。

无符号数
  • u1u2u4u8来分别表示1个字节、2个字节、4个字节和8个字节的无符号数
  • 无符号数可以用来描述数字、索引引用、数量值,或者按照utf-8编码构成字符串值
  • 由多个无符号数或其他表作为数据项构成的复合数据类型
  • 所有表都习惯性地以_info结尾
  • 表用于描述有层次关系的复合结构的数据
  • 整个Class文件本质上就是一张表

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容器计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的集合。

魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。

Class文件的魔数为:0xCAFEBABECafe Babe

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version

根据表6-1:magicu4类型,因此是从offset0-offset3

常量池

紧接着主次版本号之后的是常量池入口,常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一个u2类型的数据,代表常量池容量计数值(constant_pool_count)。这个容量计数是从1而不是0开始的。

常量池容量为十六进制的0x0016,即十进制的22,这就代表常量池中有21项常量

Class文件结构中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始。

常量池之中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name
  • 字段的名称和描述符(Descriptor
  • 方法的名称和描述符

Java代码在进行javac编译的时候,是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。

常量池中的每一项常量都是一个表,共有11种结构各不相同的表结构数据,这11种表都有一个共同的特点,就是表开始的第一位是u1类型的标志位,代表当前这个常量属于哪种常量类型。

图6-3中常量池的第一个常量,它的标志位是0x07,再看表6-3会发现这个常量属于CONSTANT_Class_info类型,此类型的常量代表一个类或接口的符号引用。

CONSTANT_Class_info结构

tag是标志位

name_index是一个索引值,它指向常量池中的一个CONSTANT_Utf8_info类型的常量,此常量代表了这个类(或者接口)的全限定名,这里的name_index值(偏移地址:0x0000000B)为0x0002,即指向了常量池中的第二项常量。它的标志位(偏移地址:0x0000000D)是0x01,查看表6-3可知是一个CONSTANT_Utf8_info类型的常量,其结构为

length值说明这个字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。

本例中这个字符串的length值(偏移地址:0x0000000F)为0x001D,也就是长29个字节,内容为org/fenixsoft/clazz/TestClass

访问标志

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或接口层次的访问信息,包括:这个class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等等。

类索引、父类索引和接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。

类索引用于确定这个类的全限定名父类索引用于确定这个类的父类的全限定名。由于Java不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0.

接口索引结合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口的索引集合中

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

对于接口索引集合,入口的第一项 - u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,那么该计数器值为0,后面接口的索引表不再占用任何字节。

字段表集合

字段表(field_info)用于描述接口或类中声明的变量。字段(field)包括了类级变量或实例级变量,但不包括在方法内部声明的变量。

各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

字段修饰符放在access_flags中

跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称及字段和方法的描述符。

全限定名org/fenixsoft/clazz/TestClass是这个类的全限定名,在使用时最后一般会加入一个;表示全限定名结束

简单名称:指没有类型和参数修饰的方法或字段名称,方法inc()和字段m的简单名称分别是incm

方法和字段的描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte, char, double, float, int, long, short, boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示

对于数组类型,每一纬度将使用一个前置的[字符来描述,如一个定义为java.lang.String[][]类型的二维数组,将被记录为:[[Ljava/lang/String;一个整型数组int[]将被记录为[I

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()之内。

如方法void inc()的描述符为()V,方法java.lang.String toString()的描述符为()Ljava/lang/String,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为([CII[CIIII)I

  • 第一个u2类型的数据为容量计数器fields_count,其值为0x0001,说明这个类只有一个字段表数据。
  • 接下来是access_flags标志,其值为0x0002,代表private修饰符的ACC_PRIVATE标志位为真,其他修饰符为假。
  • 代表字段名称的name_index的值为0x0005,从代码清单列出的常量表中可查到第五项常量是一个CONSTANT_Utf8_info类型的字符串,其值为m;代表字段描述符的descriptor_index的值为0x0006,指向常量池的字符串I
  • 根据这些信息,我们可以推断出原代码定义的字段为private int m

方法表集合

方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

  • 第一个u2类型的数据(即是计数器容量)的值为0x0002,代表集合中有两个方法。
  • 第一个方法的访问标志值为0x001,也就是只有ACC_PUBLIC标志为真
  • 名称索引值为0x0007,查看代码清单的常量池可得方法名为<init>
  • 描述符索引值为0x0008,对应常量为()V
  • 属性表计数器attributes_count的值为0x0001就表示此方法的属性表集合有一项属性
  • 属性名称索引为0x0009,对应常量为Code,说明此属性是方法的字节码描述

Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还需要必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。

属性表集合

在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要说明属性值所占用的位数长度即可。

Code 属性

Java程序方法体里面的代码经过javac编译器处理之后,最终变为字节码指令存储在Code属性内。

  • attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为Code
  • attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共是6个字节,所以属性值的长度固定为整个属性表的长度减去6个字节
  • max_stack代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度。
  • max_locals代表了局部变量表所需的存储空间
2016/3/13 posted in  Java

事务原理与实践

事务简介

  • 事务就是锁和并发的结合体。
  • 所有的针对数据库的一个操作,都可以看做成一个事务。
  • 事务单元之间只有这四种关系:读写、写读、读读、写写
  • 真实业务场景中,如何以最快的速度的方式完成事务和事务之间的关系?又能保证上面四种逻辑的顺序?
  • 做法:

    • 序列化读写ACID中的I破坏了一致性
      • 优势:不需要冲突控制
      • 劣势:慢
    • 针对同一单元的访问进行控制(排他锁):如果两个事务发生冲突,就串行;如果两个事务不会发生冲突,就并行。

      譬如两个事务单元Bob给Smith一百块;李磊给韩梅梅一百块,这两个事务单元完全没有冲突,应该可以并行
      
    • 读写锁分开读读的场景并行;读写写读写写串行

    • MVCC:多版本并发控制Muti-Version Concurrency Control,本质就是Copy on write,每次写都是写一个新的数据,并不是在原位更新,能够做到写不阻塞读,带来写读读写场景的优化。

      • 劣势:系统实现的复杂度增加

现在只剩下写写才会产生冲突

事务谁先谁后?

一个读请求应该读哪一个写之后的数据?

逻辑时间戳:内存中维护一个数据的自增ID号,本质就是用来说明事务和事务单元之间谁先谁后

故障恢复

  • 业务属性不匹配:ACID中的A 回滚,需要记住之前数据没有更改时的状态
  • 系统崩溃

死锁与死锁检测

死锁产生的原因:

  • 两个线程参与
  • 不同方向上加锁
  • 作用在相同资源

解决方案:

  • 尽可能不死锁:降低事务隔离级别
  • 碰撞检测:把所有事务单元维持的所有锁都记下来,终止死锁的一边
  • 等锁超时

事务的ACID(数据库怎么保证的)

  • 原子性

要么全部成功,要么全部失败

只是记录了一个undo日志回滚到之前的版本

Bob给Smith一百块

ver1: Bob有100元 Smith有0元
ver2: Bob有0元 Smith有0元
    (undo:Bob有100元 Smith有0元) <= 数据库回滚段
ver3: Bob有100元 Smith有100元
    (undo:Bob有0元 Smith有0元) <= 数据库回滚段
  • 一致性
-- Lock Bob and Smith --
ver1: Bob有100元 Smith有0元
ver2: Bob有0元 Smith有0元
    (undo:Bob有100元 Smith有0元) <= 数据库回滚段
ver3: Bob有0元 Smith有100元
    (undo:Bob有0元 Smith有0元) <= 数据库回滚段
-- Unlock Bob and Smith --
  • 隔离性

以性能为理由,对一致性的破坏(各种隔离级别)

隔离性扩展:快照

  • 持久性

更改便持久的保存在数据库中,只要提交了就不丢

快照读的核心做法

在回滚段中读

  • 针对读多写少的情况

大部分数据库已经将快照读映射到读未提交、读已提交

事务的调优原则

  • 在不影响业务应用的前提下,减小锁的覆盖范围
  • 增加锁上可并行的线程数:读写锁分离、允许并行读取数据
  • 选择正确锁的类型

Consistent Nonlocking Reads in MySQL

A consistent read means that InnoDB uses multi-versioning to present to a query a snapshot of the database at a point in time.

The query sees the changes made by transactions that committed before that point of time, and no changes made by later or uncommitted transactions.

The exception to this rule is that the query sees the changes made by ealier statements within the same transaction. This exception causes the following anomaly: if you update some rows in a time, a SELECT sees the latest version of the updated rows, but it might also see older versions of any rows. If other sessions simultaneously update the same table, the anomaly means that you might see the table in a state that never existed in the database.

If the transaction isolation level is REPEATABLE READ (the default level), all consistent reads within the same transaction read the snapshot established byu the first such read in that transaction. You can get a fresher snapshot for your queries by committing the current transaction and after that issuing new queries.

With READ COMMITTED isolation level, each consistent read within a transaction sets and reads its own fresh snapshot.

Consistent read is the default mode in which InnoDB processes SELECT statements in READ COMMITTED and REPEATABLE READ isolation levels. A consistent read does not set any locks on the tables it accesses, and therefore other sessions are free to modify those tables at the same time a consistent read is being performed on the table.

Suppose that you are running in the default REPEATABLE READ isolation level. When you issue a consistent read (that is, an ordinary SELECT statement), InnoDB gives your transaction a timepoint according to which your query sees the database. If another transaction deletes a row and commits after your timepoint was assigned, you do not see the row as having been deleted. Inserts and updates are treated similarly.

你读数据的时候,数据不会加锁,其他人都能改你读的数据;但是你一直读到的都是最原始的数据,其他人怎么改都不会影响你读到的东西。

In the following example, session A sees the row inserted by B onluy when B has committed the insert and A has committed as well, so that the timepoint is advanced past the commit of B.

Session ASession B
SET autocommit=0; SET autocommit=0;
SELECT * FROM t; # empty set
INSERT INTO t VALUES (1, 2);
SELECT * FROM t; # empty set
COMMIT;
SELECT * FROM t; # empty set
COMMIT;
SELECT * FROM t;

READ UNCOMMITTED: UserA will see the change made by UserB. This isolation level is called dirty reads, which means that read data is not consistent with other parts of the table or the query, and may not yet have been committed. This isolation level ensures the quickest performance, as data is read directly from the table’s blocks with no further processing, verifications or any other validation. The process is quick and the data is as dirty as it can get.

READ COMMITTED: UserA will not see the change made by UserB. This is because in the READ COMMITTED isolation level, the rows returned by a query are the rows that were committed when the query was started. The change made by UserB was not present when the query started, and therefore will not be included in the query result.

REPEATABLE READ: UserA will not see the change made by UserB. This is because in the REPEATABLE READ isolation level, the rows returned by a query are the rows that were committed when the transaction was started. The change made by UserB was not present when the transaction was started, and therefore will not be included in the query result.

This means that “All consistent reads within the same transaction read the snapshot established by the first read” (from MySQL documentation.).

SERIALIZABLE: This isolation level specifies that all transactions occur in a completely isolated fashion, meaning as if all transactions in the system were executed serially, one after the other. The DBMS can execute two or more transactions at the same time only if the illusion of serial execution can be maintained.

In practice, SERIALIZABLE is similar to REPEATABLE READ, but uses a different implementation for each database engine. In Oracle, the REPEATABLE READ level is not supported and SERIALIZABLE provides the highest isolation level. This level is similar to REPEATABLE READ, but InnoDB implicitly converts all plain SELECT statements to “SELECT … LOCK IN SHARE MODE.

The default isolation level in MySQL’s InnoDB is REPEATABLE READ.

Since old values of row data are required for current queris, databases use a special segment to store old row values and snapshot. MySQL calls this segment a Rollback Segment (Undo Segment in Oracle).

2016/3/11 posted in  数据库

垃圾收集器与内存分配策略

程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性。

Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的垃圾收集器所关注的是这部分内存

引用技术算法

虚拟机并不是通过引用计数算法来判断对象是否存活的

根搜索算法

通过一系列的名为CG Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用连(Reference Chain),当一个对象到GC Roots没有任何引用连相连时,则证明此对象是不可用的

再谈引用

  • 强引用:类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉引用的对象
  • 软引用:用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收
  • 弱引用:用来描述非必需对象,它的强度比软引用更弱。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用:也称为幽灵引用或者幻影引用。它是最弱的一种引用关系。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知

生存还是死亡

在根搜索算法中不可达的对象,它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的,低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。稍后GC将对F-Queue中的对象进行第二次小规模的标记,只要对象能重新与引用链上的任何一个对象建立关联即可。

回收方法区

方法区(或者HotSpot虚拟机中的永久机)的垃圾收集主要回收两部分内容:废弃常量和无用的类。

回收废弃常量与回收Java堆中的对象非常类似。例如一个字符串abc已经进入了常量池中,但是没有任何String对象引用常量池中的abc常量,也没有其他地方引用了这个字面量,如果在这个时候发生内存回收,而且必要的话,这个abc常量就会被系统清除出了常量池。

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻。类要同时满足下面三个条件才能算是“无用的类”:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾收集算法

标记 - 清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致:当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。

这种算法的代价是将内存缩小为原来的一半。

新生代的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间每次使用Eden和其中的一块Survivor。当回收时,EdenSurvivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。

HotSpot虚拟机默认EdenSurvivor的大小比例是8:1,只有10%的内存会被浪费掉(备用Survivor),但是我们没办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion

标记 - 整理算法

根据老年代的特点,有人提出了另外一种“标记 - 整理”(Mark-Compact)算法,标记过程仍然与“标记-清楚”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

老年代中因为对象存活率高、没有额外空间对它进行担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

内存分配与回收策略

自动内存管理最终可以归结为自动化地解决了两个问题:

  • 给对象分配内存
  • 回收分配给对象的内存

对象的内存分配,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。

几条最普通的内存分配规则。

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

新生态GC(Minor GC):指发生在新生代的垃圾收集动作,Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC / Full GC):指发生在老年代的GC。

大对象直接进入老年代

所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

大对象对虚拟机的内存分配来说就是一个坏消息;比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”。

长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一个Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认是15岁)时,就会被晋升到老年代。

动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代;如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保

在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC

新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时,就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。

2016/3/11 posted in  Java

自动内存管理机制

程序计数器

程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。

字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

由于Java虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个内核只会执行一条线程中的指令。

因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,每条线程之间的计数器互不影响,独立存储。我们称这类内存区域为“线程私有”的内存

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。

Java虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期与线程相同。

虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)

其中64位长度的longdouble类型的数据会占用两个局部变量空间(slot),其余的数据类型只占用1个。

局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在桢中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

Java

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC堆”。

方法区(Method Area)

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性。Java不要求一定只能在编译器产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特征用得比较多的就是String类的intern()方法。

对象访问

Object obj = new Object();

如果这句代码出现在方法体中,那Object obj这部分的词义将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。

new Object()这部分的语义将会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定的。另外,在Java堆中还必须包含能查找此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。

不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。

  • 如果使用句柄访问方式,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

使用句柄访问方式

  • 如果使用直接指针访问方式,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是地址

使用直接指针访问方式

  • 使用句柄访问方式的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要被修改
  • 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多。
2016/3/10 posted in  Java