深入理解Java内存模型(总结)

处理器内存模型

顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照。JMM和处理器内存模型在设计时会对顺序一致性模型做一些放松,因为如果完全按照顺序一致性模型来实现处理器和JMM,那么很多的处理器和编译器优化都要被禁止,这对执行性能将会有很大的影响。
根据对不同类型读/写操作组合的执行顺序的放松,可以把常见处理器的内存模型划分为下面几种类型。

  1. 放松程序中写-读操作的顺序,由此产生了total store ordering内存模型(简称为TSO)。
  2. 在前面1的基础上,继续放松程序中写-写操作的顺序,由此产生了partial store order内存模型(简称为PSO)。
  3. 在前面1和2的基础上,继续放松程序中读-写和读-读操作的顺序,由此产生了relaxed memory order内存模型(简称为RMO)和PowerPC内存模型。

注意,这里处理器对读/写操作的放松,是以两个操作之间不存在数据依赖性为前提的(因为处理器要遵守as-if-serial语义,处理器不会对存在数据依赖性的两个内存操作做重排序)。
下面的表格展示了常见处理器内存模型的细节特征。


在这个表格中,我们可以看到所有处理器内存模型都允许写-读重排序,原因在第一章以说明过:它们都使用了写缓存区,写缓存区可能导致写-读操作重排序(处理器执行的内存操作和实际内存执行的不一样)。同时,我们可以看到这些处理器内存模型都允许更早读到当前处理器的写,原因同样是因为写缓存区:由于写缓存区仅对当前处理器可见,这个特性导致当前处理器可以比其他处理器先看到临时保存在自己的写缓存区中的写。
上面表格中的各种处理器内存模型,从上到下,模型由强变弱。越是追求性能的处理器,内存模型设计的会越弱。因为这些处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。
由于常见的处理器内存模型比JMM要弱,Java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。同时,由于各种处理器内存模型的强弱并不相同,为了在不同的处理器平台向程序员展示一个一致的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不相同。
下图展示了JMM在不同处理器内存模型中需要插入的内存屏障的示意图。

深入理解Finalize

基础

Java技术允许使用finalize()在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
这个方法是由垃圾收集器在确定这个对象没有被引用时,对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()以整理系统资源或者执行其他清理工作(要不然会引起资源泄露,有可能导致程序崩溃)。finalize()是在垃圾收集器删除对象之前被自动调用的。
垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java提供了一个名为finalize(),它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存(垃圾回收需要2次)。
所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作(如关闭流等操作)。但JVM(Java虚拟机)不保证此方法总被调用。
finalize()抛出的未捕获异常只会导致该对象的finalize()执行退出。
用户可以自己调用对象的finalize(),但是这种调用是正常的方法调用,和对象的销毁过程无关。

对象销毁和对象重生

一个简单的对象生命周期为,UnfinalizedFinalizableFinalizedReclaimed
在对象的销毁过程中,按照对象的finalize()的执行情况,可以分为以下几种,系统会记录对象的对应状态。

unfinalized

没有执行finalize(),系统也不准备执行。

finalizable

可以执行finalize()了,系统会在随后的某个时间执行finalize

finalized

该对象的finalize()已经被执行了。
GC怎么来保持对finalizable()的对象的追踪呢。GC有一个Queue,叫做F-Queue,所有对象在变为finalizable的时候会加入到该Queue,然后等待GC执行它的finalize()
这时我们引入了对对象的另外一种记录分类,系统可以检查到一个对象属于哪一种:

reachable

从活动的对象引用链可以到达的对象。包括所有线程当前栈的局部变量,所有的静态变量等等。

finalizer-reachable

除了reachable外,从F-Queue可以通过引用到达的对象。

unreachable

