Posts tagged Java
Java中Integer的大小是int的几倍
0今天看到一个不错的PPT:Build Memory-efficient Java Applications,开篇便提出了一个问题,在Hotspot JVM中,32位机器下,Integer对象的大小是int的几倍?
我们都知道在Java语言规范已经规定了int的大小是4个字节,那么Integer对象的大小是多少呢?要知道一个对象的大小,那么必须需要知道对象在虚拟机中的结构是怎样的,来看看Hotspot中对象在内存中的结构:

从上面的这张图里面可以看出,对象在内存中的结构主要包含以下几个部分:
- Mark Word:对象的Mark Word部分占4个字节,其内容是一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。
- Class对象指针:Class对象指针的大小也是4个字节,其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址
- 对象实际数据:这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节
- 对齐:最后一部分是对齐填充的字节,按8个字节填充。
根据上面的图,那么我们可以得出Integer的对象的结构如下:

Integer只有一个int类型的成员变量value,所以其对象实际数据部分的大小是4个字节,然后再在后面填充4个字节达到8字节的对齐,所以可以得出Integer对象的大小是16个字节。
因此,我们可以得出Integer对象的大小是原生的int类型的4倍。
关于对象的内存结构,需要注意数组的内存结构和普通对象的内存结构稍微不同,因为数据有一个长度length字段,所以在对象头后面还多了一个int类型的length字段,占4个字节,接下来才是数组中的数据,如下图:

