代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
1.字符串拼接和String.intern方法
Java类库
在Java语言中,通过加号来拼接字符串。作为优化,javac编辑器会把字符串拼接操作转换成StringBuilder的使用。例如下面这段Java代码:
String s1 = "a"; String s2 = "a"; String s4 = s1 + s2;
步骤如下:
如下的s1 + s2 的执行细节:(变量s是我临时定义的)
- StringBuilder s = new StringBuilder();
- s.append(“a”);
- s.append(“b”);
- s.toString();
补充:在jdk5.0之后使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer
为了运行上面的代码,本节将实现以下3个本地方法:
- System.arrayCopy()
- Float.floatToRawIntBits()
- Double.doubleToRawLongBits()
StringBuilder.append()方法只是调用了超类的append()方法,代码如下:

AbstractStringBuilder.append()方法调用了String.getChars()方法获取字符数组,代码如下:

String.getChars()方法调用了System.arraycopy()方法拷贝数组,代码如下:

StringBuilder.toString()方法,它调用了String的构造函数,这个构造函数调用了Arrays.copyOfRange()方法,代码如下:


Arrays.copyOfRange()调用了Math.min()方法,代码如下:

Math类在初始化时需要调用Float.floatToRawIntBits()和Double.doubleToRawLongBits()方法,代码如下:

System.arraycopy方法
整体代码
在/rtda/heap/class.go中加入IsPrimitive()函数判断类是否是基本类型的类,代码如下:
func (self *Class) IsPrimitive() bool {
_, ok := primitiveTypes[self.name]
return ok
}
在\native\java\lang目录下创建System.go文件,在其中注册arraycopy()方法,代码如下:
package lang
import (
"jvmgo/ch09/native"
"jvmgo/ch09/rtda"
"jvmgo/ch09/rtda/heap"
)
const jlSystem = "java/lang/System"
func init() {
native.Register(jlSystem, "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", arraycopy)
}
// public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
// (Ljava/lang/Object;ILjava/lang/Object;II)V
func arraycopy(frame *rtda.Frame) {
//从局部变量表中拿到5个参数
vars := frame.LocalVars()
src := vars.GetRef(0)
srcPos := vars.GetInt(1)
dest := vars.GetRef(2)
destPos := vars.GetInt(3)
length := vars.GetInt(4)
//源数组和目标数组都不能是null,否则按照System类的Javadoc应该抛出NullPointerException异常
if src == nil || dest == nil {
panic("java.lang.NullPointerException")
}
//源数组和目标数组必须兼容才能拷贝,否则应该抛出ArrayStoreExceptio异常
if !checkArrayCopy(src, dest) {
panic("java.lang.ArrayStoreException")
}
//检查srcPos、destPos和length参数,如果有问题则抛出IndexOutOfBoundsException异常
if srcPos < 0 || destPos < 0 || length < 0 ||
srcPos+length > src.ArrayLength() ||
destPos+length > dest.ArrayLength() {
panic("java.lang.IndexOutOfBoundsException")
}
//参数合法,调用ArrayCopy()函数进行数组拷贝
heap.ArrayCopy(src, dest, srcPos, destPos, length)
}
func checkArrayCopy(src, dest *heap.Object) bool {
srcClass := src.Class()
destClass := dest.Class()
//确保src和dest都是数组
if !srcClass.IsArray() || !destClass.IsArray() {
return false
}
//两者都是引用数组或相同类型的基本类型数组
if srcClass.ComponentClass().IsPrimitive() ||
destClass.ComponentClass().IsPrimitive() {
return srcClass == destClass
}
return true
}
在\rtda\heap\array_object.go文件中加入ArrayCopy函数:
func ArrayCopy(src, dst *Object, srcPos, dstPos, length int32) {
switch src.data.(type) {
case []int8:
_src := src.data.([]int8)[srcPos : srcPos+length]
_dst := dst.data.([]int8)[dstPos : dstPos+length]
//利用Go的内置函数copy()进行slice拷贝
copy(_dst, _src)
case []int16:
_src := src.data.([]int16)[srcPos : srcPos+length]
_dst := dst.data.([]int16)[dstPos : dstPos+length]
copy(_dst, _src)
case []int32:
_src := src.data.([]int32)[srcPos : srcPos+length]
_dst := dst.data.([]int32)[dstPos : dstPos+length]
copy(_dst, _src)
case []int64:
_src := src.data.([]int64)[srcPos : srcPos+length]
_dst := dst.data.([]int64)[dstPos : dstPos+length]
copy(_dst, _src)
case []uint16:
_src := src.data.([]uint16)[srcPos : srcPos+length]
_dst := dst.data.([]uint16)[dstPos : dstPos+length]
copy(_dst, _src)
case []float32:
_src := src.data.([]float32)[srcPos : srcPos+length]
_dst := dst.data.([]float32)[dstPos : dstPos+length]
copy(_dst, _src)
case []float64:
_src := src.data.([]float64)[srcPos : srcPos+length]
_dst := dst.data.([]float64)[dstPos : dstPos+length]
copy(_dst, _src)
case []*Object:
_src := src.data.([]*Object)[srcPos : srcPos+length]
_dst := dst.data.([]*Object)[dstPos : dstPos+length]
copy(_dst, _src)
default:
panic("Not array!")
}
}
代码解释
在System.go,实现arraycopy()方法。先来看第一部分。先从局部变量表中拿到5个参数。源数组和目标数组都不能是null,否则按照System类的Javadoc应该抛出NullPointerException异常,代码如下:

