JVM 运行时数据区域

简介

要去了解 Java 虚拟机,我们必须要先了解它的组成。那么 Java 虚拟机是如何定义运行时数据区域的呢?在运行时数据区域中在《Java 虚拟机规范(Java SE 7)》规定下主要为:
1. 程序计数器
2. Java 虚拟机栈
3. 本地方法栈
4. Java 堆
5. 方法区
6. 运行时常量池
7. 直接内存

程序计数器

程序计数器(Program Counter Register)是一块很小的内存,计算机科学中,计数器作为指令位置的一个行号指示器,取出当前的指令后,计数器会加一指向下一条指令。Java 的指令,分支,循环,跳转,异常处理,线程恢复等都依赖于程序计数器。多线程时,每个线程都会有各自独立的程序计数器。

当执行 Java 方法时,如果为一个 Native 修饰的方法,则计数器的值为空(Undefined)

Java 虚拟机栈

Java 虚拟机栈(Java Virtual Marchine Stacks)又被粗糙地称为栈内存是线程私有的,它的生命周期与线程相同。JVM 栈描述了 Java 方法执行的内存模型,每一次方法调用都会创建一个栈帧(Stack Frame)用于保存局部变量表、操作数栈、动态链接、方法出口等信息。方法执行与结束的过程对应着JVM 栈的入栈和出栈过程。

局部变量表存放了Java的基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型, 一个对象句柄或者对象指针又或者对象位置)、returnAddress 类型 (一条字节码的地址)。一个局部变量空间大致为32位。局部变量表的空间不会动态分配。

本地方法栈

本地方法栈(Native Method Stack)Java 虚拟机栈通常是一致的,不过为 Native Method 服务,在 Sun HotSpot VM 中,与Java 虚拟机栈合二为一

Java 堆

Java堆(Java Heap)是 JVM 管理的最大的,被所有线程共享的内存区域,用来保存大多数的对象实例,但这不是“绝对”的。Java 的垃圾回收集中在 Java 堆上,因此又被称为 GC 堆(Garbage Collected Heap)。

从内存回收的角度来看,由于现在收集器基本都采用分代回收算法,所以 Java 堆中分为:新生代和老年代。细致地可以分为:Eden 空间、From Survivor 空间、To Survivor 空间。
从内存分配角度来看,Java堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。

可以通过 -Xmx -Xms 来控制 Java 堆的大小。

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,用于储存已经被虚拟机加载的类信息、常量、静态变量、即时翻译器编译后的代码等数据。为了区别方法区和堆通常,方法区有 Non-Heap 的别名。

通常不会对方法区进行内存回收因此又被称为(Java 堆的永久代),但这个区域是会被回收的。对这个区域的 GC 一般是常量池的回收和类型的卸载。

方法区空间的大小会受到 -XX:MaxPermSize 的影响。

运行时常量池

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

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,但也是一块重要的内存区域,避免了使用 NIO(New Input/Output)时,在Java 堆与Native 堆之间的数据交换。DirectByteBuffer 对象作为这块内存的引用。

了解完 JVM 运行时的内存区域,我们能够更加深入地去了解 JVM 中各区域的运行细节!

什么是框架?

引言

框架这个词在许多的地方都能见到。在给出框架是什么的定义之前,我们会讨论骨架 、框架 和架构 这三个常见的且容易混淆但又相似的术语。在实际中,我们通常会用模式或者框架又或者架构来概括这三个术语。
回溯到尚未出现软件工程之前 ,我们能够想到当时的编程环境是“天堂”又是“地狱”。软件作为一种著作物,附带版权的保护减少了软件代码被他人修改使用的可能,阻碍了软件代码的传播,也阻碍了软件行业的发展。软件作为一种思维产物,必然带有主观性。软件作者的思维直接影响到了软件自身的质量。同时,为了复用积累下来的代码,软件作者才会对复用的代码作一定的改进。这在一定程度上对常用的代码进行了维护和复用,虽然这不是框架出现的标志,但这是框架出现的起点。

复用为本

复用是框架的基础和目的。若一个框架没有达到复用的目的,那么,我们不能称之为框架。代码的复用阶段达到相当的规模时则可以简而言之框架或伪框架。通常复用会构成库和实用性工具。“库”作为一种思维抽象而存在,描述了软件中某一部分需要完成的工作。实用性工具对库的使用进行了简要但又是实用的复用,通常把这种行为称为封装。当这些库和实用工具在经过一次又一次的重复迭代下得到稳定性的保证后,软件作者很大程度上会对其所做的软件共同的流程逻辑作一次更高层面的复用。这时候骨架就诞生了。骨架的诞生使得软件作者在项目接下来的开发中能够更加充分地集中于软件开发本身。

