在虚拟机安装tomcat并部署服务,并且实现会话共享
tomcat安装
包安装
# CentOS / Rocky
# 查询当前tomcat版本 (一般需要epel源)
yum list |grep tomcat
# 安装tomcat
yum -y install tomcat tomcat-admin-webapps tomcat-docs-webapp tomcat-webapps
# 开启tomcat
systemctl enable --now tomcat.service
# 查看状态
systemctl status tomcat.service
ss -tulnp
journalctl -u tomcat.service
ps aux|grep tomcat
# 验证
curl 127.0.0.1:8080
# debian / ubuntu
# 查询当前tomcat版本
apt list | grep tomcat
# 安装tomcat (该包名包含主版本号)
apt update && apt -y install tomcat9 tomcat9-admin tomcat9- docs tomcat9-examples
# 开启tomcat
systemctl enable --now tomcat.service
# 查看状态
systemctl status tomcat.service
ss -tulnp
journalctl -u tomcat.service
ps aux|grep tomcat
# 验证
curl 127.0.0.1:8080
部署一个简单应用
-
conf/server.xml
<!-- 使用默认应用路径 --> <!-- alopex.net __> webapps/ROOT --> <!-- alopex.net __> webapps/blog --> <Host name="alopex.net" appBase="webapps" unpackWAR="true" autoDeploy="true"> <Context path="/blog" docBase="blog" reloadable="true"/> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="alopex_access_webapps_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b" /> </Host> <!-- 访问路径与磁盘路径剥离类似于Nginx的alias --> <!-- alopex.com __> /www/alopex --> <!-- alopex.com/blog __> /www/alopex/blog --> <!-- alopex.com/blogX __> /xxx/blogx --> <Host name="alopex.com" appBase="/www/alopex" unpackWAR="true" autoDeploy="true"> <Context path="" docBase=""/> <Context path="/blog" docBase="blog" reloadable="true"/> <Context path="/blogX" docBase="/xxx/blogx" reloadable="true"/> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="alopex_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b" /> </Host>
-
生成文件
tree /xxx/ /www/ ../webapps/blog/ /xxx/ └── blogx └── index.html /www/ └── alopex ├── blog │ └── index.html └── index.html ../webapps/blog/ └── index.html
-
修改用户
chown -R tomcat.tomcat blog/ /www/ /xxx/
-
修改访问客户端
/etc/hosts
Why Does Sudo Echo Fail with Permission Denied?echo "192.168.100.34 alopex.com alopex.net " | sudo tee -a /etc/hosts
-
访问
- http://alopex.net:8080 --> /usr/local/apache-tomcat-9.0.87/webapps/ROOT
- http://alopex.net:8080/blog/ --> /usr/local/apache-tomcat-9.0.87/webapps/blog
- http://alopex.com:8080/ --> /www/alopex
- http://alopex.com:8080/blog/ --> /www/alopex/blog
- http://alopex.com:8080/blogX/ --> /xxx/blogx
会话共享
-
准备工作
- nginx主机 (nginx/1.18.0 )
sudo apt install nginx
- tomcat服务器 (java/1.8.0_392, tomcat/9.0.31-1ubuntu0.4)
sudo apt install openjdk-8-jre sudo apt install tomcat8 mv /var/lib/tomcat9/webapps/ROOT/index.html /var/lib/tomcat9/webapps/ROOT/index.html.bat vim /var/lib/tomcat9/webapps/ROOT/index.jsp <%@ page language="java" %> <html> <head><title>TomcatA</title></head> <body> <h1><font color="red">Tomcat1/2 </font></h1> <table align="centre" border="1"> <tr> <td>Session ID</td> <% session.setAttribute("abc","abc"); %> <td><%= session.getId() %></td> </tr> <tr> <td>Created on</td> <td><%= session.getCreationTime() %></td> </tr> </table> </body> </html> chown -R tomcat.tomcat /var/lib/tomcat9/webapps/ROOT
session 绑定
特点:简单容实现
缺点:如果目标服务器故障后,如果没有做sessoin持久化
实际应用:实际生产很少选择该方式
-
nginx主机
vim /etc/nginx/conf.d/tomcat.conf upstream tomcat-server{ # ip_hash; # 对IP的前24位进行哈希绑定 # hash $remote_addr consistent; # 根据客户端IP的全部位 # hash $cookie_jsessionid consistent; # 通过cookie进行绑定,这样更有助于对主机区分而不是网段 # (cookie即使是大写,这里也需要转为小写) # consistent 算法选项可以确保当后端服务器的数量发生变化时,仅有一小部分请求需要重新分配到其他服务器 # 这对于维护会话的连续性和减少因服务器变动而导致的缓存失效非常有用。 server 192.168.100.121:8080; server 192.168.100.122:8080; } server { listen 192.168.100.120:80; location ~* \.(jsp|do)$ { proxy_pass http://tomcat-server; } } systemctl restart nginx.service
-
tomcat主机
systemctl restart tomcat9.service
-
测试
# 浏览器测试 http://192.168.100.120/index.jsp # curl测试 curl http://192.168.100.120/index.jsp -c /tmp/cookie.txt curl http://192.168.100.120/index.jsp -b /tmp/cookie.txt
session 复制
优点:Tomcat自己的提供的多播集群,通过多播将任何一台的session同步到其它节点;tomcat官方解决方案(建议tomcat
数量在4个节点之内
)
缺点:Tomcat的同步节点不宜过多,互相即时通信同步session需要大带宽
;每一台都拥有全部session
,内存损耗太多
-
nginx服务器
vim /etc/nginx/conf.d/tomcat.conf upstream tomcat-server{ # 轮循算法 server 192.168.100.121:8080; server 192.168.100.122:8080; } server { listen 192.168.100.120:80; location ~* \.(jsp|do)$ { proxy_pass http://tomcat-server; } } systemctl restart nginx.service
-
tomcat服务器
- tomcat9 参考文档
tomcat9
搭配java11
无法实现session同步
- conf/server.xml
address=228.0.0.4
: 使用的是组播地址(223.0.0.0 - 239.0.0.0)port=45564
: 组播udp端口frequency=500
: 500ms 发送一次dropTime=3000
: 故障阀值为3秒address="auto"
: 监听地址,此项建议修改为当前主机的IP(不支持0.0.0.0
),如果不修改可能会导致服务无法启动port=4000
: 监听端口autoBind=100
: 如果端口出现冲突,自动绑定端口范围4000-4100
SelectorTime=5000
: 自动绑定超时时常5s
<!-- 在Engine层级添加 --> <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8"> <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/> <Channel className="org.apache.catalina.tribes.group.GroupChannel"> <Membership className="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4" port="45564" frequency="500" dropTime="3000"/> <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver" address="auto" port="4000" autoBind="100" selectorTimeout="5000" maxThreads="6"/> <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter"> <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/> </Sender> <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/> <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/> </Channel> <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/> <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/> <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer" tempDir="/tmp/war-temp/" deployDir="/tmp/war-deploy/" watchDir="/tmp/war-listen/" watchEnabled="false"/> <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/> </Cluster>
- webapps/ROOT/WEB-INFO/web.xml (
Make sure your web.xml has the <distributable/> element
)<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0" metadata-complete="true"> <display-name>Welcome to Tomcat</display-name> <description> Welcome to Tomcat </description> <!-- 添加此行 --> <distributable/> </web-app>
session Server
MSM 解决方案
当前MSM
不支持 tomcat 10
版本
(memcached session manager) 提供将Tomcat的session保持到memcached或Redis的程序,可以实现高可用
-
支持Tomcat的 6.x、7.x、8.x、9.x
- memcached-session-manager-2.3.2.jar
- memcached-session-manager-tc8-2.3.2.jar
-
Session数据的序列化、反序列化类
- 官方推荐kyro
- 在webapp中WEB-INF/lib/下
-
驱动类
- memcached(spymemcached.jar)
- Redis(jedis.jar)
-
配置
# 在 $CATALINA_HOME/lib/ 目录下放入如需包
kryo-3.0.3.jar
asm-5.2.jar
objenesis-2.6.jar
reflectasm-1.11.9.jar
minlog-1.3.1.jar
kryo-serializers-0.45.jar
msm-kryo-serializer-2.3.2.jar
memcached-session-manager-tc8-2.3.2.jar
spymemcached-2.12.3.jar
memcached-session-manager-2.3.2.jar
# sticky 模式
> 即前端tomcat和后端memcached有关联(粘性)关系
# 粘性一般为交叉粘性,因此主机n1,对应failoverNodes为n1;同理主机n2,对应failoverNodes为n2
# memcachedNodes="n1:host1.yourdomain.com:11211,n2:host2.yourdomain.com:11211"
# memcached的节点: n1、n2只是别名,可以重新命名。
# failoverNodes 为故障转移节点,n1是备用节点,n2是主存储节点。另一台Tomcat将此处的n1改为n2,
# 修改$CATALINA_HOME/conf/context.xml
# 其主节点是n1,备用节点是n2。
# n1 (192.168.100.121) 配置
<Context>
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.100.121:11211,n2:192.168.100.122:11211"
failoverNodes="n1"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"/>
</Context>
# n2 (192.168.100.122) 配置
<Context>
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.100.121:11211,n2:192.168.100.122:11211"
failoverNodes="n2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"/>
</Context>
# 查看启动日志
journal -u tomcat9.service
Mar 19 13:19:52 n2 tomcat9[86253]: --------
Mar 19 13:19:52 n2 tomcat9[86253]: - finished initialization:
Mar 19 13:19:52 n2 tomcat9[86253]: - sticky: true
Mar 19 13:19:52 n2 tomcat9[86253]: - operation timeout: 1000
Mar 19 13:19:52 n2 tomcat9[86253]: - node ids: [n1]
Mar 19 13:19:52 n2 tomcat9[86253]: - failover node ids: [n2]
Mar 19 13:19:52 n2 tomcat9[86253]: - storage key prefix: null
Mar 19 13:19:52 n2 tomcat9[86253]: - locking mode: null (expiration: 5s)
Mar 19 13:19:52 n2 tomcat9[86253]: --------
# non-sticky 模式
> 即前端tomcat和后端memcached无关联(无粘性)关系,从msm 1.4.0之后版本开始支持non-sticky模式。
# 在 $CATALINA_HOME/lib/ 目录下放入如需包
kryo-3.0.3.jar
asm-5.2.jar
objenesis-2.6.jar
reflectasm-1.11.9.jar
minlog-1.3.1.jar
kryo-serializers-0.45.jar
msm-kryo-serializer-2.3.2.jar
memcached-session-manager-tc8-2.3.2.jar
spymemcached-2.12.3.jar
memcached-session-manager-2.3.2.jar
# 修改$CATALINA_HOME/conf/context.xml
# n1 (192.168.100.121) 配置
<Context>
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.100.121:11211,n2:192.168.100.122:11211"
sticky="false"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"/>
</Context>
- 检查memcached脚本 (配置完成后测试)
sudo apt install python3-pip && pip3 install python-memcached
- 显示memcache内容
总结JVM内存结构和垃圾回收算法
JVM 内存管理
线程私有的内存区域
程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间(可能位于cpu的寄存器,有待确认),可以看做是当前字节码指令执行的行号指示器,记录了当前正在执行的虚拟机字节码指令地址。每个线程都有各自独立的程序计数器,注意如果正在执行的是 Native方法,则程序计数器为空(Undifined),并且 JVM 规范中并没有对程序计数器定义 OutOfMemoryError 异常
。
虚拟机栈(VM Stack)
虚拟机栈也是线程私有的,它描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
虚拟机栈帧中,局部变量表
是比较为人所熟知的,也就是平常所说的“栈”,局部变量表所需的内存空间在编译期间分配完成,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间
不会改变局部变量表的大小。
虚拟机栈有两种异常情况:
StackOverflowError
:线程请求的栈深度大于虚拟机所允许的深度,特别是方法的递归调用时OutOfMemoryError
:虚拟机栈无法满足线程所申请的空间需求,即使经过动态扩展仍然无法满足时抛出
本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈相似,不过服务于本地方法,有些虚拟机将这两个区域合二为一。本地方法栈中抛出异常的情况与虚拟机栈相同。
线程共享的内存区域
堆(Heap)
通常来说,堆是Java虚拟机管理的内存中最大的一块
,被所有线程共享,在虚拟机启动时创建,堆的作用就是存储对象实例。
堆也是垃圾收集器所管理的主要区域,因此很多时候也被称作GC堆
。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,因此堆还可以被细分为:新生代
和老年代
。再继续细分可以分为:Eden空间
、From Survivor空间
、To Survivor空间
等,从内存分配的角度来看,线程共享的堆中还可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
堆可以是物理上不连续的空间
,只要逻辑上是连续的
即可,-Xmx和-Xms参数可以控制堆的最大和最小值。
堆的空间大小不满足时将抛出OutOfMemoryError异常
。
方法区(Method Area)
用于存储已被虚拟机加载的类信息
、常量
、静态变量
、JIT编译后的代码
等数据。Java虚拟机规范将方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap
(非堆)。
方法区同样会抛出OutOfMemoryError异常
。
在方法区中有一部分区域用来存储编译期产生的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。这里需要说明一点,常量并不是只能在编译期产生,运行期间也会产生新的常量并被发在常量池中,如 String 类的 intern() 方法。
垃圾回收机制
JVM 管理的内存中,线程私有的虚拟机栈
、本地方法栈
以及程序计数器
都是随着线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出有条不紊的进行入栈和出栈。
这部分内存区域随着线程结束或者方法退出自然的就被释放回收了,因此这部分不需要过多考虑回收问题。而 Java 堆
和方法区
则不一样,这部分内存的分配和回收都是动态的。垃圾收集器关注的区域主要指的是这部分内存。
垃圾判断 (what)
GC 在垃圾回收的时候首先需要判断哪些对象时仍在使用,哪些是已经不再使用了
引用计数法
- 给对象添加一个引用计数器,每当有一个地方引用计数器就加 1,引用失效时计数器就减 1
- 因此哪些计数器为 0 的对象都是不再被引用需要回收的对象
- 优点: 实现简单、效率也高
- 缺点: 无法解决对象之间相互引用的情况 (相互引用成一个整体,但并无其他对象对其调用)
达性分析法
(HotSpot 默认)- 通过一系列称为
GC Roots
的对象作为起点开始向下进行搜索,搜索走过的路径叫做引用链
- 如果一个对象
没有任何引用链与其相连
时说明该对象不可达,即不可能再被使用到 - 可作为
GC Roots
的对象- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中 Native 方法引用的对象
- 方法区中的常量引用的对象
- 通过一系列称为
回收方式 (how)
复制算法(Coping)
这种算法将可用内存按照容量划分为大小相等的两部分,每次只使用其中一半,当这一半使用完了就将其中还存活的对象复制到另一块内存上,然后对这一块内存进行回收,循环往复。优点是实现简单、运行高效
。缺点就是可用内存缩小到了原来的一半
,这个代价稍微有点高!
这种算法主要被用来回收新生代,因为新生代中的对象百分之九十八都是“朝生夕死”,也就是说大部分内存都会被回收掉,那就没有必要按照 1:1 的比例划分内存空间,而是将内存分为较大的一块 Eden
空间和两块较小的 Survivor
空间。每次使用 Eden
和其中一块 Survivor
区(from),当回收时将其中的存活对象复制到另外一块 Survivor
区,把 Eden
和刚才用过的 Survivor
区清理掉。HotSpot
虚拟机默认的 Eden
和 Survivor
内存比例是 8:1
,也就是说每次新生代的可用空间为整个新生代容量的 90%,这样内存的利用率很高,一定程度上避免了上面提到的可用内存折半的缺点。但是我们并没有办法保证每次回收都只有不到 10% 的对象存活(因为存活的对象会被复制到 survivor to 区,这部分只占了 10%),这样就有可能出现 Survivor
内存不够用,需要依赖其它内存(老年代)进行分配担保。
显然在对象存活率较高
的情况下这种算法效率就会降低。
标记清除算法(Mark-Sweep)
这时最基础的收集算法,分为 “标记” 和“清除”两个阶段:首先按照上面介绍的方法标记出需要回收的对象,标记完成后统一对被标记的对象内存进行回收。显然这种方式会导致产生大量不连续的内存碎片
,从而导致后面再需要分配较大对象时无法找到足够的连续内存
,从而提前触发另一次垃圾收集。
标记整理算法(Mark-Compact)
老年代由于存活率比较高(想想为什么),因此并不适合上面提到的复制算法,针对其特点,“标记 - 整理
”的算法被提出来。其标记过程与 “标记 - 清除
” 算法的过程一样,但后续并不是直接对标记对象进行清理,而是让存活的对象都向一端移动,然后直接清理掉边界以外的区域。
小结
当代虚拟机都采用 “分代收集” 的思想,一般根据对象存活周期将 Java 堆分为新生代
和老年代
,分别根据其特点选择相应的收集算法:新生代对象
存活率低,则采用 复制算法
只需要对极少比例的存活对象进行复制即可完成收集;而老年代
因为存活率高,没有额外空间对其进行分配担保,就必须使用 “标记 - 清理
”或者 “标记 - 整理
” 算法 来回收。
回收思想 (why)
对任何 “活” 的对象,一定能最终追溯到其存活在
堆栈
或静态存储区
中的引用。
基于此从堆栈
和静态存储区
开始遍历所有的引用,就能找到所有 “活” 的对象,对这些对象进行标记,将其余的对象回收。
- 实现方式
停止 - 复制模式
(对应大空间,回收空间连续)- 先暂停程序运行,将所有活得对象从当前堆复制到另一个堆,没有复制的对象都当作垃圾回收,复制到新堆时对象会被一个挨着一个整齐的排列,这样便可以按照前面说的移动 “堆指针” 的方式直接分配新空间了。当然这种 “复制移动” 式的回收方法效率较低,通常做法是按需从堆中分配几块较大的内存,复制动作发生在这几块
较大的内存之间
。
- 先暂停程序运行,将所有活得对象从当前堆复制到另一个堆,没有复制的对象都当作垃圾回收,复制到新堆时对象会被一个挨着一个整齐的排列,这样便可以按照前面说的移动 “堆指针” 的方式直接分配新空间了。当然这种 “复制移动” 式的回收方法效率较低,通常做法是按需从堆中分配几块较大的内存,复制动作发生在这几块
标记 - 清扫模式
(对应小空间,回收空间不连续)- 前一种 “停止 - 复制” 模式在垃圾较少的情况下效率仍然很低下,因为这时大量的复制行为其实没有必要,于是另一种新的方法。遍历所有引用进而
找到所有存活的对象
并对其标记,标记完成以后将没有标记的对象清理
,这个过程中并不做任何复制。当然这样的话剩下的堆空间并不是连续的
。
- 前一种 “停止 - 复制” 模式在垃圾较少的情况下效率仍然很低下,因为这时大量的复制行为其实没有必要,于是另一种新的方法。遍历所有引用进而
垃圾收集器 (which)
- 串行垃圾回收器:一个GC线程完成回收工作
- 并行垃圾回收器:多个GC线程同时一起完成回收工作,充分利用CPU资源
- 并行和并发
并行
(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。并发
(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。
- 吞吐量(Throughput)
- 吞吐量就是
CPU用于运行用户代码的时间与CPU总消耗时间的比值
- 即: 吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
- 假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
- 吞吐量就是
分类
JVM 1.8
默认的垃圾回收器:Parallel Scavenge
+ParallelOld
,所以大多数都是针对此进行调优
新生代
- Serial收集器
- 最基本、发展历史最悠久的收集器,它是采用复制算法的新生代收集器,曾经(
JDK 1.3.1之前
)是虚拟机新生代收集的唯一选择。 - 一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)
- 这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是
难以接受
的。 - Serial收集器对于运行在Client模式下的虚拟机来说是一个很不错的选择。
- 最基本、发展历史最悠久的收集器,它是采用复制算法的新生代收集器,曾经(
- ParNew 收集器
- Serial收集器的
多线程版本
,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。 - ParNew 收集器在
单CPU
的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。
- Serial收集器的
- Parallel Scavenge 收集器
- 个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个
可控制的吞吐量
(Throughput)。 - 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而
高吞吐量则可以高效率地利用CPU时间
,尽快完成程序的运算任务
,主要适合在后台运算而不需要太多交互的任务
。
- 个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个
老年代
- Serial Old收集器
- Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。 此收集器的主要意义也是在于给
Client模式
下的虚拟机使用。
- Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。 此收集器的主要意义也是在于给
- Parallel Old收集器
- 是
Parallel Scavenge
收集器的老年代版本,使用多线程和“标记-整理”算法,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
- 是
- CMS收集器
- 以获取
最短回收停顿时间
为目标的收集器,它非常符合那些集中在互联网站或者B/S
系统的服务端上的Java应用 - CMS收集器工作的整个流程分为以下4个步骤:
初始标记
(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。并发标记
(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。重新标记
(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。并发清除
(CMS concurrent sweep)
- 由于整个过程中耗时最长的
并发标记
和并发清除
过程收集器线程都可以与用户线程一起工作。所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
- 以获取
G1收集器
收集器是当今收集器技术发展最前沿的成果之一,它是一款面向
服务端
应用的垃圾收集器
HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉
JDK 1.5中发布的CMS
收集器。
并行与并发
G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集
与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
空间整合
G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
可预测的停顿
这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了
总结表格
回收时间 (when)
Partial GC
(局部 GC): 并不收集整个GC
堆的模式Young GC
: 只收集young gen
的GC
,Young GC
还有种说法就叫做 "Minor GC
"Old GC
: 只收集old gen
的GC
。只有垃圾收集器CMS
的concurrent collection
是这个模式Mixed GC
: 收集整个young gen
以及部分old gen
的GC。只有垃圾收集器G1
有这个模式
Full GC
: 收集整个堆,包括新生代
,老年代
,永久代
(在 JDK 1.8及以后,永久代被移除,换为metaspace
元空间)等所有部分的模式
收集策略 (policy)
Yong GC / Minor GC 的触发
当
Eden
区的空间耗尽时 Java 虚拟机便会触发一次Minor GC
来收集新生代的垃圾
存活下来的对象,则会被送到Survivor
区
在进行Yong GC时,通常会使用一种称为"Stop-the-World
"的方式来暂停应用程序的执行。
Yong GC / Minor GC 过程
当发生
Minor GC
时,Eden
区和from
指向的Survivor
区中的存活对象会被复制(此处采用标记复制
算法)到to
指向的Survivor
区中
然后交换from
和to
指针,以保证下一次Minor GC
时,to
指向的Survivor
区还是空的。
from
与to
只是两个指针,它们变动的,to
指针指向的Survivor
区是空的。
Old GC / Major GC / Full GC 的触发
- 显式的调用
System.gc()
- 应
避免
在代码中显式调用此方法,让虚拟机自己去管理它的内存
- 应
serial GC
中- 老年代内存剩余已经小于之前年轻代晋升老年代的平均大小
CMS
等并发收集器中- 每隔一段时间检查一下老年代内存的使用量,超过一定比例时进行
Full GC
回收
- 每隔一段时间检查一下老年代内存的使用量,超过一定比例时进行
Old GC / Major GC 过程
进行
Major GC
(大型垃圾回收)时,主要是针对Java堆中的老年代
进行回收
首先会标记老年代中的存活对象
,然后清理未被标记的对象
,释放它们所占用的内存空间。
可能会导致应用程序的停顿
,因为在执行垃圾回收时需要暂停应用程序的执行
FULL GC 过程
对整个Java堆进行垃圾回收的过程
- 暂停应用程序:为了执行FULL GC,垃圾回收器会暂停应用程序的执行。这个停顿时间的长短取决于具体的垃圾收集器和应用程序的大小。
- 年轻代垃圾回收:首先,垃圾回收器会执行年轻代的垃圾回收,类似于Yong GC的过程。它会标记和清理年轻代中的垃圾对象,并将存活对象复制到生存带或老年代中。
- 生存带垃圾回收:接下来,垃圾回收器会对生存带进行垃圾回收。生存带是年轻代中的两个区域之一,用于存放经过一次年轻代垃圾回收后仍然存活的对象。垃圾回收器会标记和清理生存带中的垃圾对象,并将存活对象复制到另一个生存带或老年代中。
- 老年代垃圾回收:最后,垃圾回收器会对老年代进行垃圾回收。老年代是存放存活时间较长的对象的区域。垃圾回收器会标记和清理老年代中的垃圾对象,并释放它们所占用的内存空间。
- 内存整理:在FULL GC过程中,垃圾回收器可能会进行内存整理操作,以减少内存碎片化。它会将存活的对象进行整理,使得内存布局更加紧凑。
恢复应用程序执行:完成FULL GC后,垃圾回收器会恢复应用程序的执行,使其继续运行。
Minor GC vs Major GC
Minor GC
可能会引起短暂的STW
暂停。当进行Minor GC
时,为了确保安全性,JVM 需要在某些特定
的点上暂停所有应用程序的线程,以便更新一些关键的数据结构。
这些暂停通常是非常短暂的,通常在毫秒级别
,并且很少对应用程序的性能产生显著影响。
Major GC
的暂停时间通常会比Minor GC
的暂停时间更长
,因为老年代的容量通常比年轻代大得多。
这意味着在收集和整理大量内存时,需要更多的时间来完成垃圾收集操作。
Survivor 区对象晋升位老年代
JVM 会记录
Survivor
区中的对象在from
和to
之间一共被来回复制的次数
一个对象被复制的次数为15
(参数-XX:+MaxTenuringThreshold
),该对象将被晋升为至老年代
OR
单个Survivor
区已经被占用了 50%(参数:-XX:TargetSurvivorRatio
)
复制次数较高的对象也会被晋升至老年代
比例关系
默认JVM试图分配最大内存的总内存的1/4
,初始化默认总内存为总内存的1/64
,年青代中heap的1/3
,老年代占2/3
总结安装Nexus步骤和私有仓库实现
安装
# 安装要求
# 要求内存8G以上,太小比如4G以下会导致无法启动
# 下载 (需要外网)
> https://help.sonatype.com/en/download.html
wget https://download.sonatype.com/nexus/3/nexus-3.66.0-02-unix.tar.gz
# 解压
tar -xf nexus-3.66.0-02-unix.tar.gz -C /usr/local/
# 链接文件
ln -s /usr/local/nexus-3.66.0-02/ /usr/local/nexus
ln -s /usr/local/nexus/bin/nexus /usr/bin/
# 配置
## 运行用户配置
vim /usr/local/nexus/bin/nexus
run_as_user="root"
## 运行端口配置
vim /usr/local/nexus/etc/nexus-default.properties
application-port=8081
application-host=0.0.0.0
nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-http.xml,${jetty.etc}/jetty-requestlog.xml
nexus-context-path=/
...
## JVM 优化配置
vim /usr/local/nexus/bin/nexus.vmoptions
-Xms2703m
-Xmx2703m
-XX:MaxDirectMemorySize=2703m
...
运行
前台运行
nexus run
可以看到运行状态,方便确认启动日志
后台运行
nexus start
关闭运行
nexus stop
配置启动脚本
-
vim /etc/systemd/system/nexus.service
[Unit] Description=nexus service After=network.target [Service] Type=forking LimitNOFILE=65536 ExecStart=/usr/local/nexus/bin/nexus start ExecStop=/usr/local/nexus/bin/nexus stop User=root #User=nexus Restart=on-abort [Install] WantedBy=multi-user.target
-
重新加载daemon
systemctl daemon-reload
首次登录
- 查看密码
/usr/local/sonatype-work/nexus3/admin.password
- 登录重置
- 确认访问方式 (默认为匿名登录,生产建议打开匿名访问功能,无需登录就可以下载资源)
- 管理员界面
自建apt仓库
/etc/apt/sources.list
deb http://192.168.100.121:8081/repository/ubuntu-proxy/ focal main restricted universe multiverse