其它的对象,不可达对象。

  1. 首先,所有的对象都是从Reachable+Unfinalized(没有执行finalize(),对象可达)走向死亡之路的。
  2. 从当前活动集到对象不可达时,对象可以从Reachable状态变到F-Reachable(进入F-Queue,对象变成finalizable状态)或者Unreachable状态。
  3. 当对象为非Reachable+Unfinalized时,GC会把它移入F-Queue,状态变为F-Reachable+Finalizable(进入F-Queue,可达,finalizable状态)。
  4. 任何时候,GC都可以从F-Queue中拿到一个Finalizable的对象,标记它为Finalized,然后执行它的finalize(),由于该对象在这个线程中又可达了,于是该对象变成Reachable了(并且Finalized)。而finalize()执行时,又有可能把其它的F-Reachable(进入F-Queuefinalizable状态)的对象变为一个Reachable的,这个叫做对象再生。
  5. 当一个对象在Unreachable+Unfinalized时,如果该对象使用的是默认的Objectfinalize,或者虽然重写了,但是新的实现什么也不干(子类覆写一个空方法)。为了性能,GC可以把该对象之间变到Reclaimed状态直接销毁,而不用加入到F-Queue等待GC做进一步处理。(不可达,不执行finalize()
  6. 从状态图看出,不管怎么折腾,任意一个对象的finalize只至多执行一次,一旦对象变为Finalized(执行过finalized()),就怎么也不会在回到F-Queuefinalizable状态)去了。当然没有机会再执行finalize了。
  7. 当对象处于Unreachable+Finalized时,该对象离真正的死亡不远了。GC可以安全的回收该对象的内存了。进入Reclaimed

原子性,临界区,互斥量,信号量

原子性

原子性就是说一个操作不可以被中途CPU暂停然后调度,即不能被中断,要不就执行完,要不就不执行。

原子操作

是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生中断之前执行完毕。
一个不正确的知识:“原子操作不需要进行同步控制”。
原子性可以应用于基本数据类型(64位的longdouble不是原子性),对于写入和读取,可以把它们当作原子操作来操作内存。但是,JVM可以将64(longdouble)的读取和写入当作两个分离的32位操作来执行,这个就产生在读取和写入操作中间发生上下文切换问题,导致不同的任务可以看到不正确结果。

JDK7自带工具

JDK7自带工具目录

1
2
3
4
C:\Users\Administrator>java -version
java version "1.7.0_76"
Java(TM) SE Runtime Environment (build 1.7.0_76-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.76-b04, mixed mode)


