世间万物皆系于四箭之上
尽管这本书是一本讲述Java的书籍,但是这本书的内容却并不只是针对Java而言。而是针对计算机整个底层的规划,如何通过底层的设计来创造出合理便捷的语言。底层开发人员需要了解上层的应用而设计合理的底层结构,上层开发人员需要连接底层的结构来更好的理解程序的内部逻辑。
程序的运行流程:
编写好的Java文件,首先通过编译器编译为class字节码文件,在这个过程中,虚拟机会对类信息、变量、方法等等信息进行一个排序。之后,运行时,虚拟机会对通过字节码文件的描述,对内存进行划分,安排堆内存、占内存等等,同时通过栈对方法进行一个顺序操作执行。即编译期:先把文件转为二进制字节码,方便运行时解析。运行期:根据字节码文件分配内存,完成执行操作
第二部分:自动内存管理机制
第2章:Java内存区域与内存溢出的异常
相对于C++而言,Java的好处是开发者并不需要过多的在意内存的回收,这些都有jvm进行处理。但即便这样,也会有一个问题,过度的依赖机器,会出现未知的问题。因而,也需要开发人员做到心中有数。
线程私有区
每个线程都会独有一个
线程共享区
直接内存:共享的对外内存。对于数据传输而言,由于数据需要经常进行搬运,如果直接使用堆内存,会进行频繁的调用,因而,jdk1.4中,对于NIO的使用,指定可以直接对外部存储进行调用,大大提高了效率。但是这也会带来OOM异常,但是很多时候不容易发现,因而在使用NIO时,需要重点注意这个
2.3 虚拟机中对象的流程
对象的创建:
一下只包括New 对象,不包括数组和class对象
对象的内存布局
对象的内存分3个区域:
对象的访问定位
程序运行时通过栈上的引用来对堆中的对象进行操作,所以需要定义指针如何找到对象,一般一下两种
常见错误的归纳:
// 1.堆溢出:不断地创建对象
// java.lang.OutOfMemoryError: Java heap space
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
// 2 虚拟机栈和本地方法栈:
// 2.1 方法的递归调用
// java.lang.StackOverflowError-JavaVMStackSOF.java:20
public void stackLeak() {
stackLength++;
stackLeak();
}
// 2.2 创建太多的线程,每个线程都要单独分配
// java.lang.OutOfMemoryError: unable to create new native thread
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
// 3 运行时常量池-不断地创建字符串常量
// OutOfMemoryError: PermGen space - RuntimeConstantPoolOOM.java:18
public static void main(String[] args) {
// 使用List保持着常量池引用,压制Full GC回收常量池行为
List<String> list = new ArrayList<String>();
// 10M的PermSize在integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
// 4 方法区-需要加载的类信息过多
// Spring、Hibernate对类进行增强时,需要加载大量数类信息,会存在这种情况
// OutOfMemoryError: PermGen space-ClassLoader.java:632
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
// 5 本机直接内存
// OutOfMemoryError-DirectMemoryOOM
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* @author zzm
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
第3章:垃圾收集器与内存分配策略
3.2 对象死亡的判断
一个对象是否可以被回收,可以通过一下方法:
java中的四种引用:
对象的两次判断:
无论是可达性还是计数器,对象在回收前,会调用一次finalize方法,让然后,等待回收,如果期间再次被引用,就可以被救起。finalize方法只会调用一次。
3.3 垃圾收集算法
3.5 垃圾收集器
垃圾收集器是垃圾回收的具体实现
并行和并发的区别:
文中的上面的并行指的是所有的垃圾收集器是并行的,用户等待
并发时用户和垃圾线程并发执行,可能互相交叉
3.6 垃圾回收策略
对象优先分配在新生代Eden+一个Survivor区。
当新生代不足以分配内存时,就会发生一次Minor DC,GC后,存下来的对象放入另一个Survivor,如果内存不够,直接进入老年代。
大对象直接进入老年代。
当一个大对象放入新生代的时候,由于大的占用,会引发不断地GC,因此,为了防止这种情况,大对象直接放入老年代。但是这样也会出现另一个问题,即大对象一般存活时间短,直接放到老年代,存活时间长,对老年代也是压力。所以需要程序员合理设置最大值
长期存活的对象进入老年代
对于存活下来的对象,进行年龄统计,到达一定的年龄,也会进入老年代。如果某个年龄的总数大于对象数量的一般,也会将当前年龄的对象全部放入老年代。一般15岁。
老年代的fullGC
在发生Minor GC前,老年代会检查,可用空间是否大于新生代所有空间,如果大于,表明没有风险,可以全部容纳,就Minor GC。
再检查老年代可用空间是否大于历次晋升对象的平均大小,如果大于,就MinorGC,否则进行一次Full GC。(仍然有风险,只是概率问题)
第4章 虚拟机监控工具
4.2 命令行工具-bin目录下
// java process status 查看所有的java线程
jsp - l
// 虚拟机持续监控-每250秒对2726线程监控垃圾收集情况,总共20次
jstat -gc 2726 250 20
// java配置信息
jinfo
// java对堆栈跟踪工具-输出线程快照
jstack -l 3500
可视化工具:
第5章: 实战优化案例
第三部分:虚拟机执行子系统
第6章 类文件结构
6.3 class文件结构
class文件结构以一个字节作为单位,使用16进制表示,即两个16进制位。
6.4 字节码指令
类似于汇编的寄存器操作
第7章 类加载机制
java中,类的加载、连接及初始化都是在运行期间完成的。
周期:加载-验证-准备-初始化-卸载
初始化规定:
注意以上的一些案例
7.3 类加载过程
1加载
3件事情
2验证
验证是否符合jvm要求
3准备
为类变量分配内存,并初始化。
4解析
将常量池的符号引用转为直接引用
5初始化-开始执行类中的代码
7.4 类加载器:
类的加载需要合理的管理,否则会存在混乱,同样的名字,代表的内存却不是一样的。所以,这里使用双亲委派模型,即每个类的加载都向上传递,交由父类进行加载,如果父类加载失败,才由自身加载,防止父类和自身加载冲突。即保证只有一个被加载
第8章 虚拟机字节码执行引擎
执行引擎就是通过给定的字节码指令,执行对应的调用过程
8.2 运行时栈帧结构
一个线程会独占一个栈,栈是由多个栈帧组成,每一个栈帧相当于一个线程方法。包含了操作所需的局部变量、操作数栈、动态链接等。线程的执行过程就是方法入栈出栈的过程。栈顶栈帧叫做当前帧,即运行时帧。这些变量的内存占用在编译时就已经确定
slot复用:为了节省栈帧内存,Java局部变量表slot可以复用。即在一个方法内部,代码块中的变量生存周期只在代码块内部,如果出了代码块,即便有引用,也会被回收。但是,这种回收也是不确定的,所以,一个编码规则是:程序员需要主动对不用的对象,赋值为null
public static void main(String[] args) {
{
int[] temp = new int[2014];
}
int t = 0; // 加上这句,主动去复用,才会回收temp,否则,不回收,尽量直接使用temp=null
System.gc();
}
操作数栈:
虚拟机栈是方法调用的栈结构,操作数栈是方法内部计算的栈结构。即常见的表达式计算。操作数栈主要处理两类数据:一类是已知的基本数据的加减乘除,另一类是方法传递的加减乘除(即方法返回的结果等)
动态链接
虚拟机线程共享区有一个方法区,方法区中有一个常量池,用于存储固定的常量和方法名的引用(便于直接查找方法)。动态链接保存的就是当前栈帧需要用到的方法引用,便于运行时直接调用。
方法返回地址:
即方法调用完成后,需要恢复上层局部变量表和操作数栈,并将返回值压入操作数栈,并调用PC计数器指向下一个指令。如果运行期出现未处理异常,就会直接导致方法退出
8.3 方法调用
方法调用不同于方法执行,调用只是设定方法调用的引用,相当于把所有的方法通过链表连接起来,作为一个调用顺序。
以下为几种调用方式:
解析调用:在class文件时,方法的调用存储的只是方法的符号引用。直到类加载的解析阶段,才会将一部分方法的符号引用转化为直接引用,即最终的引用。这包括invokestatic和invokespecial,即静态方法和私有方法、实例构造器、父类方法4类;另外还有final方法。可以发现这五个类型是一种不变的类型,即方法的指引对象在运行期不会变化(这个需要注意Java多态)
解析调用是一个静态的过程,即在编译期就完全确定,不用等到运行期。
分派调用
分派调用时Java多态的一种实现,即重载和重写。分为静态分派和动态分派。
静态分派即在编译器完成分派,即直接能确定的;动态分派是指在运行期完成分派,需要运行时才知道确定类型的。
比如Human man = new Man();man变量有两个类型,分别是编译期和运行期,编译期是Human,即静态类型;运行期是Man,即实际类型。
重载:重载的参数是在编译器确定的
重写:重写的参数是在运行期确定的
比较:
// 1-静态分派-重载
public class StaticDispatch {
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
static abstract class Human { }
static class Man extends Human { }
static class Woman extends Human { }
public void sayHello(Human guy) {
System.out.println("hello, guy");
}
public void sayHello(Man guy) {
System.out.println("hello, man");
}
public void sayHello(Woman guy) {
System.out.println("hello, woman");
}
}
// output:
hello, guy
hello, guy
// 2-动态分派-重写
public class DynamicDispatch {
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
}
// outptu:
man say hello
woman say hello
woman say hello
重载的调用顺序:
当前指定参数-向上类型转换-自动装箱-接口向上-父类向上-可变长参数
类型转换:char-int-long-float-double
动态分派太过繁杂,为了便于查找,java在方法区中使用了一个虚拟机表,用于保存方法的索引,便于查找
java基于栈的指令集和汇编基于寄存器的指令集
对于1+1操作:
// 基于栈:把两个元素取出来操作,再放回
iconst_1
iconst_2
iadd
istore_0
// 基于寄存器:直接在单个元素上操作,第二个参数是确定的数
mov eax, 1
add eax, 2