什么是骨架

骨架是一个软件的基本构成部分。在软件中,骨架更普遍存在在软件执行流程中。因此,骨架更像是软件共同逻辑(例如用户登录动作,用户注册动作被抽象为动作处理骨架)的抽象复用。骨架的作用使得我们能够较为轻易地构造一些传统上较为复杂的软件。例如,管理系统。管理系统通常是由较为固定的步骤构成的(例如CRUD),这些步骤抽象成一个整体的骨架能够提高代码的可维护性。同时由于骨架的存在我们可以关注在管理系统的自身,如用户数据,被管理数据,以及相关的业务操作流程。这些操作流程也可以继续进一步以一个特定的骨架而存在。这样一个个骨架能够以描述性的方式把软件从脑海中描述出来。我们可以使用流程图达到这样的目的。另外,由于骨架的简单性,我们可以在需要的时候进行较多的扩展。在这个时候软件作者更多地通过钩子(Hook)的形式来提供,即使用模板模式 。骨架在这个时候可以被看作一个单骨架的框架。了解一个单独的骨架总比多个骨架简单得多,这是一些大项目使用小框架的原因之一。总而言之,骨架具有简单性,骨架的诞生是为了对特定流程的复用;骨架具有可扩展性,可扩展性和简单性一般是关联而存在的。通过极为简单的钩子,我们可以定义一些具有特别用途的操作例如事件发布,外置插件(Plugin)等等。当一个骨架能够以一种更为通用的形式给软件带来一定的简化时,框架就出现了。

什么是框架

框架是一个软件的蓝图轮廓或者蓝图的一部分。蓝图中具体的内容能够通过骨架提供较为普遍的方案。这样能够使得“蓝图的内容”是能够被维护的。和骨架相同,框架也是为了让软件作者关注软件作者需要关注的地方,而无需对关注点以外的事情付出精力。和骨架不同,框架致力于简化软件开发,使得软件开更像一个搭积木的过程。而骨架是为了降低软件开发的复杂性把流程进行复用。框架在骨架的基础上对软件构建进行了简化,这些简化通常包含骨架层面上的简化,第三方扩展支持的简化,编码规范上的简化等等。骨架作为流程逻辑的重用,在框架中作为主要部分。这里值得说明的是,框架的大小在某些程度上并没有大小之分,仅仅是骨架和扩展涉及的范围的宽广问题。
由于涉及的范围过大而导致框架必须发生改变。这些连带作用导致了框架变得复杂和难以看到全貌。就是这样,有了小项目用大框架,大项目用小框架这个说法。对于小型项目来说,小项目并不需要在深层次了解和全局控制框架自身。因此常常为了节约不必要的框架和插件维护而选择经起考验的大框架和第三方扩展。这样一来项目只需要更加关注在自身的开发。这样节约了时间,节约了维护成本,但这些小项目通常是不考虑软件依赖的风险。而对于大项目来说,框架的本身是需要的,但框架的绝大多数骨架和扩展又是不必要的。这种情况通常会被称为冗余。为什么是不必要的呢?大项目必须要考虑到后续的开发。
骨架越多,框架就越复杂。扩展越多,需要处理的依赖风险就越多。在这个复杂的情形下,越是需要维护的软件,依赖的风险越是不可控,例如依赖被废弃对项目会有不必要的危急影响,这个影响犹如四年前的软件依赖突然需要升级最新的版本到现在一样。涉及范围广的框架所提供的骨架和扩展功能与项目自身无关,浪费了项目实际关注点的精力。这些精力对于大型项目来说是极其关键的。灵感是不稳固的,不要要求灵感能够一直留在脑海中。常常会有过度设计的事情发生,不管是笔者还是其他人。我们很难避免过度设计,这些过度设计在某些程度上是软件作者对软件发展的预测。对于小型项目,它更加需要快速开发,后续维护较少。而对于大型项目需要各个部件的密切配合甚至给扩展预留空间,往后又是需要频繁的维护和需求变更。这就要求大型项目需要承受更多的挑战。想要项目承受得起挑战,软件作者必须要确保每一部分都是简单的。

架构与框架

