目录
自动内存管理机制
Java内存区域与内存溢出异常
一、运行时数据区域
二、虚拟机中对象的创建
三、内存泄漏和内存溢出
垃圾收集器与内存分配策略
一、概念
二、如何判断对象“已死”
三、垃圾收集算法
四、垃圾收集器
虚拟机性能监控与故障处理工具
一、JDK的命令行工具
二、JDK的可视化工具
虚拟机执行子系统
类文件结构
一、相关
二、Class类文件的结构
虚拟机类加载机制
一、相关
二、类加载的时机
三、类加载的过程
四、类加载器
虚拟机字节码执行引擎
一、相关
二、运行时栈帧结构
三、方法调用
类加载及执行子系统的案例
一、Tomcat的类加载器架构
二、字节码生成技术与动态代理的实现
程序编译与代码优化
早期(编译期)优化
一、相关
二、Javac编译器
三、Java语法糖
晚期(运行期)优化
一、相关
二、HotSpot虚拟机内的即时编译器
三、 编译优化技术
四、Java与C/C++的编译器对比
1. 程序计数器
2. 虚拟机栈
3. 本地方法栈
4. 堆
5. 方法区
6. 运行时常量池
7. 直接内存
1. 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2. 在类加载检查通过后,接下来虚拟机将为新生对象分配内存;
3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4. 接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头之中。
5. 执行new指令之后会接着执行
1. 概念
2. Java堆溢出
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List list = new ArrayList();
while(true) {
list.add(new OOMObject());
}
}
}
//结果:java.lang.OutOfMemoryError: Java heap space
3. 虚拟机栈和本地方法栈溢出
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
//结果:java.lang.StackOverflowError
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
//结果:java.lang.OutOfMemoryError: unable to create new native thread
4. 方法区和运行时常量池溢出
public class RuntimeConstantPoolOOM {
List list = new ArrayList();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
//结果:OutOfMemoryError: PermGen space
1. 为什么需要了解GC和内存分配?
2. Java堆和方法区这部分内存的分配和回收是动态的,垃圾收集器所关注的是这部分内存。
1. 引用计数算法
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
//占内存
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
2. 可达性分析算法
3. 引用
4. 对象死亡过程
5. 回收方法区
1. 标记-清除算法
2. 复制算法
3. 标记-整理算法
4. 分代收集算法
1. Serial收集器
2. ParNew收集器
3. Parallel Scavenge收集器
4. Serial Old收集器
5. Parallel Old收集器
6. CMS收集器
7. G1收集器
1. 当应用程序部署到生产环境后,无论是直接接触物理服务器还是远程Talnet到服务器上都可能会收到限制,借助tools.jar类库里面的接口,可以直接在应用程序中实现功能强大的监控分析功能。
2. jps
3. jstat
4. jinfo
5. jamp
6. jhat
7. jstack
8. HSDIS
1. JConsole
2. VisualVM
1. 越来越多的程序语言选择了与操作系统和集器指令集无关的、平台中立的格式作为程序编译后的存储格式。
2. 与平台无关的理想最终实现在操作系统的应用层上:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的一次编写,到处运行。
3. 可以运行在Java虚拟机上的语言:Clojure、Groovy、JRuby、Jython、Scala等。
4. 实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机不和任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
1. 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来,类或接口并不一定都得定义在文件里,因为也可以通过类加载器直接生成。
2. Class文件是一组以8位字节为基础单位的二进制流;
3. Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
1. 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
2. Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。
1. 类的生命周期
2. 其中1-5阶段的顺序是确定的,而解析阶段则不一定,解析阶段也可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定。
3. 5种情况必须对立即对类进行初始化
注:这5种场景的行为称为对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化,成为被动引用。
4. 被动引用举例
public class SuperClass {
static {
System.out.println("SuperClass init")
}
public static int value = 123;
}
public class SubClass extends SuperClass{
static {
System.out.println("SubClass init")
}
}
public class Main {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
//输出结果:
//SuperClass init
//123
注:对于静态字段,只有直接定义这个字段的类才会被初始化。
public class SuperClass {
static {
System.out.println("SuperClass init")
}
}
public class Main {
public static void main(String[] args) {
SuperClass[] sc = new SuperClass[10];
}
}
//输出结果:无
注:该代码触发了另外一个名为“[L包名.SuperClass”的类的初始化阶段,这个类代表了一个元素类型为包名.SuperClass的一维数组。
public class SomeClass {
static {
System.out.println("SomeClass init");
}
public static final String HELLO_WORLD = "hello world";
}
public class Main {
public static void main(String[] args) {
System.out.println(SomeClass.HELLO_WORLD);
}
}
注:如上面第1种情况所描述:被final修饰、已在编译期把结果放入常量池的静态字段不会触发该类的初始化。
5. 接口的初始化过程与类的初始化过程有细微差别,在第3种情况:接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化,如引用接口中定义的常量。
1. 加载
注1:其中第1条包括这样一些方式:
从ZIP包中获取(JAR、EAR、WAR);
从网络中获取(Applet);
运行时计算生成(动态代理技术);
由其他文件生成(JSP);
从数据库中获取;
注2:数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的,一个数组类的创建过程遵循一下规则:
2. 验证
3. 准备
4. 解析
5. 初始化
public class Test {
static class Parent {
public static int A = 1;
static {
System.out.println(A); //1
A = 2;
//System.out.println(B); //编译失败
B = 2; //能访问到,但是赋值没成功
}
public static int B = 1;
}
static class Sub extends Parent {
public static int C = A;
public static int D = 1;
}
public static void main(String[] args) {
System.out.println(Sub.A); //2
System.out.println(Sub.B); //1
System.out.println(Sub.C); //2
System.out.println(Sub.D); //1
}
}
static class DeadLoopClass {
static {
try {
if (true) {
System.out.println(Thread.currentThread() + " init DeadLoopClass " + System.currentTimeMillis());
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + " start " + System.currentTimeMillis());
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + " stop " + System.currentTimeMillis());
}
};
new Thread(runnable, "A").start();
new Thread(runnable, "B").start();
}
}
1. 将类加载阶段中的“通过一个类的全限定名来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获得所需要的类,这个动作的代码模块成为“类加载器”。
2. 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,相等包括:代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果和使用instanceof关键字做对象所属关系判定的情况。
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = null;
try {
obj = myLoader.loadClass("ClassLoaderTest").newInstance();
} catch (Exception e) {
e.printStackTrace();
}
//obj由自定义类加载器加载,ClassLoaderTest由系统应用程序类加载器加载
System.out.println(obj instanceof ClassLoaderTest); //false
}
}
3. 类加载器的双亲委派模型
1. 虚拟机的执行引擎是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。
2. 在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地码执行)两种选择,也可能两者兼备,甚至还可能会包括几个不同级别的编译器执行引擎,但从外观上看来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
1. 栈帧
2. 局部变量表
3. 操作数栈
4. 动态连接
5. 方法返回地址
1. 方法调用阶段唯一的任务就是确定被调用方法的版本。
2. 静态解析:所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。这些方法包括:静态方法、私有方法、实例构造器(
public class Main {
public static void sayHello() {
System.out.println("hello");
}
public static void main(Stringp[] args) {
Main.sayHello();
}
}
3. 静态分派
public class Overload {
static class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human human) {
System.out.println("human");
}
public void sayHello(Man man) {
System.out.println("man");
}
public void sayHello(Woman woman) {
System.out.println("woman");
}
public static void main(String []args) {
Human human = new Human();
Human man = new Man();
Human woman = new Woman();
Overload o = new Overload ();
o.sayHello(human);
o.sayHello(man);
o.sayHello(woman);
}
}
4. 动态分派
public class Override {
static class Human {
public void sayHello() {
System.out.println("human");
}
}
static class Man extends Human {
public void sayHello() {
System.out.println("man");
}
}
static class Woman extends Human {
public void sayHello() {
System.out.println("woman");
}
}
public static void main(String []args) {
Human human = new Human();
Human man = new Man();
Human woman = new Woman();
human.sayHello();
man.sayHello();
woman.sayHello();
}
}
5. 单分派和多分派
public class Dispatch {
static class QQ {}
static class _360 {}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father {
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
father.hardChoice(new _360());
father.hardChoice(new QQ());
Father son = new Son();
son.hardChoice(new _360());
son.hardChoice(new QQ());
Son son1 = new Son();
son.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
6. 动态类型语言支持
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
class Test {
class GrandFather {
void thinking() {
System.out.println("i am grandfather");
}
}
class Father extends GrandFather {
void thinking() {
System.out.println("i am father");
}
}
class Son extends Father {
void thinking() {
try {
MethodType mt = MethodType.methodType(void.class);
//JDK1.7
//MethodHandle mh = MethodHandles.lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
//JDK1.8
Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
IMPL_LOOKUP.setAccessible(true);
MethodHandle mh = ((MethodHandles.Lookup) IMPL_LOOKUP.get(null)).findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
mh.invoke(this);
} catch (Throwable e) {
}
}
}
public static void main(String[] args) {
(new Test().new Son()).thinking();
}
}
1. 一个功能健全的Web服务器,要解决如下问题:
2. Tomcat目录结构中,有4组目录可以存放Java类库:
注:其中WebApp类加载器和JSP类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个JSP类加载器。
1. java.lang.reflect.Proxy或实现java.lang.reflect.InvocationHandler接口。
2. Spring中如果Bean是面向接口编程,那么在Spring内部都是通过动态代理的方式来对Bean进行增强的。
3. 动态代理中所谓的“动态”是针对使用Java代码实际编写了代理类的“静态”代理而言的,优势不在于省去了编写代理类的工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活地重用于不同的应用场景之中。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
interface IHello {
void sayHello();
}
static class Hello implements IHello {
@Override
public void sayHello() {
System.out.println("hello");
}
}
static class DynamicProxy implements InvocationHandler {
Object originalObj;
Object bind(Object originalObj) {
this.originalObj = originalObj;
return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("welcome");
return method.invoke(originalObj, args);
}
}
public static void main(String[] args) {
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
1. Java的编译期
2. 虚拟机设计团队把性能的优化集中到了后端的即时编译器中,这样可以让那些不是由javac产生的Class文件也同样能享受到编译器优化所带来的好处。
3. javac编译器做了许多针对Java语言编码过程的优化措施来改善程序员的编码风格和提高编码效率,如语法糖。
1. javac编译器是由Java语言编写的程序。
2. 编译过程大致有3个步骤:
1. 泛型与类型擦除
public class Main {
public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "你好");
System.out.println(map.get("hello"));
}
}
//编译为class文件后反编译
public class Main {
public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "你好");
System.out.println((String) map.get("hello"));
}
}
2. 自动装箱、拆箱与遍历循环
public class Main {
public static void main(String[] args) {
List list = Arrays.asList(1, 2, 3, 4);
//List list = [1, 2, 3, 4]; //JDK1.8
int sum = 0;
for (int i : list) {
sum += i;
}
System.out.println(sum);
}
}
//编译为class文件后反编译
public class Main {
public static void main(String[] args) {
List list = Arrays.asList(new Integer[] {
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4)
});
int sum = 0;
for (Iterator localIterator = list.iterator(); localIterator.hasNext();) {
int i = ((Integer) localIterator.next()).intValue();
sum += i;
}
System.out.println(sum);
}
}
注:加减乘除运算的时候,引用类型会自动拆箱为基本类型;“==”运算的时候,如果是两个引用类型,则比较的是地址,是否同一个对象引用,如果是一个引用类型一个基本类型,则引用类型会自动拆箱为基本类型,比较的是值是否相等,如果是两个基本类型,则比较的是值是否相等;“equals()”判断的时候,如果是一个引用类型一个基本类型,基本类型会自动装箱为引用类型。
3. 条件编译
public class Main {
public static void main(String[] args) {
if (true) { //必须使用条件为常量的if语句才行
System.out.pritnln("true");
} else {
System.out.println("false");
}
while (false) { //编译失败
System.out.println("while");
}
}
}
//编译为class文件后反编译
public class Main {
public static void main(String[] args) {
System.out.pritnln("true");
}
}
4. 其他语法糖
1. 当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为热点代码,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT)。
2. 即时编译器并不是虚拟机必须的部分,但是即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心且最能体现虚拟机技术水平的部分。
1. 解释器与编译器
2. 编译对象与触发条件
1. 示例
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.get();
//
z = b.get();
sum = y + z;
}
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.value;
//
z = b.value;
sum = y + z;
}
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.value;
//
z = y;
sum = y + z;
}
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.value;
//
y = y;
sum = y + y;
}
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.value;
//
sum = y + y;
}
2. 公共子表达式消除
3. 数组边界检查消除
4. 方法内联
5. 逃逸分析
1. Java虚拟机的即时编译器与C/C++的静态优化编译器相比,可能会由于下列这些原因而导致输出的本地代码有一些劣势: