这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如”public static int value = 123;”,value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如”public static final int value = 123;”就不一样了,在准备阶段,虚拟机就会给value赋值为123。
解析
将符号引用替换为直接引用的过程,
符号引用, 包括: 类和接口的全限定名; 字段的名称和描述符; 方法的名称和描述符
例如下面这串代码:
package com.xrq.test6;
public class TestMain
{
private static int i;
private double d;
public static void print()
{
}
private boolean trueOrFalse()
{
return false;
}
}
public class SuperClass
{
public static int value = 123;
static
{
System.out.println("SuperClass init");
}
}
public class SubClass extends SuperClass
{
static
{
System.out.println("SubClass init");
}
}
public class TestMain
{
public static void main(String[] args)
{
System.out.println(SubClass.value);
}
}
运行结果为
SuperClass init
2、通过数组定义引用类,不会触发此类的初始化
public class SuperClass
{
public static int value = 123;
static
{
System.out.println("SuperClass init");
}
}
public class TestMain
{
public static void main(String[] args)
{
SuperClass[] scs = new SuperClass[10];
}
}
3、引用静态常量时,常量在编译阶段会存入类的常量池中,本质上并没有直接引用到定义常量的类
public class ConstClass
{
public static final String HELLOWORLD = "Hello World";
static
{
System.out.println("ConstCLass init");
}
}
public class TestMain
{
public static void main(String[] args)
{
System.out.println(ConstClass.HELLOWORLD);
}
}
运行结果为
Hello World
https://dzone.com/articles/how-use-verbose-options-java
-verbose:class is used to display the information about classes being loaded by JVM. This is useful when using class loaders for loading classes dynamically or for analysing what all classes are getting loaded in a particular scenario.
运行时数据区域
程序计数器(programme counter register)
若执行的是非native方法, 则保存下条指令的地址; 若是native方法, 则为空;每个线程独有, 互不影响
虚拟机栈(virtual machine stacks), 本地方法栈(native method stack)
每个线程独有, 每个方法创建的时候都会创建一个栈帧(stack frame),用于存储方法的局部变量, 操作数栈等. , 虚拟机栈和本地方法栈的不同是,前者执行java方法, 后者执行native方法
java 堆(heap)
存放对象的实例和数组, 所有线程所共有; 如果堆中没有内存完成实例的分配, 并且堆也无法再扩展时,抛出 OutOfMemoryError
方法区(Method Area)
线程间共享, 存储每个类的结构,包括运行时常量 (包括string pool) ,静态变量,即时编译器编译后的代码等数据
本地内存(native memory, C heap)
虚拟机对象
创建 每个线程分配一块独立的内存,本地线程分配缓冲(Thread local allocation buffer),来控制给每个对象分配内存时是线程安全的
对象的内存布局 对象头, 实例数据, 对齐填充
对象的访问 sun hotspot通过直接指针的方式, reference存储了对象的地址,存储在栈区(应该指的是虚拟机栈),直接访问到堆中的对象的数据,对象的数据中包含类的信息.
内存泄漏原因
StackOverFlowError
OutOfMemoryError
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
java heap分析工具
问题
三. 垃圾回收
可达性分析
引用
强引用 只要强引用存在, 则永远不会被回收
软引用 SoftReference来实现, 除非内存准备溢出了, 不然不会被回收.
弱引用 WeakReference的对象, 若只被弱引用引用, 不被其他任何强引用引用时, 如果GC运行, 那么该对象就会被回收.例子 ThreadLocal中的Entry 持有对ThreadLocal对象的弱引用, 若所有使用该ThreadLocal的线程均退出,
虚引用
垃圾收集策略
需要回收的对象进行一次标记,标记完成后统一回收
缺点
标记-复制算法
将内存分配为一块eden和两块survivor, 比例是8:1, 每次使用新生代内存的90%, gc时将存活对象复制到空闲的survivor, 剩余对象一次性清理
缺点
标记整理算法 将可用的对象向一端移动,然后清理掉边界以外的内存,有效避免了内存碎片(针对标记清除算法)和需要有一部分空间来作为留存空间(针对标记-复制算法).
分代收集算法 对新生代和老年代采用不同的垃圾收集算法, 例如HotSpot新生代采用标记-复制, 老年代采用标记整理
垃圾收集器
Serial
ParNew(parallel 并行)
Parallel Scavenage
新生代收集器, 注重控制吞吐量来控制GC的停顿时间, 虚拟机运行100分钟, GC一分钟, 吞吐量 99% 重要参数 : -XX:MaxGCPauseMills , -XX:GCTimeRatio, -XX:UseAdaptiveSizePolicy(自适应调节) 与ParNew的最大区别
是Serial 的老年代版本, 与Parallel Scavenage配合使用, 作为CMS的备案, 在发生concurrent mode failure使用
是Parallel Scavenage的老年代版本, 可与Parallel Scavenage 配合使用
主要是将回收时间降至最短,基于标记-清除算法,
问题
java heap 分代(基于jdk1.8)
GC回收过程
GC调优案例
将SurvivorRatio由默认的8改为2
NewParSize调优
hotSpot的算法实现
什么时候开始GC 当eden和一个survivor的空间容不下新的对象时,产生minorGC,将长期存活对象移到老年代, 若老年代的空间不够, 则进行fullGC
GC root
寻找GC Root的引用链
但是导致引用变化的指令可能非常多, 可能导致 OOP Map的所占空间巨大
开发中的GC优化
类加载机制
参考自 link
验证
准备
例如下面这串代码:
用javap把这段代码的.class反编译一下:
1、子类引用父类静态字段,不会导致子类初始化。至于子类是否被加载、验证了,前者可以通过”-XX:+TraceClassLoading”来查看
2、通过数组定义引用类,不会触发此类的初始化
3、引用静态常量时,常量在编译阶段会存入类的常量池中,本质上并没有直接引用到定义常量的类
在编译阶段通过常量传播优化,常量HELLOWORLD的值”Hello World”实际上已经存储到了NotInitialization类的常量池中,以后NotInitialization对常量ConstClass.HELLOWORLD的引用实际上都被转化为NotInitialization类对自身常量池的引用了。也就是说,实际上的NotInitialization的Class文件中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。
类与类的加载器 只要当两个类来自同一个class文件,被同一个虚拟机加载,类加载器相同, equals(), isAssignableFrom(), instanceof 才能返回两个类相等.
双亲委派模型(parents delegation model) 当一个类加载器收到了类加载的请求, 首先把请求委派给父类加载器执行, 所以所以的加载请求都会首先传递到顶层的启动类加载器, 当父类无法加载时,子加载器才会尝试自己加载
classpath 可以参考honghailiang888, 非常齐全
如何手动编译并运行 java文件
class文件发现规则:class文件所在目录 = classpath + '\' + 包名中的'.'全变成'\', 一般会把运行java, javac程序的当前目录(.)也加入到classpath中, 然后会遍历所有的classpath, 在每个classpath下面找 包名+类名 对应的class文件.
当需要在任意目录编译 A.java时, 需要知道所引用的B.java的位置, 假设运行javac的目录为D: , 因为当前目录是D:, 在当前目录下用包名无法找到B.java, 故需要手动指定额外的classpath, 则会在packageA和packageB生成各自的class文件.
运行java 也需要通过classpath 找到对应的class文件, 并且需要指定 包名.类名 , 项目结构如之前所示, 在D: 下运行java,
GC 调优
jvm常用命令
问题