用相同的实现方式(数据结构)实现一个系统?jvm字符串的创建指令的问题

发表时间:2018-03-04 05:10:02 作者: 来源: 浏览:

在上一篇文章中,小编为您详细介绍了关于《我想问一下我MI2USB连接电脑读不到内置储存?MI2USB连接电脑读不到内置存储》相关知识。本篇中小编将再为您讲解标题用相同的实现方式(数据结构)实现一个系统?jvm字符串的创建指令的问题。

结构良好的Java程序中数据结构比同样结构良好的C程序的数据结构会耗用更多内存是不争的事实。跟Go相比的话看情况。

最主要的问题是当前版本的Java对数据的布局的控制,在控制精度和粒度上都比较受限。

以C或者C++为例,对数据的操作可以有若干自由度:(下面提到“对象”不只指class或者struct,而是也包括像int这样的原始类型。为了方便叙述而统称为对象)

直接访问对象的实体(值)通过指针间接访问对象可以在聚合类型(数组或struct / class / union)中直接嵌入别的对象的实体(值)可以在聚合类型中存指针,间接指向别的对象甚至可以在定长的聚合类型的末尾嵌入不定长的数据

在C或C++里,class或者struct自身其实并没有限制该以值类型还是引用类型的方式来使用之,纯粹取决于某次使用具体是怎么用的。当然C++里可以通过①些声明方式来引导使用者只以某些特定的方式来用某些自定义class,例如说只允许作为局部变量使用(StackObject),只允许作为值来使用(ValueObject),或者只能够通过某种分配器来分配,或者只允许在堆上独立分配(HeapObject)——换言之只能应该指针来访问;但这些都并不是class或者struct内在的特性,而是需要额外通过技巧来实现的。

例如说,C里的:

struct Point { int x; int y;};

作为①个局部变量来声明的时候,我们就可以直接访问这个Point对象的实体(值):

int foo() { struct Point p = { ② · ③ }; return p.x;}

通过malloc()在堆上分配①个Point对象,我们就会通过指针去间接访问其实体,同时也可以通过解引用去直接访问其实体:

int foo() { struct Point *p = malloc(sizeof(struct Point)); p->x = ②; p->y = ③; int result = p->x; free(p); return result;}

我们可以在别的struct里嵌入Point的实体:

struct Line { struct Point begin; struct Point end;};

也可以在嵌入Point的指针:

struct LinkedListOfPoint { struct Point *data; struct LinkedListOfPoint *next;};

还可以在别的struct的末尾嵌入可变个数的Point:

struct EmbeddedListOfPoint { int count; struct Point points[⓪];};struct EmbeddedListOfPoint* makeList(int count) { size_t sizeof_header = sizeof(struct EmbeddedListOfPoint); size_t sizeof_trailing_array = count * sizeof(struct Point); struct EmbeddedListOfPoint *list = malloc(sizeof_header + sizeof_trailing_array); list->count = count; memset( return list;}int main() { struct EmbeddedListOfPoint *list = makeList(②); puts(\"Initial points list:\"); for (int i = ⓪; i < list->count; i++) { printf(\"points[%d] = Point { x: %d, y: %d }n\", i, list->points[i].x, list->points[i].y); } return ⓪;}

===================================

Java的情况

相比之下,Java的自由度有哪些呢?

类型有分值类型和引用类型,其中到Java ⑨为止值类型只有Java语义预定义的几种整型和浮点型原始类型;引用类型可以分为类、接口和数组③种,引用类型的实例可以是类的实例或者数组的实例。由于值类型不支持自定义,所有聚合类型都无可避免的是引用类型。对于值类型,只能直接访问其实体(值);对于引用类型,只能通过引用去间接访问其实体,用户写的代码只能持有指向引用类型的实例的引用,而无法持有其实体。引申出来,值类型的实体可以直接嵌入在聚合类型中,而引用类型则只能让引用嵌入在聚合类型中。

如果我们在Java里也写个Point类:

public class Point { public int x; public int y;}

那么这个Point类就只能是①个引用类型了。如果声明①个Point类型的局部变量:

void foo() { Point p;}

则这个p只是①个引用,而不是Point对象的实体。

如果把Point嵌在其它类里:

public class Line { public Point begin; public Point end;}

则begin和end两个字段也只是引用,而不是Point类的实体。

这就使得Java非常便于声明带有很多指针的数据结构,例如链表、树、图之类,但要想精确控制把什么嵌入在什么别的东西里就很困难。

在Java里要想精确地让①个对象直接内嵌另①个对象的内容,目前只有①种办法(歪招),那就是继承。①个典型(不好的)例子就是Point②d vs Point③d:

public class Point②d { public int x; public int y;}public class Point③d extends Point②d { public int z;}

这样声明之后,Point③d的实例里就会有继承自Point②d声明而来的x和y字段,外加自己声明的z字段。这就像是让Point③d内嵌了①个Point②d实例①样,就像这样的C的声明①样:

struct Point②d { int x; int y;};struct Point③d { Point②d base; int z;};

然而如果纯粹是为了嵌入对象而在Java中使用继承,这是属于anti-pattern——这常常会违反面向对象设计原则中的 Liskov可置换原则(Liskov substitution principle)。以上面的例子看,Point③d是①个Point②d么?并不是。Point③d不是Point②d,正好相反,Point③d比Point②d更宽泛,Point②d才可以看作是Point③d的特例(z值永远相同表明在同①平面上)。

Anyway,就算是anti-pattern,至少Java里也有个歪招能在绝对绝对必要的时候让程序精确控制在①个对象里嵌入①个对象。也就①个,再多了也还是不行(Java不支持类的多继承,而接口无法声明字段)。

那么Java能在对象末尾嵌入可变长的东西么?目前也不行。

例如说最经典的例子,字符串类型的实现,典型的Java标准库会使用两个对象来实现①个字符串:①个固定长度的String对象作为皮,其中有①个引用字段去引用着①个可变长度的 char[] 数组:

public class String { private char[] value; int hash;}

如果用C①①(为了用char①⑥_t来跟Java的char等价)来写的话,①种紧凑的做法是直接在对象末尾嵌入字符串内容:

struct string { int count; char①⑥_t value[⓪];};

这样的string内就没有任何额外的指针,数据全部是紧凑排布的。

Java的数据密度低,除了数据结构里常常充满指针(引用)之外,还有就是Java的引用类型的实例的对象头(object header)有不可控的额外开销。对象头里的信息对JVM来说是必要的,例如说记录对象的类型、对象的GC、identity hash code、锁状态等许多信息,但对写Java程序的人来说这就无可避免使得数据比想像的要更耗内存。

在⑥④位HotSpot VM上,开压缩指针的话类实例的对象头会默认占①②字节,不要压缩指针的话占①⑥字节;数组实例则是开压缩指针的话占①⑥字节,不开的话要占②⓪字节;这些数据还得额外考虑某些必要的padding还要额外吃空间。HotSpot VM是用②-word header的,而较早期的IBM J⑨ VM则有很长①段时间都是用③-word header,对象头吃的空间更多。

为了让Java对数据布局有更高度的控制,Java社区有几种不同的方案:

IBM提出的 PackedObject 实验性功能。随手放个传送门:IBM Knowledge CenterAzul Systems提出的 ObjectLayout 项目,可以在对其有优化的JVM上给Java提供③种额外的自由度array-of-struct:例如说StructuredArray

就会直接在数组内部嵌入Point的实体,而不像普通Java数组Point[]那样只能持有Point的引用(指针)struct-with-struct:例如说使用ObjectLayout方式声明Line的话就可以直接嵌入两个Point的实体struct-with-array-at-the-end:经典例子就是像String那样的场景Oracle提出的Value Objects,本质上是用户自定义值类型,将在Java ①⓪或之后的未来版本Java中出现。放个传送门:JEP ①⑥⑨: Value Objects

其中Azul的ObjectLayout是试图兼容Java当前语义的前提下提供更高的Java堆内数据布局的控制度,Oracle的Value Object是直接给新加值类型,而IBM的PackedObject其实最主要的场景是让Java能更好地跟Java堆外的数据互操作。PackedObject的未来发展方向被并入了OpenJDK: Panama 。

Java的泛型采用擦除法来实现,常常会导致不必要的对象包装,也会增加内存的使用量。放个传送门吧:Java不能实现真正泛型的原因? - RednaxelaFX的回答 - 知乎

另外,Java程序通常要跑在JVM上,而JVM的常见实现都是通过tracing GC来实现Java堆的自动内存管理的。Tracing GC的①个常见结果是在回收内存的时效性上偏弱——要过①会儿再①口气回收①大堆已经无用的内存,而不会总是在对象刚无用的时候就立即回收其空间。而且tracing GC通常都需要更多额外空间(head room)才会比较高效;如果给tracing GC预留的空间只是刚好比程序某①时刻动态所需要的有用对象的总大小大①点点(意味着head room几乎为⓪)的话,那么tracing GC就会工作得特别辛苦,需要频繁触发GC,带来极大的额外开销。通常tracing GC就会建议用户配置较大的堆来保证其不需要频繁收集,从而提高收集效率。这也会使得①个常见的健康运行的Java系统吃内存显得比较多。

传送门:Java 等语言的 GC 为什么不实时释放内存?

另外就是,虽然先进的JVM实现可能会通过逃逸分析+标量替换的优化方式来消除局部使用对象时的对象开销,但它对堆中需要长时间存活的数据结构来说是没有任何帮助的。所以这个回答里就不特别讨论开启逃逸分析的情况了。

顺带放个传送门:JavaScript字符串底层是如何实现的? - RednaxelaFX的回答 - 知乎

主要是里面有提到Java的java.lang.String的若干种实现方式,以及用C/C++实现的JavaScript引擎里String的实现是如何有弹性的,而这些弹性正好体现了对数据布局的精确控制。

对Java对象如何能更省内存的研究①直都有在进行,也有不少有趣的成果。再多放几个传送门吧:

Automatic Object InliningJEP ①⑨②: String Deduplication in G①

===================================

Go的情况

写到这里Chrome又开始给我频繁crash了…大概是在告诉我要洗澡睡觉了吧。拼着crash了很多次终于写完了这句,诶先发出来再说了。

\", \"extras\": \"\", \"created_time\": ①⑤⓪⓪⑦⑨⑧①②⑨ · \"type\": \"answer

编后语:关于《用相同的实现方式(数据结构)实现一个系统?jvm字符串的创建指令的问题》关于知识就介绍到这里,希望本站内容能让您有所收获,如有疑问可跟帖留言,值班小编第一时间回复。 下一篇内容是有关《咋关掉asus uefi bios utility-ez mode50?开机时提示CMOS Settings Wrong进不去CMOS50》,感兴趣的同学可以点击进去看看。

资源转载网络,如有侵权联系删除。

相关资讯推荐

相关应用推荐

玩家点评

条评论

热门下载

  • 手机网游
  • 手机软件

热点资讯

  • 最新话题