命令 | 说明
—|—
appletviewer | 用于运行并浏览applet小程序。
extcheck | 扩展检测工具,主要用于检测指定jar文件与当前已安装的Java SDK扩展之间是否存在版本冲突。
idlj | IDL转Java编译器(IDL-to-Java Compiler),用于为指定的IDL文件生成Java绑定。IDL意即接口定义语言(Interface Definition Language)。
jar | jar文件管理工具,主要用于打包压缩、解压jar文件。
jarsigner | jar密匙签名工具。
java | Java运行工具,用于运行.class字节码文件或.jar文件。
javac | Java编译工具(Java Compiler),用于编译Java源代码文件。
javadoc | Java文档工具,主要用于根据Java源代码中的注释信息生成HTML格式的API帮助文档。
javafxpackager | JavaFX包装器,用于执行与封装或签名JavaFX应用有关的任务。JDK 8u20已经迁移此工具到javapackager。
javah | Java头文件工具,用于根据Java类生成C/C++头文件和源文件(主要用于JNI开发领域)。
javap | Java反编译工具,主要用于根据Java字节码文件反汇编为Java源代码文件。
javapackager | 执行针对Java应用程序和JavaFX应用程序的打包和签名的任务。包含了javafxpackager的功能。
jcmd | Java 命令行(Java Command),用于向正在运行的JVM发送诊断命令请求。
jconsole | 图形化用户界面的监测工具,主要用于监测并显示运行于Java平台上的应用程序的性能和资源占用等信息
jdeps | 用于分析Java class的依赖关系
jdb | Java调试工具(Java Debugger),主要用于对Java应用进行断点调试
jhat | Java堆分析工具(Java Heap Analysis Tool),用于分析Java堆内存中的对象信息。
jinfo | Java配置信息工具(Java Configuration Information),用于打印指定Java进程、核心文件或远程调试服务器的配置信息。
jjs | 对Nashorn引擎的调用。Nashorn是基于Java实现一个轻量级高性能的JavaScript运行环境。
jmap | Java内存映射工具(Java Memory Map),主要用于打印指定Java进程、核心文件或远程调试服务器的共享对象内存映射或堆内存细节。
jmc | Java任务控制工具(Java Mission Control),主要用于HotSpot JVM的生产时间监测、分析、诊断。开发者可以使用jmc命令来创建JMC工具。
jps | JVM进程状态工具(JVM Process Status Tool),用于显示目标系统上的HotSpot JVM的Java进程信息。
jrunscript | Java命令行脚本外壳工具(command line script shell),主要用于解释执行javascript、groovy、ruby等脚本语言。
jsadebugd | Java可用性代理调试守护进程(Java Serviceability Agent Debug Daemon),主要用于附加到指定的Java进程、核心文件,或充当一个调试服务器。
jstack | Java堆栈跟踪工具,主要用于打印指定Java进程、核心文件或远程调试服务器的Java线程的堆栈跟踪信息。
jstat | JVM统计监测工具(JVM Statistics Monitoring Tool),主要用于监测并显示JVM的性能统计信息,包括gc统计信息。
jstatd | jstatd(VM jstatd Daemon)工具是一个RMI服务器应用,用于监测HotSpot JVM的创建和终止,并提供一个接口,允许远程监测工具附加到运行于本地主机的JVM上。
jvisualvm | JVM监测、故障排除、分析工具,主要以图形化界面的方式提供运行于指定虚拟机的Java应用程序的详细信息。
keytool | 密钥和证书管理工具,主要用于密钥和证书的创建、修改、删除等。主要用于获取或缓存Kerberos协议的票据授权票据。允许用户查看本地凭据缓存和密钥表中的条目(用于Kerberos协议)。Kerberos密钥表管理工具,允许用户管理存储于本地密钥表中的主要名称和服务密钥。
native2ascii | 本地编码到ASCII编码的转换器(Native-to-ASCII Converter),用于”任意受支持的字符编码”和与之对应的”ASCII编码和(或)Unicode转义”之间的相互转换。
orbd | 对象请求代理守护进程(Object Request Broker Daemon),它使客户端能够透明地定位和调用位于CORBA环境的服务器上的持久对象。
pack200 | JAR文件打包压缩工具,它可以利用Java类特有的结构,对普通JAR文件进行高效压缩,以便于能够更快地进行网络传输。这是微软提供的对象包装程序,用于对象安装包。
policytool | 策略工具,用于管理用户策略文件(.java.policy)。
rmic | Java RMI 编译器,为使用JRMP或IIOP协议的远程对象生成stub、skeleton、和tie类,也用于生成OMG IDL。
rmid | Java RMI 激活系统守护进程,rmid启动激活系统守护进程,允许在虚拟机中注册或激活对象。
rmiregistry | Java 远程对象注册表,用于在当前主机的指定端口上创建并启动一个远程对象注册表。
schemagen | XML schema生成器,用于生成XML schema文件。
serialver | 序列版本命令,用于生成并返回serialVersionUID。
servertool | Java IDL 服务器工具,用于注册、取消注册、启动和终止持久化的服务器。
tnameserv | Java IDL瞬时命名服务。
unpack200 | JAR文件解压工具,将一个由pack200打包的文件解压提取为JAR文件。
wsgen | XML Web Service 2.0的Java API,生成用于JAX-WS Web Service的JAX-WS便携式产物。
wsimport | XML Web Service 2.0的Java API,主要用于根据服务端发布的wsdl文件生成客户端存根及框架。
xjc | 主要用于根据XML schema文件生成对应的Java类。