关于对象内存布局更多的内容,可以看这篇文章:Java Objects Memory Structure
Varamyr – 让Nook 2也可以轻松阅读中文的epub
2好啦,这个标题有点标题党的嫌疑,前几天入手了Nook 2,但是苦于Nook对中文支持并不好,看中文的epub直接显示方块或者问号之类的乱码。
网上看了下这个问题的大致解决方法有两个:一个是Root掉Nook;另一个往epub文件中塞入一段CSS,再在Nook中看的时候只需要选择Publisher Defaults就可以正常显示中文。
第一个方法风险太高,可能导致Nook变砖;
第二个方法的话可以用Calibre之类的工具来完成,但是Calibre这个东西本身太重了,每次打开关闭很耗时间,UI又丑,实在不想用。
所以,我需要一个命令行的工具来完成往epub中塞入一段CSS这个任务,这样可以和*nix下的其他工具结合起来使用,检测一个文件夹中如果新加入了epub文件,就直接进行转换,让整个过程完全自动化。现在这个命令行工具就是Varamyr(Varamyr是马丁大爷的冰与火之歌的第五部《魔龙的狂舞》序言里面的一个狼灵,打酱油的角色。)
运行Varamyr你需要
- JRE环境
- *nix系统,不支持windows也不会支持windows
- 下载varamyr.jar
如何使用?
- 运行命令:
java -jar varamyr.jar <path-to-epub-file>,<path-to-epub-file>为你需要修改的epub文件的路径 - 看看在你修改的epub文件的路径下是不是多了一个类似
xxxx-varamyr.epub的文件。
源代码 & 原理
- 源代码放在github上了:https://github.com/khotyn/Varamyr
- 原理很简单,不用讲了,直接看源代码吧。
内存屏障
2前几天一直在看Java内存模型(JMM)这个东西,在了解JMM的过程中也不断地遇到了内存屏障这个名词,遂花了一点时间去了解了一下。
什么是内存屏障
内存屏障,说白了就是一个指令,其作用正如其名字里面所描述的,起到的是屏障的作用,具体地来说,内存屏障这条指令的作用是让屏障指令前面的指令不会被重排序到屏障指令之后,屏障指令之后的指令也不会被重排序到屏障指令之前,相当于一道屏障挡在了指令之间,前面和后面的指令都不能跨过屏障(关于指令重排序,这里不再解释,有兴趣了解的可以去Google一下)。
内存屏障的种类
鉴于CPU和编译器都可能对指令进行重排序,所以我将内存屏障分为两种:
- 编译器级别的:防止编译器对指令进行重排序,例如GCC的
asm volatile("" ::: "memory")等等。 - 处理器级别的:防止处理器对指令进行重排序,例如X86的
lock,lfence(),sfenec()等等(关于处理器级别的内存屏障如何实现,可以参考这篇文章)。
Java的volatile如何通过内存屏障保证其语义的:
首先,需要知道Java中的volatile有两个语义:
- 线程对volatile变量的读写都会马上反映到主存上,对其他线程马上可见。
- 对volatile变量的读写操作不会和其他对共享内存进行操作的指令重排序。
跟踪HotSpot JVM中对volatile的变量的写操作可以发现,其在写操作之后插入了一条指令
asm volatile ("lock; addl $0,0(%%esp)" ::: "cc", "memory");
我们可以把这个指令分成两部分来看:
asm volatile ("" ::: "memory"):这个是GCC中常用的编译器级别的内存屏障,首先这是一条汇编指令,加入了volatile表示指令不会被重排,后面的clobber list里面加入了”memory”表示告诉编译器,指令对主存做了修改,所有CPU中的缓存都失效了,后面的指令需要重新从主存中重新Load数据。lock; addl $0,0(%%esp):对于这一条指令,首先看addl $0,0(%%esp)这个部分,这个部分实际上是个无用操作,如果esp栈顶是0的话,就给栈顶加上一个0,由于这个操作会影响到程序状态字寄存器,所以在后面的clobber list中加入了cc(Condition Code Registry,也称为Flag Registry或Programm Status Word Registry);再看这个汇编指令前面加入了一个lock前缀,这个lock前缀的意思是执行指令的CPU会发出LOCK#信号,独占系统主线直到前面的读写操作全部完成并且执行完加了lock前缀的这条指令,在这个过程中其他CPU的读写操作全部被Hold住(参考Intel的开发手册)。所以lock前缀实际上可以看作是一个多处理器的内存屏障。
这两个部分合起来就起到了一个完整的内存屏障,即对编译器有效,也对处理器有效,另外也保证了volatile的可见性问题。
JVM规范之内存结构
0原文地址:http://www.goldendoc.org/2011/11/jvm_memory_management/
JVM规范中一块很重要的地方就是内存的管理,JVM规范里面在功能上对JVM的内存进行了一定的划分,理解这一块内容对理解JVM的规范有一定的帮助。当然,就像许多规范一样,JVM规范虽然对内存进行了划分,但是各个JVM的实现却不一定会做这样的划分,说地确切一点是负责对应功能的内存肯定在每一个JVM的实现里面都是有的(如果没有提供参数给程序员调整的话,有些对你来说就是不可见的),但是其实现方式可以是五花八门的。另外,JVM对内存的分配和管理事实上是和Java Class文件的格式,JVM的运行机制都有密切相关,如果对这些东西有所了解,那么对内存的管理理解也会有所帮助。
先来看一张图来直观地感受下JVM规范里面是怎样对JVM的内存进行功能上的区分的:
在这张图里面,首先根据内存是线程私有的还是线程共享的,把内存区域分成了两块:
线程私有
这一块区域主要由程序计数器(PC),虚拟机栈(Virtual Machine Stack)和本地方法栈(Native Method Stack)组成
- 程序计数器:同计算机的程序计数器;如果正在执行Java方法,则指向虚拟机字节码指令的地址,如果是native的,这个计算器的值为空。
- 虚拟机栈:虚拟机栈是和线程的生命周期相同的,每创建一个线程,就会创建一个虚拟机栈。而在一个线程中,每调用一个方法,就会在虚拟机栈中创建一个栈帧(Stack Frame),用于保存方法相关的信息,栈帧的具体结构看上图的箭头左边部分:
- 本地变量表(Local Variable):用于保存本地变量,这里的本地变量是指方法的入参以及在方法中声明的变量,如果在编译Java文件的时候加上”-g”参数,然后用”javap -p -verbose”打印出class文件后可以看到一个方法的本地变量表的内容,下面是一个简单的“Hello, world!”程序的main方法的本地变量表:
LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;
- 操作数栈(Operand Stack):首先要了解JVM的架构是基于栈的指令集,指令的操作都是对栈的操作,所以方法运行的时候的一些参数和中间变量都必须先放入栈中才能够被操作,这里面的栈指的就是操作数栈,注意不要和前面提到的虚拟机栈混为一谈。
- 对运行时常量池的引用(Runtime Constant Pool Reference):这里面主要保存对在方法中调用的方法的符号引用,这些符号引用会在运行时被解析成对方法的真正引用。
- 本地变量表(Local Variable):用于保存本地变量,这里的本地变量是指方法的入参以及在方法中声明的变量,如果在编译Java文件的时候加上”-g”参数,然后用”javap -p -verbose”打印出class文件后可以看到一个方法的本地变量表的内容,下面是一个简单的“Hello, world!”程序的main方法的本地变量表:
- 本地方法栈:本地方法栈的功能和虚拟机栈的作用相同,不同的是本地方法栈服务的对象是本地方法。
线程共享
线程共享这一块区域说起来就一块东西:堆;稍微细分一下可以分为方法区(Method Area)和”存放类实例的区域”
- 方法区:主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译以后的代码,其中运行时常量池也在方法区中。
- “存放类实例的区域”:这一部分内存是存放对象,所有实例化的对象都会放在这个地方。
直接内存
前面看到的都是JVM的运行时数据区的部分,JVM的内存中还有另外一部分叫直接内存(Direct Memory,对应上面的图中的最下方的地方),它不是JVM运行时数据区的一部分,不受JVM的GC管理。其中Java NIO中的Buffer直接在堆外分配的就属于这个部分。
OpenJDK编译手记
4作为一位程序员,我坚信了解一件事物的最佳方式是深入到它的最底层去研究其运行的原理。既然我大部分时间都在写Java代码,那么就肯定需要了解JVM的运行原理。所以上周尝试了编译了一下JVM的开源实现:OpenJDK。
准备工作
在开始编译之前,首先需要一些准备工作:
- 操作系统:建议在2.x内核的Linux系统上编译,在Windows下编译的话还要装CYGWIN,比较麻烦,如果你在3.0以上内核版本的Linux下编译,首先你会遇到一个This OS is not supported的错误,然后即使你在编译的时候加上了DISABLE_HOTSPOT_OS_VERSION_CHECK=ok这个选项,你可能会遇到一些宏定义找不到之类的编译错误。
- 源代码:编译OpenJDK当然需要OpenJDK的源代码,可以从这里下载
- 依赖:编译OpenJDK所需的东西在下载过来的源代码包里面的README_builds.html中都有列出,这里列出一些大概的依赖,具体所需的版本可以在README_builds.html中找到:OpenJDK(是的,编译OpenJDK需要一个OpenJDK,有点鸡生蛋,蛋生鸡的感觉),make,g++,ant,axel(Linux声音相关),X11(图形相关,编译awt的时候需要),freetype2(字体相关)等
- 环境变量:编译以前请先分别设置语言和编译所需的OpenJDK的路径:
export LANG=C ALT_BOOTDIR=/usr/lib/jvm/java-openjdk
编译
安装好所需的依赖以后,可以用以下命令检查一下依赖是否都已经正确安装了:
make sanity
如果输出没有错误,则可以用一下命令编译:
make ALLOW_DOWNLOADS=true
加上ALLOW_DOWNLOADS=true的原因是编译过程中ant可能会需要下载一些东西,如果你要编译一个DEBUG版本,则可以用以下命令
make debug_build ALLOW_DOWNLOADS=true
遇到的问题
我在编译过程还是遇到了不少问题,首先在Ubuntu 11.10下编译,遇到了前面提到的Linux内核3.0的问题,后来改到在CentOS 6下编译。
在CentOS下编译首先遇到了一个ant的一个类找不到的问题(在一个叫optional的package下),后来看了一下我直接用yum install安装的ant默认并没有安装一些optional的包,后来又用yum install安装了那个类对应的包,算是解决了这个错误。如果读者是直接在ant的官方网站下载安装应该不会有这个问题。
再之后就是在编译过程中遇到了很多缺少X11的头文件的错误,幸好发现yum有yum provides这个命令可以找到头文件所在的包,然后用yum install安装。(:-),一直在Ubuntu下用apt-get的孩子伤不起啊~~)
整个编译过程大概需要1个多小时(AMD速龙3800,2G RAM,单Job),这个期间可以搞点下午茶吃吃,搞部小电影看看~~
最后感谢下方攀同学在过程中提供的帮助,^_^
