【读书笔记】On Java 8 - 运算符、控制流、初始化、垃圾回收器如何工作

阅读来源 - On Java 8,如有侵权请联系删除

读书笔记中只记录关键知识点

文章目录

    • 运算符
    • 控制流
    • 初始化和清理
    • 垃圾回收器如何工作
    • 类的初始化

运算符

  1. Integer 内部维护着一个 IntegerCache 的缓存,默认缓存范围是 [-128, 127],所以 [-128, 127] 之间的值用 ==!= 比较也能能到正确的结果,但是不推荐用关系运算符比较,具体见 JDK 中的 Integer 类源码。

  2. 十六进制(以 16 为基数),适用于所有整型数据类型,由前导 0x0X 表示,后跟 0-9 或 a-f (大写或小写)。

  3. 八进制(以 8 为基数)由 0~7 之间的数字和前导零 0 表示。

  4. Java 7 引入了二进制的字面值常量,由前导 0b0B 表示,它可以初始化所有的整数类型。

  5. 使用整型数值类型时,显示其二进制形式会很有用。在 Long 型和 Integer 型中这很容易实现,调用其静态的 toBinaryString() 方法即可。 但是请注意,若将较小的类型传递给 Integer.toBinaryString() 时,类型将自动转换为 int

  6. Java 7 中有一个深思熟虑的补充:我们可以在数字字面量中包含下划线 _,以使结果更清晰。这对于大数值的分组特别有用。

     double d = 341_435_936.445_667;
    
    • 仅限单 _,不能多条相连。
    • 数值开头和结尾不允许出现 _
    • FDL的前后禁止出现 _
    • 二进制前导 b 和 十六进制 x 前后禁止出现 _
  7. Java 用 %n 实现的可以忽略平台间差异而生成适当的换行符,但只有当你使用 System.out.printf()System.out.format() 时。

  8. Java 也添加了一种“不分正负”的右移位运算符(>>>),它使用了“零扩展”(zero extension):无论正负,都在高位插入 0。这一运算符是 C/C++ 没有的。

  9. 如果移动 charbyteshort,则会在移动发生之前将其提升为 int,结果为 int

  10. 想对结果进行四舍五入,可以使用 java.lang.Mathround() 方法。

  11. Java 不需要 sizeof() 方法来满足这种需求,因为所有类型的大小在不同平台上是相同的。我们不必考虑这个层次的移植问题 —— Java 本身就是一种“与平台无关”的语言。

控制流

  1. 如果你的方法声明的返回值类型为非 void 类型,那么则必须确保每个代码路径都返回一个值。
  2. Java 7 增加了在字符串上 switch 的用法。

初始化和清理

  1. 利用构造器保证初始化
  2. 方法重载
  3. 区分重载方法:方法签名。两个 f() 方法具有相同的参数,但是参数顺序不同,根据这个就可以区分它们。
  4. 无法根据返回值重载:有些情况下编译器无法根据上下文确定调用的具体方法。
  5. 一旦你显式地定义了构造器(无论有参还是无参),编译器就不会自动为你创建无参构造器。
  6. 类的非静态方法的调用会隐式传递一个 this 对象(内部实现)。
  7. this,意味着"这个对象"或"当前对象",它本身生成对当前对象的引用。在一个构造器中,当你给 this 一个参数列表时,它是另一层意思。它通过最直接的方式显式地调用匹配参数列表的构造器。
  8. 如果用 this(param...) 的方法调用构造器,必须放在构造函数的开始,即先使用已有的构造器完成对象构造,再根据构造参数继续设置构造对象字段。
  9. 当垃圾回收器准备回收对象的内存时,首先会调用其 finalize() 方法,并在下一轮的垃圾回收动作发生时,才会真正回收对象占用的内存。所以如果你打算使用 finalize() ,就能在垃圾回收时做一些重要的清理工作。
  10. finalize() 不同于 C++ 中的析构函数,能将 finalize() 作为通用的清理方法,原因是在 Java 中对象并非总是被垃圾回收,也就是说对象可能不被垃圾回收,垃圾回收不等同于析构。可能存在的情况是只要程序没有濒临内存用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,而垃圾回收器一直没有释放你创建的任何对象的内存,则当程序退出时,那些资源会全部交还给操作系统。
  11. finalize() 的用途:对于一些本地方法如 C/C++ 中的 malloc() 进行 free
  12. 无论是"垃圾回收"还是"终结",都不保证一定会发生。如果 Java 虚拟机(JVM)并未面临内存耗尽的情形,它可能不会浪费时间执行垃圾回收以恢复内存。

垃圾回收器如何工作

  1. Java 从堆空间分配的速度可以和其他语言在栈上分配空间的速度相媲美。
  2. 垃圾回收器通过重新排列对象,实现了一种高速的、有无限空间可分配的堆模型。
  3. 一种简单但速度很慢的垃圾回收机制叫做引用计数。每个对象中含有一个引用计数器,每当有引用指向该对象时,引用计数加 1。当引用离开作用域或被置为 null 时,引用计数减 1。因此,管理引用计数是一个开销不大但是在程序的整个生命周期频繁发生的负担。
  4. 引用计数的缺点:如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。对垃圾回收器而言,定位这样的循环引用所需的工作量极大。引用计数常用来说明垃圾回收的工作方式,但似乎从未被应用于任何一种 Java 虚拟机实现中。
  5. 更快的策略中,垃圾回收器并非基于引用计数。它们依据的是:对于任意"活"的对象,一定能最终追溯到其存活在栈或静态存储区中的引用。这个引用链条可能会穿过数个对象层次,由此,如果从栈或静态存储区出发,遍历所有的引用,你将会发现所有"活"的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是该对象包含的所有引用,如此反复进行,直到访问完"根源于栈或静态存储区的引用"所形成的整个网络。你所访问过的对象一定是"活"的。注意,这解决了对象间循环引用的问题,这些对象不会被发现,因此也就被自动回收了。
  6. Java 虚拟机采用了一种自适应的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的 Java 虚拟机实现。其中有一种做法叫做停止-复制(stop-and-copy)。顾名思义,这需要先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。另外,当对象被复制到新堆时,它们是一个挨着一个紧凑排列,然后就可以按照前面描述的那样简单、直接地分配新空间了。当对象从一处复制到另一处,所有指向它的引用都必须修正。位于栈或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想象成一个表格,将旧地址映射到新地址)。
  7. 这种所谓的"复制回收器"效率低下主要因为两个原因。其一:得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间。某些 Java 虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。
  8. 其二在于复制本身。一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。为了避免这种状况,一些 Java 虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种模式(即"自适应")。这种模式称为标记-清扫(mark-and-sweep),Sun 公司早期版本的 Java 虚拟机一直使用这种技术。对一般用途而言,"标记-清扫"方式速度相当慢,但是当你知道程序只会产生少量垃圾甚至不产生垃圾时,它的速度就很快了。
  9. "标记-清扫"所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。但是,每当找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。"标记-清扫"后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。
  10. "停止-复制"指的是这种垃圾回收动作不是在后台进行的;相反,垃圾回收动作发生的同时,程序将会暂停。在 Oracle 公司的文档中会发现,许多参考文献将垃圾回收视为低优先级的后台进程,但是早期版本的 Java 虚拟机并不是这么实现垃圾回收器的。当可用内存较低时,垃圾回收器会暂停程序。同样,"标记-清扫"工作也必须在程序暂停的情况下才能进行。
  11. 如前文所述,这里讨论的 Java 虚拟机中,内存分配以较大的"块"为单位。如果对象较大,它会占用单独的块。严格来说,"停止-复制"要求在释放旧对象之前,必须先将所有存活对象从旧堆复制到新堆,这导致了大量的内存复制行为。有了块,垃圾回收器就可以把对象复制到废弃的块。每个块都有年代数来记录自己是否存活。通常,如果块在某处被引用,其年代数加 1,垃圾回收器会对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象仍然不会复制(只是年代数会增加),含有小型对象的那些块则被复制并整理。Java 虚拟机会监视,如果所有对象都很稳定,垃圾回收的效率降低的话,就切换到"标记-清扫"方式。同样,Java 虚拟机会跟踪"标记-清扫"的效果,如果堆空间出现很多碎片,就会切换回"停止-复制"方式。这就是"自适应"的由来,你可以给它个啰嗦的称呼:"自适应的、分代的、停止-复制、标记-清扫"式的垃圾回收器。
  12. Java 虚拟机中有许多附加技术用来提升速度。尤其是与加载器操作有关的,被称为"即时"(Just-In-Time, JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快。当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 .class 文件,然后将该类的字节码装入内存。你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。另一种做法称为***惰性评估***,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快。

类的初始化

  1. 局部变量如果没有初始化会产生编译错误,而对于类的成员变量会进行自动初始化(基本类型为对应类型的零值,引用类型为 null)。自动初始化的进行,会在构造器被调用之前发生。
  2. 类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。
  3. 静态初始化只有在必要时刻才会进行,初始化的顺序先是静态对象(如果它们之前没有被初始化的话),然后是非静态对象。
  4. 静态初始化只会在首次加载 Class 对象时初始化一次。
  5. Java 提供了被称为实例初始化的类似语法,用来初始化每个对象的非静态变量,类似于 static 代码块,只是没有 static 关键字修饰,可以使用它保证某些操作一定会发生,而不管哪个构造器被调用。
  6. 可变参数列表使得方法重载更加复杂了。

你可能感兴趣的:(java基础,on,Java,8)