架构是一个用于描述整体的宏图。架构一般描述了整个应用的大体部分,如在Web 中的Frontend-Middle-Backend架构中有C/S 架构和B/S架构两种形式。这两个架构中把服务端作为后端,把HTTP 作为中间件,把客户端和浏览器作为前端。这就是架构的作用,无需注意到Web应用的具体。而集中在相应的架构层。通常我们也会在框架中使用架构来描述框架自身是如何设计的。

总结

那么框架应该是什么?框架是一个以复用为基础和目的,以骨架为主要内容的,以简化应用开发为核心的设计基础。

Java 历史及特性

总结自:深入理解Java虚拟机:JVM高级特性与最佳实践 第2版

Java 历史

年份 纪事
1991年4月 James Gosling 的 Green Project 开始执行,该“绿色项目”的结果是 Oak(Java 前身)。
1995年5月23日 Oak 改名为 Java 并在 SunWorld 发布 Java 1.0 提出 “Write Once, Run Anywhere(一次编写到处执行) ”的口号
1996年1月 Sun Classic VM 发布
1996年4月 10个主要操作系统供应商嵌入 Java 技术,大约 8.3 万网站使用了 Java, 首次举行 JavaOne 这个后来的 Java 技术大会
1997年2月19日 JDK1.1发布,添加了新技术和Java语法
1997年2月19日 ~ 1999年4月8日 JDK1.1.0 ~ JDK 1.1.8发布: 1.1.4-Sparkler, 1.1.5-Pumpkin, 1.1.6-Abigai, 1.1.7-Brutus, 1.1.8-Chelsea
1998年12月4日 里程碑式版本 JDK 1.2-Playground 发布,分为三个版本:J2MEJ2SEJ2EE
1999年3月 JDK1.2.1
1999年7月 JDK 1.2.2
1999年4月27日 HotPot VM 发布,JDK 1.2 可选 VM, JDK 1.3 后默认的 VM 。
2000年5月8日 JDK 1.3-Kstrel, 对数学运算作出了改进,JNDI 作为平台服务,基于 CORBA IIOP 实现 RMI 通讯协议, Java 2D API,Timer API改进以及添加了 JavaSound 库
2001年5月17日 JDK-1.3.1-Ladybird
2002年2月13日 JDK-1.4-Merlin 发布,这是第一个走向成熟的版本。
2002年9月16日 JDK-1.4.1-Grasshopper
2003年6月26日 JDK-1.4.2-Mantis
2004年9月30日 JDK-1.5-Tiger 语法易用性上的改进,以及改进内存模型,提供了 java.util.concurrent 并发包。最后一个支持 Windows 9.x 平台的 Java。
2006年12月11日 JDK-1.6-Mustang 结束 J2SE、J2EE、J2ME 的命名, 启用 Java SE 6、 Java EE 6、Java ME 6 命名方式。
2006年11月13日 OpenJDK 初现。
2009年2月19日 JDK-1.7-Dolphin 第一个JDK 1.7规划的里程碑版本发布。于 2010年9月9日 所有的JDK 1.7 里程碑才完成。没有按计划完成
2012年10月16日 在 JDK 1.6发布后,Sun 公司,由于代码复杂性、JDK 开源、开发 JavaFX、经济危机、Sun 公司收购案等原因,Sun 公司被拖累。无法再进行两年一主版本。 JDK 1.6 一共发布了 37个更新

Java 特性

JDK 1.0

  1. JVM
  2. Applet (网页小程序)
  3. AWT

JDK 1.1

  1. *.jar 文件格式
  2. JDBC
  3. JavaBeans
  4. RMI
  5. 内部类 (Inner Class)
  6. 反射(Reflection)

JDK 1.2

  1. EJB
  2. Java Plug-in
  3. Java IDL
  4. Swing
  5. JIT
  6. strictfp 关键字
  7. Collections 集合类

JDK 1.3

  1. Timer API
  2. 平台级 JNDI
  3. 基于 CORBA IIOP 实现的 RMI
  4. 改进的 Java 2D
  5. JavaSound

JDK 1.4

  1. 正则表达式
  2. 异常链
  3. NIO
  4. 日志类
  5. XML 解释器
  6. XSLT 转换器

JDK 1.5

  1. 自动装箱
  2. 泛型
  3. 动态注解
  4. 枚举
  5. 可变长参数
  6. 遍历循环(foreach 循环)