源数组和目标数组必须兼容才能拷贝,否则应该抛出ArrayStoreExceptio异常,代码如下:

checkArrayCopy()函数首先确保src和dest都是数组,然后检查数组类型。如果两者都是引用数组,则可以拷贝,否则两者必须是相同类型的基本类型数组。

接下来检查srcPos、destPos和length参数,如果有问题则抛出IndexOutOfBoundsException异常,参数合法,调用ArrayCopy()函数进行数组拷贝,代码如下:

真正的数组拷贝逻辑在ch09\rtda\heap\array_object.go文件中,利用Go的内置函数copy()进行slice拷贝。

Float.floatToRawIntBits和Double.doubleToRawLongBits方法
Float.floatToRawIntBits()和Double.doubleToRawLongBits()返回浮点数的编码。
在\native\java\lang目录下创建Float.go文件,在其中注册floatToRawIntBits()本地方法,代码如下:
package lang
import (
"jvmgo/ch09/native"
"jvmgo/ch09/rtda"
"math"
)
const jlFloat = "java/lang/Float"
func init() {
native.Register(jlFloat, "floatToRawIntBits", "(F)I", floatToRawIntBits)
native.Register(jlFloat, "intBitsToFloat", "(I)F", intBitsToFloat)
}
// public static native int floatToRawIntBits(float value);
// (F)I
func floatToRawIntBits(frame *rtda.Frame) {
value := frame.LocalVars().GetFloat(0)
bits := math.Float32bits(value) // todo
frame.OperandStack().PushInt(int32(bits))
}
// public static native float intBitsToFloat(int bits);
// (I)F
func intBitsToFloat(frame *rtda.Frame) {
bits := frame.LocalVars().GetInt(0)
value := math.Float32frombits(uint32(bits)) // todo
frame.OperandStack().PushFloat(value)
}
在native\java\lang目录下创建Double.go文件,在其中注册doubleToRawLongBits()本地方法,代码如下:
package lang
import (
"jvmgo/ch09/native"
"jvmgo/ch09/rtda"
"math"
)
const jlDouble = "java/lang/Double"
func init() {
native.Register(jlDouble, "doubleToRawLongBits", "(D)J", doubleToRawLongBits)
native.Register(jlDouble, "longBitsToDouble", "(J)D", longBitsToDouble)
}
// public static native long doubleToRawLongBits(double value);
// (D)J
func doubleToRawLongBits(frame *rtda.Frame) {
value := frame.LocalVars().GetDouble(0)
bits := math.Float64bits(value) // todo
frame.OperandStack().PushLong(int64(bits))
}
// public static native double longBitsToDouble(long bits);
// (J)D
func longBitsToDouble(frame *rtda.Frame) {
bits := frame.LocalVars().GetLong(0)
value := math.Float64frombits(uint64(bits)) // todo
frame.OperandStack().PushDouble(value)
}Go语言的math包提供了一个类似函数:Float32bits(),正好可以用来实现floatToRaw-IntBits()方法,我们之前讲过,不在阐述。
String.intern方法
前面讨论字符串时,实现了字符串池,但它只能在虚拟机内部使用。
下面实现String类的intern方法,让Java类库也可以使用它。
在\native\java\lang目录下创建String.go,在其中实现intern()方法,代码如下:
package lang
import (
"jvmgo/ch09/native"
"jvmgo/ch09/rtda"
"jvmgo/ch09/rtda/heap"
)
const jlString = "java/lang/String"
func init() {
native.Register(jlString, "intern", "()Ljava/lang/String;", intern)
}
// public native String intern();
// ()Ljava/lang/String;
func intern(frame *rtda.Frame) {
this := frame.LocalVars().GetThis()
interned := heap.InternString(this)
frame.OperandStack().PushRef(interned)
}
在ch09\rtda\heap\string_pool.go加入InternString()函数,如果字符串还没有入池,把它放入并返回该字符串,否则找到已入池字符串并返回。代码如下:
// todo
func InternString(jStr *Object) *Object {
goStr := GoString(jStr)
if internedStr, ok := internedStrings[goStr]; ok {
//如果找到已入池字符串并返回
return internedStr
}
//如果还没有入池,把它放入并返回该字符串
internedStrings[goStr] = jStr
return jStr
}
测试代码
下面的Java程序对字符串拼接和入池进行了测试。
package jvmgo.book.ch09;
public class StringTest {
public static void main(String[] args) {
String s1 = "abc1";
String s2 = "abc1";
System.out.println(s1 == s2); // true
int x = 1;
String s3 = "abc" + x;
System.out.println(s1 == s3); // false
s3 = s3.intern();
System.out.println(s1 == s3); // true
}
}
编译本章代码
go install jvmgo\ch09
ch09 -classpath D:\MAT_log -Xjre "D:\software\java\jre" StringTest

