Java生态
1. Java 源码编译
语法糖(Syntactic sugar):这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会. E.g. 用a[i]表示*(a+i),用a[i][j]表示*(*(a+i)+j), 从面向过程到面向对象也是一种语法糖
2. 解析执行
Java编译器:将Java源文件(.java文件)编译成字节码文件(.class文件,是特殊的二进制文件,二进制字节码文件),这种字节码就是JVM的“机器语言”。javac.exe可以简单看成是Java编译器。
Java解释器:是JVM的一部分。Java解释器用来解释执行Java编译器编译后的程序。java.exe可以简单看成是Java解释器。
JVM:一种能够运行Java字节码(Java bytecode)的虚拟机。
JDK=JRE + javac compiler+monitor tool. tomcat need jdk in unix for compile.
Tomcat can be run as a daemon using the jsvc tool from the commons-daemon project. Source tarballs for jsvc are included with the Tomcat binaries, and need to be compiled. Building jsvc requires a C ANSI compiler (such as GCC), GNU Autoconf, and a JDK.
字节码:字节码是已经经过编译,但与特定机器码无关,需要解释器转译后才能成为机器码的中间代码。
Java字节码:是Java虚拟机执行的一种指令格式。
解释器:是一种电脑程序,能够把高级编程语言一行一行直接翻译运行。解释器不会一次把整个程序翻译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每翻译一行程序叙述就立刻运行,然后再翻译下一行,再运行,如此不停地进行下去。它会先将源码翻译成另一种语言,以供多次运行而无需再经编译。其制成品无需依赖编译器而运行,程序运行速度比较快。
即时编译(Just-in-time compilation: JIT):又叫实时编译、及时编译。是指一种在运行时期把字节码编译成原生机器码的技术,一句一句翻译源代码,但是会将翻译过的代码缓存起来以降低性能耗损。这项技术是被用来改善虚拟机的性能的。
JIT编译器是JRE的一部分。原本的Java程序都是要经过解释执行的,其执行速度肯定比可执行的二进制字节码程序慢。为了提高执行速度,引入了JIT。在运行时,JIT会把翻译过来的机器码保存起来,以备下次使用。而如果JIT对每条字节码都进行编译,则会负担过重,所以,JIT只会对经常执行的字节码进行编译,如循环,高频度使用的方法等。它会以整个方法为单位,一次性将整个方法的字节码编译为本地机器码,然后直接运行编译后的机器码。
1.1.1类的加载时机
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(class文件加载到JVM中):
a. 创建类的实例(new 的方式)。访问某个类或接口的静态变量,或者对该静态变量赋值,调用类的静态方法
b. 反射
c. 初始化某个类的子类,则其父类也会被初始化
d. Java虚拟机启动时被标明为启动类的类,直接使用java.exe命令来运行某个主类(包含main方法的那个类)
e. 当使用JDK1.7的动态语言支持时
1.1.2如何将类加载到jvm
class文件是通过类的加载器装载到jvm中的类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因
加载类的开放性
在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器。这一动作是放在Java虚拟机外部去实现的,以便让应用程序自己决定如何获取所需的类。
虚拟机规范并没有指明二进制字节流要从一个Class文件获取,或者说根本没有指明从哪里获取、怎样获取。这种开放使得Java在很多领域得到充分运用,例如:
从ZIP包中读取,这很常见,成为JAR,EAR,WAR格式的基础
从网络中获取,最典型的应用就是Applet
运行时计算生成,最典型的是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
由其他文件生成,最典型的JSP应用,由JSP文件生成对应的Class类
从java虚拟机角度来讲,只存在两种不同的类加载器:
(1)一种是启动类加载器,由C++语言实现的,属于虚拟机的一部分;
(2)一种是所有的其他类加载器,这些都是由Java实现的,独立于虚拟机外部,继承自java.lang.ClassLoader;
从开发人员角度来讲, Java默认有三种类加载器:
各个加载器的工作责任:
1)Bootstrap ClassLoader:负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,这个加载器很特殊,它不是Java类,因此它不需要被别人加载,它嵌套在Java虚拟机内核里面,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码),它可以去加载别的类,不是ClassLoader子类
2)Extension ClassLoader:负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
3)App ClassLoader:负责记载classpath中指定的jar包及目录中class
工作过程:
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
其实这就是所谓的双亲委派模型 (parent delegation model)。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。(https://juejin.im/post/5b3cc84ee51d4519873f08da)
好处:防止内存中出现多份同样的字节码(安全性角度)
特别说明:
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。
双亲委派模型在JDK1.2中引入,但不是强制性的。在一定条件下,为了完成某些操作,可以“破坏”模型。
1.重新loadClass方法
2.利用线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的 setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承 一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序 类加载器。
例如JNDI服务,但是当JNDI要对资源进行集中化管理时,他需要调用其他公司实现并部署在应用程序的classpath下的JNDI接口,因为这些代码是需要我们开发者自己来实现的,这时启动类加载器是无法识别这些类的,于是乎出现了一种线程上下文加载器(Thread Context ClassLoader),JNDI服务可以调用该加载器去加载所需要的代码,就是通过父类加载器去请求子类加载器来实现的
3.为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。
那能不能自己写个类叫java.lang.System?
答案:通常不可以,但可以采取另类方法达到这个需求。
解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
https://www.zhihu.com/question/46719811
1.1.3类加载详细过程
加载器加载到jvm中,接下来分为几个步骤:
1) 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象。
2) 连接,连接又包含三块内容:验证、准备、初始化。 1)验证,文件格式、元数据、字节码、符号引用验证; 2)准备,为类的静态变量分配内存,并将其初始化为默认值; 3)解析,把类中的符号引用转换为直接引用
3) 初始化,为类的静态变量赋予正确的初始值。
https://juejin.im/post/5b3cc84ee51d4519873f08da
https://www.mrsssswan.club/2018/06/30/jvm-start1/
1.1.4 JIT即时编译器 (Just-in-time compiler)
Just in time编译,也叫做运行时编译,不同于 C / C++ 语言直接被翻译成机器指令,javac把java的源文件翻译成了class文件,而class文件中全都是Java字节码。那么,JVM在加载了这些class文件以后,针对这些字节码,逐条取出,逐条执行,这种方法就是解释执行(interprete)。
还有一种,就是compile, 把这些Java字节码重新编译优化,生成机器码,让CPU直接执行。这样编出来的代码效率会更高。通常,我们不必把所有的Java方法都编译成机器码,只需要把调用最频繁,占据CPU时间最长的方法找出来将其编译成机器码。这种调用最频繁的Java方法就是我们常说的热点方法
因为编译也是要花费时间的,我们一般对热点代码做compile,非热点代码直接interprete就好了。
热点代码解释:一、多次调用的方法。二、多次执行的循环体
使用热点探测来检测是否为热点代码,热点探测有两种方式:采样, 与计数器
目前HotSpot使用的是计数器的方式,它为每个方法准备了两类计数器:
1) 方法调用计数器(Invocation Counter)
2) 回边计数器(Back EdgeCounter)。
在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。
https://www.ibm.com/developerworks/cn/java/j-lo-just-in-time/
https://zhuanlan.zhihu.com/p/28476709
1.2 JVM的内存模型
方法区: 线程共享,用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码. JDK 8的HotSpot JVM现在使用的是本地内存来表示类的元数据,这个区域就叫做元空间(meta space). 元空间的特点:充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致; 每个加载器有专门的存储空间; 只进行线性分配.不会单独回收某个类; 省掉了GC扫描及压缩的时间. 元空间里的对象的位置是固定的, 提高Full GC的性能,在Full GC期间,Metadata到Metadata pointers之间不需要扫描了,别小看这几纳秒时间
https://www.jianshu.com/p/7b88aa16c2f6
堆: 是JVM中最大的一块区域,线程共享,此区唯一的目的就是存放对象实例 (方法区中的类实例化之后),几乎所有对象实例都在这里分配,但是随着JIT编译器及逃逸分析技术的发展,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有对象都分配在堆上也渐渐变的不是那么绝对.
针对于jdk1.7之后:常量池位于堆中. 常量池存储的是:
a) 字面量(Literal):文本字符串等---->用双引号引起来的字符串字面量都会进这里面
b) 符号引用(Symbolic References) - 类和接口的全限定名(Full Qualified Name) - 字段的名称和描述符(Descriptor) - 方法的名称和描述符
字符串常量池只存储引用,不存储内容
虚拟机栈/栈内存
见题目for string.intern() https://zhuanlan.zhihu.com/p/39536807
虚拟机栈:即我们平时经常说的栈内存,也是线程私有,是Java方法执行时的内存模型,类中的每个方法在执行时都会创建一个栈帧(frame)用于储存以下内容:
栈帧(frame):
局部变量表:32位变量槽,存放了编译期可知的各种基本数据类型的值、对象引用、returnAddress类型。Stack memory only contains local primitive variables and reference variables to objects in heap space.
操作数栈:基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
动态连接:每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接。
方法出口:返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。
frameis used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions.
Each frame has its own array of local variables (§2.6.1), its own operand stack (§2.6.2), and a reference to the run-time constant pool (§2.5.5) of the class of the current method.
本地方法栈:线程私有,与虚拟机栈类似,为native方法服务。
程序计数器:一块较小的内存空间,可以看作当前线程所执行的字节码行号指示器。
程序计数器是线程私有,各线程之间互不影响
如果正在执行java方法,计数器记录的是正在执行的虚拟机字节码指令地址
如果执行native方法,这个计数器为null
程序计数器也是在Java虚拟机规范中唯一没有规定任何OutOfMemoryError异常情况的区域
举例说明
宏观简述一下例子中的工作流程:
1、通过java.exe运行Java3yTest.class,随后被加载到JVM中,元空间存储着类的信息(包括类的名称、方法信息、字段信息..)。
2、然后JVM找到Java3yTest的主函数入口(main),为main函数创建栈帧,开始执行main函数
3、main函数的第一条命令是Java3y java3y = new Java3y();就是让JVM创建一个Java3y对象,但是这时候方法区中没有Java3y类的信息,所以JVM马上加载Java3y类,把Java3y类的类型信息放到方法区中(元空间)
4、加载完Java3y类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Java3y实例分配内存, 然后调用构造函数初始化Java3y实例,这个Java3y实例持有着指向方法区的Java3y类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用
5、当使用java3y.setName("Java3y");的时候,JVM根据java3y引用找到Java3y对象,然后根据Java3y对象持有的引用定位到方法区中Java3y类的类型信息的方法表,获得setName()函数的字节码的地址
6、为setName()函数创建栈帧,开始运行setName()函数
从微观上其实还做了很多东西,正如上面所说的类加载过程(加载-->连接(验证,准备,解析)-->初始化),在类加载完之后jvm为其分配内存(分配内存中也做了非常多的事)。由于这些步骤并不是一步一步往下走,会有很多的“混沌bootstrap”的过程,所以很难描述清楚。扩展阅读(先有Class对象还是先有Object): https://www.zhihu.com/question/30301819
https://www.journaldev.com/4098/java-heap-space-vs-stack-memory
1.7.1JVM垃圾回收
判断对象死去有两种方法:
1) 引用计数法-->这种难以解决对象之间的循环引用的问题
2) 可达性分析算法-->主流的JVM采用的是这种方式
回收对象的方法:
标记-清除算法:(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
复制算法: (Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
标记-压缩算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法 (一般GC常用方法。 其实就是组合上面的算法,不同的区域使用不同的算法)
无论是可达性分析算法,还是垃圾回收算法,JVM使用的都是准确式GC(Type accurate GC. 给定某个位置上的某块数据, 知道其是不是指针。 与之相对的是保守与半保守式GC, 其实现较为简单)。JVM是使用一组称为OopMap (object reference)的数据结构,来存储所有的对象引用(这样就不用遍历整个内存去查找了,空间换时间)。 并且不会将所有的指令都生成OopMap,只会在安全点(SafePoint)上生成OopMap,在安全区域(Safe Region)上开始GC。
http://www.cnblogs.com/strinkbug/p/6376525.html
https://my.oschina.net/u/1757225/blog/1583822
在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举(可达性分析)。上面所讲的垃圾收集算法只能算是方法论,落地实现的是垃圾收集器:图中两个收集器之间有连线,则说明它们可以配合使用.
Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,但可能会产生较长的停顿,只使用一个线程去回收。
ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
Parallel Old收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程“标记-整理”算法
CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担。CMS无法处理浮动垃圾。CMS的“标记-清除”算法,会导致大量空间碎片的产生。
G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution. One difference is that G1 is a compacting collector. Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets
The older garbage collectors (serial, parallel, CMS) all structure the heap into three sections: young generation, old generation, and permanent generation of a fixed memory size.
The G1 collector takes a different approach.
The heap is partitioned into a set of equal-sized heap regions, each a contiguous range of virtual memory. Certain region sets are assigned the same roles (eden, survivor, old) as in the older collectors, but there is not a fixed size for them. This provides greater flexibility in memory usage.
JVM 调试
32 JVM 优于 64JVM
DirectMemory
JVM除了堆内存之外,就只有栈内存和DirectMemory了。栈空间每个线程是固定的,线程数也没可能多到可以占用这么多内存的程序,所以怀疑的目标就在DirectMemory上了。
DirectMemory是java nio引入的,直接以native的方式分配内存,不受jvm管理。这种方式是为了提高网络和文件IO的效率,避免多余的内存拷贝而出现的。DirectMemory占用的大小没有直接的工具或者API可以查看,不过这个在Bits类中是有两个字段存储了最大大小和已分配大小的,使用反射可以拿到这个数据。
Class<?> c = Class.forName("java.nio.Bits");
Field maxMemory = c.getDeclaredField("maxMemory");
maxMemory.setAccessible(true);
Field reservedMemory = c.getDeclaredField("reservedMemory");
reservedMemory.setAccessible(true);
Long maxMemoryValue = (Long)maxMemory.get(null);
Long reservedMemoryValue = (Long)reservedMemory.get(null);
原来,DirectMemory 的默认大小是64M,而JDK6之前和JDK6的某些版本的SUN JVM,存在一个BUG,在用-Xmx设定堆空间大小的时候,也设置了DirectMemory的大小。加入设置了-Xmx2048m,那么jvm最终可分配的内存大小为4G多一些,是预期的两倍。
解决方式是设置jvm参数-XX:MaxDirectMemorySize=128m,指定DirectMemory的大小。
getRuntime.exec()
占用资源很多,尽量避免使用。其调用时会先clone一个和现在虚拟机一样环境变量的进程。用这个新进程去执行外部命令
异步改消息队列
参考资料
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html
https://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java#memory-management-in-java-8211-java-garbage-collection
https://www.jianshu.com/p/63fe09fe1a60