1.执行java文件
zhangjg@linux:/deve/workspace/HelloJava/src$ javac HelloWorld.java
zhangjg@linux:/deve/workspace/HelloJava/src$ ls
HelloWorld.class HelloWorld.java
HelloWorld.class并不能直接被系统识别执行,需要java虚拟机来执行。
每启动一个JAVA程序,必然会先创建一个JVM进程,由JVM加载class文件,转成字节码执行程序。
JVM是进程,执行的只有是线程,可以是一个主线程和多个其他线程
(取自 https://www.cnblogs.com/yixianyixian/p/7680321.html)
java命令首先启动虚拟机进程,虚拟机进程成功启动后,读取参数“HelloWorld”,把他作为初始类加载到内存,对这个类进行初始化和动态链接(关于类的初始化和动态链接会在后面的博客中介绍),然后从这个类的main方法开始执行。也就是说我们的.class文件不是直接被系统加载后直接在cpu上执行的,而是被一个叫做虚拟机的进程托管的。首先必须虚拟机进程启动就绪,然后由虚拟机中的类加载器加载必要的class文件,包括jdk中的基础类(如String和Object等),然后由虚拟机进程解释class字节码指令,把这些字节码指令翻译成本机cpu能够识别的指令,才能在cpu上运行。
从这个层面上来看,在执行一个所谓的java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程,而不是我们写的一个个的class文件。这个叫做虚拟机的进程处理一些底层的操作,比如内存的分配和释放等等。我们编写的class文件只是虚拟机进程执行时需要的“原料”。这些“原料”在运行时被加载到虚拟机中,被虚拟机解释执行,以控制虚拟机实现我们java代码中所定义的一些相对高层的操作,比如创建一个文件等,可以将class文件中的信息看做对虚拟机的控制信息,也就是一种虚拟指令。
2.JVM体系结构
JVM三大子系统
类加载器:用于加载.class文件,但并不是在开始运行时加载所有类,而是当程序需要某个类时,才会加载。
执行引擎:由虚拟机加载的类,被加载到Java虚拟机内存中之后,虚拟机会读取并执行它里面存在的字节码指令。虚拟机中执行字节码指令的部分叫做执行引擎。就像一个人,不是把饭吃下去就完事了,还要进行消化,执行引擎就相当于人的肠胃系统。在执行的过程中还会把各个class文件动态的连接起来。
垃圾收集子系统:Java虚拟机会进行自动内存管理。具体说来就是自动释放没有用的对象,而不需要程序员编写代码来释放分配的内存。这部分工作由垃圾收集子系统负责。
虚拟机内存区
堆(HEAP):存储对象和数组,如new Person(),new String[]。是最耗内存的区域,被所有线程共享,又称GC堆。全局变量是存在堆里的,不管是静态的还是非静态的(java 1.8以后)
程序计数器:又称寄存器,记录当前线程执行到哪个位置。一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。不会发生内存泄漏
Java栈(STACK):线程私有,每一个栈帧对应一个被调用的方法,记录了该方法的
-- 局部变量:方法中的局部变量,基本类型直接存储,对象则存储的引用
-- 操作数栈:表达式求值
-- 指向常量池的引用:即调用类中的常量
-- 方法返回地址:方法执行完后会返回到调用该方法的地址
本地方法栈:线程私有,执行JVM中native method
方法区:线程共享,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量(java 1.8以后类静态变量也存在了堆里)、常量以及编译器编译后的代码等。不同的虚拟机实现方法区的方式也不相同。对于HotPot而言,jdk1.7版本前称方法区为永久代(PermGen Space),由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出, 抛出“java.lang.OutOfMemoryError: PermGen space “异常,常用-XX:PermSize -XX:MaxPermSize来增大方法区内存大小避免内存溢出。但jdk1.8之后,用meta space元空间取代了PermGen Space,元空间受限于本地内存,PermGen则受限于虚拟机
常量池:1.8以后存在了堆里,包含字面量和符号引用两大类。作用是存储共享的常量,这样就避免了堆上创建重复对象额外的开销。8大基本类型全存储在常量池中,但所有封装类中只有Float和Double不存在常量池中。
字符串对象本身是不可变的(final),但它的引用是可以改变的,所以a="1",a="2",并没有将1这个对象改成2,而是将a的引用地址从1改为了2,“1”如果后面没有用到会被垃圾回收期回收;
final String a="1" 和 String a="1" 含义不一样,前者引用已经固定死不能修改,后者还能修改引用。所以在编译期间,String b=a+"3" 前者相当于b="1"+"3"直接在字符串常量池中创建“13”这个字符串,后者还是得在运行期间StringBuffer.append("3")来拼接一个新的字符串对象,存放于堆中。
静态常量池在编译期间把所有常量保存起来,运行常量池在运行期间先把静态常量池里所有东西拿过来,同时在运行过程中往常量池里添加新内容,如String.intern()。
2.1 类加载器
类加载器只分两种,bootstrap加载器和其他加载器
注意:classLoader有属性 private final ClassLoader parent
自定义的类加载器parent是AppClassLoader,AppClassLoader的parent是ExtClassLoader,但ExtClassLoader是没有父级的!!!
BootStrapClassLoader是C++编写的,并不是ExtClassLoader的父级!!!
双亲委派模型(https://blog.csdn.net/u011080472/article/details/51332866)
加载器加载文件执行classLoader类中loadClass方法。
1.先判断是否已加载过该文件 findLoadedClass
2.如果该classLoader有父级,则执行父级的loadClass super.loadClass
3.如果没有父级了,则启用bootstrap加载器作为父级,执行本地方法 findBootstrapClass
4.如果所有父级都无法加载class文件,则用本classLoader中的findClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派的好处是,例如java.lang.Object类,永远只会被BootStrap ClassLoader执行,Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
2.2 类初始化
https://www.cnblogs.com/chenyangyao/p/5296807.html
2.3 类加载过程
加载(装载):获得类的二进制流,存入方法区运行时数据,生成Java.lang.Class作为各种数据访问接口
验证:class甚至可以由自己创建16进制的文件获得,但要满足一定条件,如文件格式:开头几个字是CA FE BA BY,后面是主版本号次版本号,常量池等等;元数据验证:类是否有父类,是否继承了final类,抽象类的实现类是否实现了所有方法;字节码验证:类型转换;符号引用验证;
准备:为类变量分配内存并设置类变量初始值,都在方法区内执行。只会分配类变量(static修饰),实例变量是在对象实例化时一起分配在堆中。Private static int value=123 存的是初始值0,value=123是在初始化阶段执行的。Private static final int value=123 ,因为是final修饰,存的是123。
解析:将常量池中的符号引用替换为直接引用的过程
初始化:<clinit>类构造器执行的方法,即初始化静态变量和静态代码块(准备阶段是设置类变量初始值)。<clinit>方法仅在类有类变量赋值或者静态代码块时才执行,对于类会默认先执行父类中的<clinit>方法,接口里的变量默认是static final类型,所以不会执行父类的<clinit>。如果多线程同时去初始化一个类,只会有一个执行,其他阻塞,且同一个classLoader只会初始化一次同一个类。
public class DeadLoopClass {
static{
if(true){
System.out.println(Thread.currentThread()+" 单线程初始化DeadLoopClass类,其余线程等待");
// while(true)测阻塞,屏蔽掉测classLoader只初始化一次类
while(true){}
}
}
}
public class TestInitClass {
public static void main(String[] args) {
Runnable script = new Runnable() {
@Override
public void run() {
// 只会初始化一次DeadLoopClass
System.out.println("thread start");
DeadLoopClass dd = new DeadLoopClass();
System.out.println("初始化完成");
}
};
Thread t1= new Thread(script);
Thread t2 = new Thread(script);
t1.start();
t2.start();
}
}
3 垃圾收集器
3.1 判断对象是否已死
引用计数法:引用计数器,每当有一个地方引用到该对象,则+1,引用失效则-1。但无法解决objA.instance=objB,objB.instance=objA这样的循环引用。
可达性分析:GC ROOTS(栈中引用对象,方法区类静态属性引用对象,方法区常量引用对象,native方法引用对象)作为起始点,向下搜索引用链,如果对象和GC ROOTS间没有引用链,则不可达,可以回收。
3.2 引用类型
https://blog.csdn.net/u011179993/article/details/54564380
强引用:类似Object A= new Object();
软引用(SoftReference):SoftRefernce<Object> soft = new SoftReference(new Object()); soft.get()软引用对象只有在内存不足的情况下会被回收。
/**
* @Auther: ycig
* @Date: 2018/11/9 09:16
* @Description: java -Xms10m -Xmx10m SoftReferenceTest 软引用只有内存溢出了才会被回收
*/
public class SoftReferenceTest {
static class HeapObject {
byte[] bs = new byte[1024 * 1024];
}
public static void main(String[] args) {
SoftReference<HeapObject> softReference = new SoftReference<>(new HeapObject());
List<HeapObject> list = new ArrayList<>();
while (true) {
if (softReference.get() != null) {
list.add(new HeapObject());
System.out.println("list.add");
} else {
System.out.println("---------软引用已被回收---------");
break;
}
System.gc();
}
}
}
弱引用():无论内存是否足够,只要开始gc,就会被清除掉
public class WeakReferenceTest {
static class TestObject{
}
public static void main(String[] args) throws InterruptedException {
WeakReference<TestObject> weakReference=new WeakReference<>(new TestObject());
System.out.println(weakReference.get() == null);//false
System.gc();
TimeUnit.SECONDS.sleep(1);//暂停一秒钟
System.out.println(weakReference.get() == null);//true
}
}
虚引用(PhantomReference):不能获得对象实例,也不会对生存时间产生影响。唯一作用就是被收集器回收时能收到一个系统通知。
3.3 两次执刑
对于可达性分析而言,对象销毁至少需要经过两次gc,第一次发现没有和GC roots的引用链,会被标记。如果对象没有重写finalize()方法或是finalize()方法已执行了一次,则直接判定死刑。如果对象有重写finalize()方法,且是第一次执行,则会放入F-QUEUE队列中,执行一遍finalize(),如果在方法中有与引用链中任意对象关联,则将会逃出死刑。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK=null;
/**
* 当虚拟机认为该对象没有再被引用,会执行finalize方法
* 如果没有重写该方法或者已经被虚拟机调用过一次,则不会执行该方法
* 只有第一次gc执行,且重写了finalize,才会执行
* 推荐用try finally方法替代
* @throws Throwable
*/
@Override
public void finalize() throws Throwable{
super.finalize();
System.out.println("finalize 开始执行");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
// 第一次执行gc,可以拯救回来
System.gc();
Thread.sleep(500);
if(SAVE_HOOK !=null){
System.out.println("SAVE_HOOK IS ALIVE");
}else{
System.out.println("SAVE_HOOK IS DEAD");
}
// 加了这句话 SAVE_HOOK = new FinalizeEscapeGC(); 第二次执行gc还是会拯救回来
SAVE_HOOK = null;
// 第二次执行gc,不会进入finalize()方法,拯救不回来了
System.gc();
Thread.sleep(500);
if(SAVE_HOOK !=null){
System.out.println("SAVE_HOOK IS ALIVE");
}else{
System.out.println("SAVE_HOOK IS DEAD");
}
}
}
3.4 垃圾收集算法
标记-清除:效率一般,容易产生空间碎片
复制:内存分为大小相等两块,先用一块,快用完了就把还存活的对象复制到另一块,把原先的一块清除掉。这样来回,适用于对象存活时间短的地方,如新生代。其实现在是用一个Eden区(80%)和两个survivor区(10%+10%)来复制对象,每次使用eden区和一块Survivor0区(90%),用完了后复制存活对象到另一个Survivor1区(10%),清空eden和survivor0,接着再用eden和survivor1进行内存管理,如此反复。默认只有10%不到的对象存活了下来,如果超过10%,就得向其他区借内存。
标记整理:先标记,后让存活对象都向一端移动,然后清理掉端边界以外的内存。
分代收集:当代虚拟机都采用这种方法,根据对象生存周期将内存分为几部分。一般把JAVA堆分为新生代和老生代,新生代采用复制算法,因为每次回收过程中都有大批对象死去。老年代对象存活率高,采用标记-清除或者标记-整理方法。
现我们知道了不同区域采用不同的收集方法,可达性分析需要找到GC ROOTS和引用链,程序一直在并发运行,GC ROOTS也是时刻变换的,只有在某一时刻STOP THE WORLD,才能通过OopMap找到这一时刻的GC ROOTS。STW需要在安全点执行,安全点位置一般在
- 1、循环的末尾
- 2、方法临返回前 / 调用方法的call指令后
- 3、可能抛异常的位置
3.5 收集器
新生代收集器和老年代收集器搭配方式
新生代收集器
老年代收集器
serial old Parallel Old CMS
CMS:Concurrent Mark Sweep,希望系统停顿时间最短。标记-清除分为四个步骤 初始标记、并发标记、重新标记、并发清除。
初始标记需要stw,仅仅是标记GC ROOTS直接关联的对象(蓝色部分),因为有Oop Map的存在,所以该步骤很快。
并发标记不需要stw,从上一步被标记的对象进行可达性分析,组成关系网,与主线程共同执行。因为是并发的,所以有可能会标记一些本不该回收的对象。
重新标记,再次stw,仅针对上一步标记的对象,所以时间相当短。
并发清除,清除掉那些不在关系网上的对象,无需stw。由于是并发清除,所以可能会在清除过程中又产生些新的垃圾,又称“浮动垃圾”
优点是停顿时间短,缺点是耗CPU,且标记-清除方法容易产生碎片,导致内存空间不连续。
G1 收集器
回收范围是整个堆。将整个堆分为多个region,每个region依然存在年轻代和年老代的概念。每个region都有remembered set,记录了相关引用信息,垃圾收集扫描就无需全堆扫描了,只用扫描remembered set。年轻代采用复制,年老代采用标记-整理。也分为四步骤:初始标记、并发标记、最终标记、筛选回收,其中筛选回收指每个region回收的空间各不相同,对各个region的回收价值和成本进行排序,用户可通过-XX:MaxGCPauseMills=50毫秒设置有限的收集时间。
4. 内存模型
因为CPU处理速度特别快,内存跟不上,所以需要告诉缓存来作为内存和处理器间的缓冲。将运算所需的数据从内存复制到高速缓存区,运算完毕后再同步回内存中。
内存模型中,每一个工作内存对应一个线程,里面用到的公共数据来源于主内存(局部变量在方法中属于线程私有)。多个线程共用同一个类变量会引发线程安全问题。