2.Object.hashCode、equals和toString
Object类有3个非常重要的方法:
- hashCode()返回对象的哈希码;
- equals()用来比较两个对象是否“相同”;
- toString()返回对象的字符串表示。
equals()和toString()则是用Java写的,hashCode()是个本地方法。

下面实现hashCode()方法。
打开ch09\native\java\lang\Object.go,导入unsafe包并注册hashCode()方法,代码如下:
func init() {
native.Register(jlObject, "getClass", "()Ljava/lang/Class;", getClass)
native.Register(jlObject, "hashCode", "()I", hashCode)
}
实现hashCode()方法,代码如下:
// public native int hashCode();
// ()I
func hashCode(frame *rtda.Frame) {
// 获取局部变量表第一个元素,this引用
this := frame.LocalVars().GetThis()
// 将对象引用转换成int32
hash := int32(uintptr(unsafe.Pointer(this)))
// 将对象引用转换成int32
frame.OperandStack().PushInt(hash)
}把对象引用(Object结构体指针)转换成uintptr类型,然后强制转换成int32推入操作数栈顶。
Golang 提供了 unsafe 包,让我们能够直接操作指定内存地址的内存。
官网:unsafe – The Go Programming Language (studygolang.com)
官网描述:

我翻译了一下:
—任何类型的指针值都可以转换为Pointer。
—Pointer可以转换为任意类型的指针值。
—uintptr类型可以转换为Pointer类型。
—Pointer类型可以转换为uintptr类型。
也就是说:
- 其他类型的指针只能转化为Pointer,也只有Pointer才能转化成任意类型的指针
- 只有uintptr才支持加减操作,而uintptr是一个非负整数,表示地址值,没有类型信息,以字节为单位
总结:
- 通过 unsafe.Pointer() 函数,我们能够获取变量的内存地址表示,本质上这是个整数。可以将任意变量的地址转换成 Pointer 类型,也可以将 Pointer 类型转换成任意的指针类型。
- 但 Pointer 不支持运算,如果要在内存地址上进行加减运算,需要将其转为 uintptr 类型。
- 将指针转换为uintptr类型(但不返回指针)。将指针转换为uintptr类型会产生所指向的值的内存地址,作为一个整数。
- 一般来说,将uintptr类型转换回指针是无效的。uintptr是一个整数,而不是引用。将指针转换为uintptr类型会创建一个没有指针语义的整数值。即使uintptr保存了某个对象的地址,如果对象移动,垃圾收集器也不会更新该uintptr的值,也不会阻止该对象被回收。
- 指针与uintptr类型之间的算术转换。如果p指向一个已分配的对象,则可以通过转换为uintptr,加上偏移量,然后转换回指针来在对象中前进。
- 使用unsafe.Pointer进行类型转换一般用于读取结构的私有成员变量或者修改结构的变量
本节只实现这一个本地方法。重新编译本章代码,然后测试下面的Java程序:
package jvmgo.book.ch09;
public class ObjectTest {
public static void main(String[] args) {
Object obj1 = new ObjectTest();
Object obj2 = new ObjectTest();
System.out.println(obj1.hashCode());
System.out.println(obj1.toString());
System.out.println(obj1.equals(obj2));
System.out.println(obj1.equals(obj1));
}
}重新编译本章代码:
go install jvmgo\ch09
ch09 -classpath D:\MAT_log -Xjre "D:\software\java\jre" ObjectTest