JDK 1.6

  1. 启用动态语言支持(Mozilla JavaScript Rhino)
  2. 提供编译API
  3. 微型HTTP服务器 API
  4. 锁与同步改进
  5. 垃圾收集算法改进
  6. 类加载算法改进

JDK 1.7

  1. G1收集器
  2. 非Java语言调用支持(JSR-292)
  3. 升级类加载架构

我给自己写的名言

我们总是无法避免未知。重要的是如何对待未知。(2018/5/2)
事情已经发生现实已不可逆转,所能做的就只不过是让自己未来不再糊涂不再后悔。(2018/9/9)
班级干部是一种用于负责任并用于出气的吸血蚂蚱。(2018/9/16)
优秀的潜质在于对自己所处的环境以及周围变化的了解。(2018/9/17)
活着不是为了谁,也不是为了什么,仅仅是为了解脱自己。(2018/10/18)
历史总是惊人地相似,未来总是频繁地改变。不再重蹈覆辙,取决于你的努力程度!(2018/11/3)
节约时间的唯一方法是专注,计划只能帮助你专注。(2018/11/3)
什么都会什么都不会,什么都会是无能的特征。(2018/11/11)
只要察觉了问题,一切都将解决。(2018/11/30)
拖延意味着任务还没有完成,甚至还没有开始。(2018/12/4)
期限会在最后一天给你亲切的恶意,你有机会改变局面,但又让你感觉到无力。(2018/12/5)
一切事物都会朝着熵增的方向,失去控制?还是全力管制?(2018/12/9)
对事物开始的期望,对事物结束的失望。(2018/12/10)
所有到来的一切都是有原因的,但最终都归结于自身的失败。(2018/12/25)
结束是另外一个开始,永无止境的开始,永无止境的结束。(2018/12/25)
琐碎的事情是一切时间不够的根源,小麻烦滚成大麻烦偷走你的时间。(2018/12/26)
相信客观证据告诉你的话语,而不是一时的不合预期的全盘否定。(2019/7/10)
因为坚信着可能性,所以没有放弃自身的可能性。(2019/8/9)
努力的力量来源于身边重要的人由衷相信的期望。(2019/8/9)
纠结之人莫纠结。无意义的纠结不会给你带来任何有意义的结果。(2019/9/17)
宣誓向正确的方向而努力,痛苦而坚持,疲倦而不敢忘。(2019/10/13)
人性慵懒不可相信拖延之词,重蹈覆辙不断,离开原处如何?(2019/12/12)
如何度过这小小的一生?分清是非,追求可能性,体验丰富的生活,不沉沦于过去,不迷失掉自我。(2019/12/13)

页码数字计数的秘密

问题描述


一本书的页码从自然数 1 开始顺序编码直到自然数 n。书的页码按照通常的习惯编排,每个页码都不含多余的前导数字0。例如,第 6 页用数字 6 表示,而不是 06 或 006 等。数字计数问题要求对给定书的总页码 n ,计算出书的全部页码中分别用到多少次数字0,1, 2,…,9。

问题要点


1.页码的数字不含前导的零
2.对每页的数字包含的数字进行计数

问题分析


问题的处理对象其实就只有个位的数字,也就是说我们把数字用 10 求余就可以取得个位的数字,接着除以 10 就可以取下一位的数字。

基于这样的分析我们用 C++ 语言写出了以下代码:

#include <iostream>
typedef unsigned long long ull;
using namespace std;

int n;
//用于统计数字出现次数
ull count[10];

//用于统计一个数字 n 中的数字出现次数
//并添加到 count 数组中
void count_num(int n);

int main()
{
    while (cin >> n) {
        for (int i = 0; i < 10; i++) {
            count[i] = 0;
        }
        for (int i = 1; i <= n; i++) {
            count_num(i);
        }
        for (int i = 0; i < 10; i++)
            cout << i << ":" << count[i] << " ";
        cout << endl;
    }
    return 0;
}

void count_num(int n) {
    int low;
    while (n != 0) { // n 总是大于 0
        low = n % 10;
        count[low]++;
        n /= 10;
    }
}

复杂度如下:

项目 复杂度
n × m O(nm)
input O(n)
output O(n)
合计 O(nm)

但这样做如果我们需要计算 n = 109 的话,O(n2)=1018一台普通的计算机大约需要 2 分钟才算完。我们还有办法吗?问题伊始就说道不需要前导的零,这个是不是暗示呢?不管怎样,让我们试着往这方面思考。

行是知之始,知是行之成。——陶行知