Jstat

Jstat用于监控基于HotSpot的JVM,对其堆的使用情况进行实时的命令行的统计,使用jstat我们可以对指定的JVM做如下监控:

  • 类的加载及卸载情况。
  • 查看新生代、老生代及持久代的容量及使用情况。
  • 查看新生代、老生代及持久代的垃圾收集情况,包括垃圾回收的次数及垃圾回收所占用的时间。
  • 查看新生代中Eden区及Survior区中容量及分配情况等。

jstat工具特别强大,它有众多的可选项,通过提供多种不同的监控维度,使我们可以从不同的维度来了解到当前JVM堆的使用情况。详细查看堆内各个部分的使用量,使用的时候必须加上待统计的Java进程号,可选的不同维度参数以及可选的统计频率参数。它主要是用来显示GC及PermGen相关的信息。

线程安全和锁机制

概念

临界区

临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待(例如:bounded waiting等待法),有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用。只能被单一线程访问的设备,例如:打印机。

互斥量

互斥量是一个可以处于两态之一的变量:解锁和加锁。
这样,只需要一个二进制位表示它,不过实际上,常常使用一个整型量,0表示解锁,而其他所有的值则表示加锁。互斥量使用两个过程。当一个线程(或进程)需要访问临界区时,它调用mutex_lock。如果该互斥量当前是解锁的(即临界区可用),此调用成功,调用线程可以自由进入该临界区。
另一方面,如果该互斥量已经加锁,调用线程被阻塞,直到在临界区中的线程完成并调用mutex_unlock。如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许它获得锁。

Java Copy-On-Write并发优化策略

基础知识

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。
开始都在共享同一个内容,当想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。

CopyOnWrite容器

CopyOnWrite容器即写时复制的容器。通俗的理解:往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。(但是读取的数据会有延迟)所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。目的是为了提高并发能力。

Java Class文件的结构(实践篇)

实例

首先有一个TestClass类,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.ejushang.TestClass;

public class TestClass implements Super {
private static final int staticVar = 0;
private int instanceVar = 0;

public int instanceMethod(int param) {
return param + 1;
}

}

interface Super {
}

通过jdk1.6.0_37的javac编译后的TestClass.java对应的TestClass.class的二进制结构如下。
TestClass.class
用WinHex工具查看字节码。

Java Class文件的结构(理论篇)


学习Java的朋友应该都知道Java从刚开始的时候就打着平台无关性的旗号,说“一次编写,到处运行”,其实说到无关性,Java平台还有另外一个无关性那就是语言无关性,要实现语言无关性,那么Java体系中的class的文件结构或者说是字节码就显得相当重要了,其实Java从刚开始的时候就有两套规范,一个是Java语言规范,另外一个是Java虚拟机规范,Java语言规范只是规定了Java语言相关的约束以及规则,而虚拟机规范则才是真正从跨平台的角度去设计的。今天我们就以一个实际的例子来看看,到底Java中一个Class文件对应的字节码应该是什么样子。 这篇文章将首先总体上阐述一下Class到底由哪些内容构成,然后再用一个实际的Java类入手去分析class的文件结构。

Byte大小端模式

什么是大小端

大端模式

高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。

小端模式

高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。

位运算

基础知识

&(与)

与运算符用符号&表示,其使用规律如下:两个操作数中位都为1,结果才为1,否则结果为0。

例子1

1
2
3
int a=129;
int b=128;
System.out.println("a&b的结果是:"+(a&b));
1
a&b的结果是:128

“a”的值是129,转换成二进制就是:10000001
“b”的值是128,转换成二进制就是:10000000
根据与运算符的运算规律,只有两个位都是1,结果才是1,可以知道结果就是10000000,即128。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×