3.Object.clone
Object类提供了clone()方法,用来支持对象克隆。这也是一个本地方法,代码如下:

在\native\java\lang\Object.go文件中注册clone()方法,代码如下:
func init() {
native.Register(jlObject, "getClass", "()Ljava/lang/Class;", getClass)
native.Register(jlObject, "hashCode", "()I", hashCode)
native.Register(jlObject, "clone", "()Ljava/lang/Object;", clone)
}
继续编辑Object.go,实现clone()方法,如果类没有实现Cloneable接口,则抛出CloneNotSupportedException异常,否则调用Object结构体的Clone()方法克隆对象,然后把对象副本引用推入操作数栈顶。
// protected native Object clone() throws CloneNotSupportedException;
// ()Ljava/lang/Object;
func clone(frame *rtda.Frame) {
// 获取局部变量表第一个元素,this引用
this := frame.LocalVars().GetThis()
// 加载Cloneable类
cloneable := this.Class().Loader().LoadClass("java/lang/Cloneable")
if !this.Class().IsImplements(cloneable) {
panic("java.lang.CloneNotSupportedException")
}
// 将克隆后的对象引用入栈
frame.OperandStack().PushRef(this.Clone())
}
新建\rtda\heap\object_clone.go文件,加入Clone(),数据克隆逻辑在cloneData()函数中,因为数组也实现了Cloneable接口,所以代码中的case语句针对各种数组进行处理。
package heap
func (self *Object) Clone() *Object {
return &Object{
class: self.class,
data: self.cloneData(),
}
}
func (self *Object) cloneData() interface{} {
switch self.data.(type) {
case []int8:
elements := self.data.([]int8)
elements2 := make([]int8, len(elements))
copy(elements2, elements)
return elements2
case []int16:
elements := self.data.([]int16)
elements2 := make([]int16, len(elements))
copy(elements2, elements)
return elements2
case []uint16:
elements := self.data.([]uint16)
elements2 := make([]uint16, len(elements))
copy(elements2, elements)
return elements2
case []int32:
elements := self.data.([]int32)
elements2 := make([]int32, len(elements))
copy(elements2, elements)
return elements2
case []int64:
elements := self.data.([]int64)
elements2 := make([]int64, len(elements))
copy(elements2, elements)
return elements2
case []float32:
elements := self.data.([]float32)
elements2 := make([]float32, len(elements))
copy(elements2, elements)
return elements2
case []float64:
elements := self.data.([]float64)
elements2 := make([]float64, len(elements))
copy(elements2, elements)
return elements2
case []*Object:
elements := self.data.([]*Object)
elements2 := make([]*Object, len(elements))
copy(elements2, elements)
return elements2
default: // []Slot
slots := self.data.(Slots)
slots2 := newSlots(uint(len(slots)))
copy(slots2, slots)
return slots2
}
}
重新编译本章代码,测试下面的Java程序。
package jvmgo.book.ch09;
public class CloneTest implements Cloneable {
private double pi = 3.14;
@Override
public CloneTest clone() {
try {
return (CloneTest) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
CloneTest obj1 = new CloneTest();
CloneTest obj2 = obj1.clone();
obj1.pi = 3.1415926;
System.out.println(obj1.pi);
System.out.println(obj2.pi);
}
}
4.自动装箱和拆箱
每种基本类型都有一个包装类与之对应。从Java 5开始,Java语法增加了自动装箱和拆箱(autoboxing/unboxing)能力,可以在必要时把基本类型转换成包装类型或者反之。这个增强完全是由编译器完成的,Java虚拟机没有做任何调整。
以int类型为例,它的包装类是java.lang.Integer。它提供了2个方法来帮助编译器在int变量和Integer对象之间转换:
静态方法value()把int变量包装成Integer对象;实例方法intValue()返回被包装的int变量。这两个方法的代码如下:

由上面的代码可知,Integer.valueOf()方法并不是每次都创建Integer()对象,而是维护了一个缓存池IntegerCache。对于比较小(默认是–128~127)的int变量,在IntegerCache初始化时就预先加载到了池中,需要用时直接从池里取即可。IntegerCache是Integer类的内部类,为了便于参考,下面给出它的完整代码。


IntegerCache在初始化时需要确定缓存池中Integer对象的上限值,为此它调用了sun.misc.VM类的getSavedProperty()方法。要想让VM正确初始化需要做很多工作,这个工作第11章进行。这里先用一个hack让VM.getSavedProperty()方法返回非null值,以便IntegerCache可以正常初始化。
在实现功能之前,我们,先对代码,进行一点优化:
在/rtda/heap/class.go将getStaticMethod方法替换成getMethod方法
func (self *Class) getMethod(name, descriptor string, isStatic bool) *Method {
for c := self; c != nil; c = c.superClass {
for _, method := range c.methods {
if method.IsStatic() == isStatic &&
method.name == name &&
method.descriptor == descriptor {
return method
}
}
}
return nil
}再将引用到getStaticMethod方法的方法替换成getMethod,在多加个参数,判断是否是静态。

在/rtda/heap/class.go加入以下方法,代码简单,不在阐述:
func (self *Class) GetInstanceMethod(name, descriptor string) *Method {
return self.getMethod(name, descriptor, false)
}
func (self *Class) GetRefVar(fieldName, fieldDescriptor string) *Object {
field := self.getField(fieldName, fieldDescriptor, true)
return self.staticVars.GetRef(field.slotId)
}
func (self *Class) SetRefVar(fieldName, fieldDescriptor string, ref *Object) {
field := self.getField(fieldName, fieldDescriptor, true)
self.staticVars.SetRef(field.slotId, ref)
}
在\native\sun\misc\VM.go文件,然后在VM.go文件中注册initialize()方法,代码如下:
package misc
import (
"jvmgo/ch09/instructions/base"
"jvmgo/ch09/native"
"jvmgo/ch09/rtda"
"jvmgo/ch09/rtda/heap"
)
func init() {
native.Register("sun/misc/VM", "initialize", "()V", initialize)
}
// private static native void initialize();
// ()V
func initialize(frame *rtda.Frame) { // hack: just make VM.savedProps nonempty
vmClass := frame.Method().Class()
savedProps := vmClass.GetRefVar("savedProps", "Ljava/util/Properties;")
key := heap.JString(vmClass.Loader(), "foo")
val := heap.JString(vmClass.Loader(), "bar")
frame.OperandStack().PushRef(savedProps)
frame.OperandStack().PushRef(key)
frame.OperandStack().PushRef(val)
propsClass := vmClass.Loader().LoadClass("java/util/Properties")
setPropMethod := propsClass.GetInstanceMethod("setProperty",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;")
base.InvokeMethod(frame, setPropMethod)
}
initialize()方法的实现如上,上面的hack代码可能有些不好理解,但翻译成等价的Java代码后只有一句话:
private static native void initialize() {
VM.savedProps.setProperty("foo", "bar")
}最后,需要修改invokenative.go,在其中导入misc包,改动如下:
import (
"jvmgo/ch09/instructions/base"
"jvmgo/ch09/native"
_ "jvmgo/ch09/native/java/lang"
_ "jvmgo/ch09/native/sun/misc"
"jvmgo/ch09/rtda"
)重新编译本章代码,然后测试下面的Java程序:
package jvmgo.book.ch09;
import java.util.ArrayList;
import java.util.List;
public class BoxTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
System.out.println(list.toString());
for (int x : list) {
System.out.println(x);
}
}
}
如图,执行结果如下:

5.参考
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)