补零会有什么结果呢,或者说为什么要这样做呢?会不会是补零后,我们会有另外的发现呢?带着疑问,我们对数字加了零:

01 02 03 04 05 06 07 08 09 10

0:10 1:2 2~9:1

这样做似乎没什么规律,如果把零去掉,并把 10 去掉

1 2 3 4 5 6 7 8 9

0:0 1~9:1

这样看来好像也没什么规律,如果前面加一个零呢?

0 1 2 3 4 5 6 7 8 9

0~9: 1

这样每个数字都加一就可以了!在细心一点看一看会发现,这都是一位数,如果是两位呢?

00 01 02 03 04 05 06 07 08 09
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
………………………..
80 81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98 99

0~9: 20

这样的话,再由此推到三位数呢?

000 001 002 003 004 005 006 007 008 009
010 011 012 013 014 015 016 017 018 019
…………………………………
100 101 102 103 104 105 106 107 108 109
110 111 112 113 114 115 116 117 118 119
…………………………………
580 581 582 583 584 585 586 587 588 589
590 591 592 593 594 595 596 597 598 599
…………………………………
980 981 982 983 984 985 986 987 988 989
990 991 992 993 994 995 996 997 998 999

0~9:300

如果继续推到四位数,五位数呢?把位数和上面的结果联系起来,我们可以得出这个式子:

出现次数 = 位数 × 10(位数 – 1)

这是巧合吗?实际上,我们可以以递归的形式来解释这个式子

f(1)=1
f(n)=f(n-1) * 10 + 10(n – 1)

这说明两位数是一位数的 10 倍加上十位数上每个数字出现了10次,三位数如此类推。
关于详细的证明过程,这里不累赘了。

我们为什么要推出数字出现次数与所有相关位数的关系呢?我们企图从直观上毫无规律之中找出规律。这些规律必然会带有各种条件,既然我们有了一个所有相关位数都的出现次数是一样的规律,那么我们如何使用这个规律呢?

如果我们把求到了的所有相关位数的数字出现次数减去额外补的零的个数那么我们就得到了准确的每个数字出现的次数。我们在处理问题的时候,可以一步一步地接近我们想得到的。我们按照上面推出所有相关位数的数字出现次数的思路可以得出第 n 位的补零次数为:

f(1) = 1
f(n) = 10 * f(n – 1)

接下来便是如何根据特定的自然数 n 来计算 0~9 各个数字出现的次数。我们来考察一个随意的数字 156 :

000 001 002 003 004 005 006 007 008 009
010 011 012 013 014 015 016 017 018 019
…………………………………
100 101 102 103 104 105 106 107 108 109
110 111 112 113 114 115 116 117 118 119
…………………………………
150 151 152 153 154 155 156

我们不能直接通过位数来得到正确的答案,但发现 156 大于 100,意味着我们可以当作两位数 (00~99) 来计算一遍,结果为 0~9:20 ,那么我们就只剩下:

000 001 002 003 004 005 006 007 008 009
010 011 012 013 014 015 016 017 018 019
…………………………………
100 101 102 103 104 105 106 107 108 109
110 111 112 113 114 115 116 117 118 119
…………………………………
150 151 152 153 154 155 156

可以知道,我们少算 100 个零。另外,我们可以知道总共多出的零的个数为 111 (100 + 10 + 1) 个。接下来就需要计算 100~156 的部分,接下来会表示得详细一点。

100 101 102 103 104 105 106 107 108 109
110 121 112 113 114 115 116 117 118 119
120 121 122 123 124 125 126 127 128 129
130 131 132 133 134 135 136 137 138 139
140 141 142 143 144 145 146 147 148 149
150 151 152 153 154 155 156

我们稍微观察一下便可以发现如果去了最高位,那么就会得到 5 个一位的情况。
(待续)

文章约定

0b0001内容
0b0010格式
0b0011注意
0b0100著作

0b0001 内容

每个分类都会有相应的 “目录” 通过目录你可以看到你感兴趣的内容。每一篇文章都会尽可能地遵循 top-down 思路。

top-down:先讲述 Why 再讲述 How。

0b0010 格式

留意本篇文章的格式。

0b0011 注意

本人无意侵犯你的权利,如果在某些情形下没有注明作者请联系我。

0b0100 著作

在一般情况下,我没有在文章顶头以引用形式显示作者信息时即认为
是本人著作。引用通常不是作者著作。

2018-03-20